Building real-time AI agents with email webhooks
Stop polling. Subscribe to inbound Agent Inbox events, verify the webhook, make handlers idempotent, then hand the thread to a Neuro OS role.
Discuss this post in AI
Send a pre-filled prompt to ChatGPT, Claude, Gemini, or Perplexity — get a summary, ask follow-ups, or compare ideas from this guide.
Polling an inbox every thirty seconds looks simple until it is not. You pay for empty reads, you race two workers on the same unread flag, and you still miss a burst when the poller is restarting. Email is an event stream. Treat it that way.
Agent Inbox exposes an API mailbox. Subscribe to inbound delivery, verify the webhook, process each event once, and hand the thread to a Neuro OS role. The role drafts a reply. Outbound mail stays on Ask. This article is the engineering loop, not a product announcement of IMAP.
Why polling fails in production
A cron that lists new messages has no natural backpressure. If processing is slower than arrival, unread counts grow and the next poll overlaps the last. Duplicate handling becomes folklore: “we mark seen first” versus “we mark seen after success.” One choice drops mail on a crash; the other double-sends.
Long poll intervals make OTP flows feel broken. Short intervals hammer the API and still cannot beat a webhook that fires when the message exists. Keep a slow reconciliation poll as a safety net, not as the primary path.
Subscribe, then verify
Create the inbox, then register an HTTPS endpoint the platform can reach:
from agentinbox import AgentInbox
client = AgentInbox()
inbox = client.inboxes.create(username="events", domain="agentinbox.space")
The handler should reject requests that fail signature or shared-secret checks. Do not trust a payload because it arrived on a URL you thought was secret. Respond quickly with 2xx after the event is durably queued. Do the model work off the request thread so retries do not stack inside a 30-second HTTP timeout.
Rotate secrets like any other webhook. Log the event id, not the full body, unless you are in a controlled debug window.
Idempotency is the real feature
Every webhook will retry. Your table key is the provider event id, or a hash of inbox id plus message id. Insert that key in the same transaction that enqueues work. If the insert conflicts, return 200 and stop. Do not start a second Neuro OS run for the same message.
Store enough to replay: inbox id, thread id, message id, received time. Do not store credentials. If the role needs the body, fetch it again from the API with the message id rather than trusting a stale copy in the webhook cache.
Hand the thread to a role
The webhook worker is not the agent. It authenticates, deduplicates, and starts a role with the thread id. The role loads the conversation, classifies, and either extracts (OTP, invoice, routing) or drafts a reply. Sends default to Ask. If a human must see the raw thread, forward it into Neuro OS rather than paging a Slack channel with the full email.
Retries of the worker must not retry the send. The send is a separate state machine: draft created, approved, delivered. A webhook retry that re-enters “draft” is fine if the draft id already exists; it is not fine if it creates a second draft and a second Ask card.
Operate the loop
Alert on webhook error rates, handler lag, and Ask queue age. A real-time agent that drafts in two seconds and waits three hours for approval is not real-time for the customer. Staff the queue. Keep a dead-letter path for signatures that fail or payloads you cannot parse.
Start with one inbox and one event type — inbound received. Add bounce and delivery receipts later. The architecture is small: verified events, exactly-once enqueue, a role that may not send without Ask. That is enough to retire the poller as the source of truth.
Agent Inbox gives each role a mailbox people can reply to, with forwarding into Neuro OS when a human must see the thread. Outbound mail defaults to Ask. Run the role on Neuro OS. To scope the first inbox, get started.