Skip to main content

On This Page

How to Build an AI-Driven Property Management Email Agent Without Shared Inbox Chaos

4 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Triage tenant maintenance requests with a property-management agent

Developer Qasim presents a property-management agent architecture that gives the building its own mailbox—[email protected]—as a first-class participant. Every tenant request is ingested, classified by urgency using an LLM, and routed to the correct priority queue without a human touching the inbox at 11pm.

Why This Matters

Most ‘AI for property management’ solutions point a model at a human’s mailbox to ‘help them keep up,’ but the real bottleneck is not reading—it’s that a leaky faucet, a dead furnace in January, and a ‘porch light is out’ all arrive at the same address with zero structure, forcing a human to decide which one is an emergency. Giving the property its own mailbox as a first-class participant—with LLM-based content classification and server-side sender routing—eliminates the human triaging at 11pm and removes the ‘did anyone see this one?’ failure mode entirely.

Key Insights

  • Structural limitation: inbound Rules match only sender fields (from.address, from.domain, from.tld) and cannot read subject or body—so urgency classification must happen in application code, not in the rule engine.
  • Provisioning simplicity: a maintenance Agent Account is created with a single POST to /v3/connect/custom using provider “nylas”—no OAuth dance, no refresh token to babysit, just one API call.
  • Deduplication pattern: Nylas guarantees at-least-once delivery, meaning the same event can arrive up to three times; deduplication on the top-level notification id (not the message id) is required to prevent double dispatching and double acknowledgment emails.
  • Architectural boundary: sender-based routing belongs server-side in Rules (before the app wakes up), while content-based urgency classification belongs in application code—get this wrong and you waste a weekend trying to match urgent keywords in a Rule that structurally ignores the subject.
  • State management constraint: Agent Account resources do not support custom metadata, so priority, state, and turn counts must live in the application’s own database (Postgres/Redis) keyed by message ID.

Working Examples

Provisions a new Agent Account (grant) for the property’s own mailbox, eliminating the need for a shared human inbox.

curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"provider": "nylas",
"name": "Oakwood Maintenance",
"settings": {
"email": "[email protected]"
}
}'

CLI one-liner to create the same Agent Account, printing the grant_id for all subsequent API calls.

nylas agent account create [email protected] --name "Oakwood Maintenance"

Webhook handler skeleton that acknowledges immediately, verifies signature, filters to the maintenance grant, prevents self-triggering loops, then queues the request for AI-based classification.

app.post("/webhooks/nylas", async (req, res) => {
  res.status(200).end(); // ack fast; verify X-Nylas-Signature first in real code
  const event = req.body;
  if (event.type !== "message.created") return;
  const msg = event.data.object;
  if (msg.grant_id !== MAINTENANCE_GRANT_ID) return;
  if (msg.from?.[0]?.email === "[email protected]") return;
  await intake(msg);
});

CLI commands to create a notification list of vendor domains and a server-side rule that routes those emails before any application code runs.

# Add domains to notification list
nylas agent list create --name "Known vendors" --type domain \
  --item acme-hvac.com --item reliable-plumbing.example

# Create rule to route by sender
nylas agent rule create \
  --name "Route known vendors to a folder" \
  --trigger inbound \
  --condition from.domain,in_list,<LIST_ID> \
  --action assign_to_folder=<VENDORS_FOLDER_ID> \
  --action mark_as_read

At-least-once delivery deduplication pattern: guard on top-level notification id with an atomic insert, protecting against double dispatching and double acknowledgment emails.

const notificationId = event.id; // constant across redeliveries of one event
const wasInserted = await db.processedEvents.setIfAbsent(notificationId, {
  receivedAt: Date.now(),
});
if (!wasInserted) return; // already handled this delivery — bail

Practical Applications

  • Automated maintenance intake: Tenants email the property’s own mailbox directly; the agent fetches the full message body, classifies it into one of four priority levels (emergency, high, normal, low) using an LLM, and moves it into the corresponding folder—a plumber is dispatched within seconds for a ceiling leak without any human triage.
  • Known-vendor routing: HVAC contractors, insurance adjusters, and building owners send from known domains like acme-hvac.com; a server-side Rule (matching from.domain with an in_list operator) drops those messages into a vendor folder and marks them read, so vendor traffic never competes with tenant requests for the model’s attention.
  • Threaded status updates: When the agent acknowledges a request or pushes a status, it sends the reply from [email protected] using reply_to_message_id—this sets In-Reply-To and References headers automatically, so the tenant’s response lands inside the existing conversation and the full thread history is preserved.

References:

Continue reading

Next article

Automated MTProto Proxy Scraper: Fresh Telegram Proxies via CI/CD

Related Content