Build a webhook-driven email pipeline for your AI agent A developer from Nylas built a webhook-driven email pipeline for AI agents, replacing wasteful polling with an event-driven architecture. The pipeline uses Nylas Agent Accounts, a queue for at-least-once delivery, and workers for processing, with explicit handling of idempotency, retries, and backpressure. Most "AI email" tutorials end with a while True loop that polls an inbox every thirty seconds, runs the new messages through a model, and sends a reply. It demos fine. Then you put it in front of real traffic and the cracks show up immediately: you're burning API calls to fetch nothing 99% of the time, your reaction latency is bounded by your poll interval, and the moment you scale to more than one inbox the polling cost multiplies. Polling an agent's inbox is wasteful. Webhooks are the right primitive for this. The mailbox already knows the instant a message lands — there's no reason to keep asking. What you actually want is a pipeline: inbound mail fans out to a verified ingest endpoint, lands on a queue, and gets picked up by workers that drive your agent runtime and send the reply. This post is about that architecture end to end — not a single feature, but the whole flow, with the parts that bite you in production idempotency, retries, ordering, backpressure called out honestly. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm wiring this up. I'll show the curl HTTP call and the CLI equivalent for every concrete step, because you'll use both: curl in your provisioning scripts, the CLI when you're poking at a live account. Before any of the pipeline matters, here's the one abstraction that makes the whole thing simple. An Agent Account is a Nylas grant — it has a grant id , an inbox, an email address on a domain you own, and it speaks every grant-scoped endpoint you already know: Messages, Threads, Folders, Drafts, Attachments, Calendars, Events, Contacts, Webhooks. There's no OAuth token to refresh, no provider-specific quirks, no separate SDK. If you've built against a connected Gmail or Microsoft grant before, the data plane is identical. Nothing new to learn there. What's different is that the agent is a participant. It has its own address — support@yourcompany.com — rather than borrowing a human's inbox. That's what makes the event-driven design clean: one grant, one inbox, one webhook stream, no shared-mailbox coordination problem. Provision one with POST /v3/connect/custom : 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", "settings": { "email": "support@yourcompany.com" }, "name": "Support Agent" }' Or the CLI, which creates the nylas connector for you if it doesn't exist yet and auto-provisions a default workspace and policy: nylas agent account create support@yourcompany.com --name "Support Agent" The settings.email has to be on a domain you've registered with Nylas a custom domain, or a .nylas.email trial subdomain to start . New domains warm over roughly four weeks before you push real volume through them. Hold onto the grant id you get back — every endpoint below is scoped to it. Here's the flow. Each arrow is a place where something can go wrong, and naming them is half the work: inbound mail │ message.created one app-scoped endpoint, all grants ▼ ingest endpoint ── verify X-Nylas-Signature HMAC-SHA256 │ ── return 200 immediately │ enqueue {notification id, grant id, message id, thread id} ▼ queue ── at-least-once; dedup on notification id │ ▼ worker ── full message via API re-fetch on .truncated │ ── per-thread lock ▼ agent runtime ── generate the reply │ ▼ send ── POST .../messages/send ── delivered / bounced / complaint / rejected The guiding principle: the ingest endpoint does as little as possible. It verifies the signature, drops a small job on the queue, and returns 200 . Everything expensive — re-fetching a truncated message, calling a model, sending — happens in a worker behind the queue. This isn't just tidy; it's what keeps Nylas from retrying you. If your endpoint is slow to ack, Nylas assumes the delivery failed and sends the event again. One thing to get straight before you wire anything up: webhooks in Nylas are application-scoped, not grant-scoped. You subscribe once at the app level with POST /v3/webhooks , and events for every grant in the application — every Agent Account you've provisioned — arrive at that single endpoint. Each payload carries a grant id , and routing to the right agent is your handler's job, not a subscription setting. So if you run several agents support@ , sales@ , scheduling@ , they share one webhook stream and you fan out on grant id downstream. You want message.created for inbound, and the four deliverability triggers for outbound visibility — message.delivered , message.bounced , message.complaint , and message.rejected . Agent Accounts emit all four; connected grants don't. curl --request POST \ --url "https://api.us.nylas.com/v3/webhooks" \ --header "Authorization: Bearer $NYLAS API KEY" \ --header "Content-Type: application/json" \ --data '{ "trigger types": "message.created", "message.delivered", "message.bounced", "message.complaint", "message.rejected" , "webhook url": "https://pipeline.yourcompany.com/webhooks/nylas", "description": "Support agent inbound + deliverability" }' The CLI version, which I use constantly because it's one line: nylas webhook create \ --url https://pipeline.yourcompany.com/webhooks/nylas \ --triggers message.created,message.delivered,message.bounced,message.complaint,message.rejected \ --description "Support agent inbound + deliverability" Both return a webhook record with a webhook secret . Save it somewhere your ingest endpoint can read it — that secret is the signing key for the next step. If you want to develop against real webhooks without deploying, the CLI ships a local receiver. nylas webhook server --tunnel cloudflared --secret