Long-running email conversations: waiting, timeouts, and resuming

An agent run is short. An email conversation is not. Somewhere between "I asked" and "they answered" sits a gap of hours or days that your architecture has to cross, and how you cross it determines whether the agent is cheap and reliable or expensive and forgetful.

5 min read

A conversation held open across days while the agent run ends and resumes
The conversation outlives the run. Something has to hold the state, and it should not be a loop.

Three ways to cross the gap

ApproachHow it worksWhat it costs
PollA scheduled job wakes the agent to check for new mailLatency you choose, requests you waste, and state you store yourself
Blocking waitThe agent calls wait_for_reply and the server returns when the reply landsOne held call, no scheduler, no state of your own
Event-drivenA message.received webhook starts a fresh runLowest latency, but you supply the context on resume

Most production agents use two of these: a blocking wait inside an active conversation, and webhooks for anything arriving cold. Polling is the one to avoid, and it is usually chosen by accident because it is what an IMAP client would have done.

The blocking wait

wait_for_reply parks the conversation server-side and returns when the other party writes back or the timeout expires. The agent does not loop, and there is no cron entry to maintain.

send_email { to: ["lead@prospect.io"], subject: "Demo times", body: "..." }
wait_for_reply { thread_id: "thr_...", timeout_seconds: 172800 }
reply_all { message_id: "msg_...", body: "Booked for Tuesday at 10:00." }

This is the right tool when the agent is in the middle of something and the next step depends on the answer: proposing interview slots, confirming a payment date, asking the one qualifying question. The run stays coherent, and the reply arrives in the same context that produced the question.

It is the wrong tool for a run you cannot afford to keep alive, or for a wait measured in weeks. In those cases, end the run and let an event start the next one.

Choosing a timeout

The timeout is a product decision disguised as a parameter. Set it to the point at which silence becomes information.

  • Warm inbound lead: one to two days. After that the lead has moved on and a follow-up is a new conversation, not a continuation.
  • Customer mid-troubleshooting: a few hours during working hours. Any longer and you should close the thread and let them reopen.
  • Candidate in a process: a week. People interview while employed, and pressure costs you more than patience.
  • Invoice awaiting payment confirmation: three to five days, aligned to your dunning steps rather than to a round number.
  • Anything where a human is waiting on the other side: short, and prefer an interim acknowledgement over a long silence.

When it expires, the default should be one follow-up and then silence. The temptation is a schedule of four, and that is where complaints and reputation damage come from, as covered in keeping an autonomous agent from becoming a spam problem.

Timeout ranges plotted against how long each kind of conversation stays alive
Set the timeout to the point where silence starts to mean something.

Resuming cleanly

An event-driven resume gives you a message, not a memory. Rebuilding enough context to answer well takes three calls and no storage of your own.

  1. get_thread for the conversation so far, already stripped of quoted history.
  2. lookup_contact for who this is and any facts previous runs recorded.
  3. Your own systems for the current state of whatever the thread is about: the order, the invoice, the pipeline stage.

That is usually enough to produce a reply indistinguishable from one written by a run that never ended. What is not in those three places should be written back at the end of each run with remember_about_contact or a note, which is what makes the second conversation good rather than merely correct. The tools are described in contacts, notes, and vault.

The full map of which store holds what is in where each piece of an agent's state belongs.

Idempotency, because email retries

Two things arrive twice in email systems: webhook deliveries, which retry on failure, and human replies, which sometimes fork a thread into two branches. Neither should produce two answers.

Key your work on the message id rather than on the thread, record that you have handled it, and make the send itself the last step so a crash mid-run leaves you with no reply rather than two. Signed webhook deliveries with retries are described in webhooks, and the choice between them and polling is worked through in webhooks or polling.

Out of office, auto-replies, and other false positives

A blocking wait returns on the first inbound message, and the first inbound message is quite often an autoresponder. An agent that treats "I am on leave until the 14th" as an answer will produce a confused reply and burn a follow-up.

Check before you act. Auto-submitted and precedence headers catch most of them, and a short list of phrases catches the rest. The correct behaviour is usually to keep waiting, with the timeout extended past the date in the message when there is one, which is a nice example of a small piece of understanding paying for itself.

When the conversation should end

The hardest part of a long-running conversation is knowing when it is over. Useful stopping rules, all of which are better decided in code than by the model:

  • The task completed. Archive and stop.
  • The human took over. Stay out unless invited back.
  • One follow-up sent, no response. Close the loop with a final short message and stop.
  • Any negative signal, including a soft one. Suppress and stop.
  • The thread has run past a length that suggests the agent is not going to resolve it. Escalate.

An agent that stops cleanly is trusted with more work than one that has to be watched. That is the whole return on this section.

Questions

How does an AI agent wait for an email reply?
With a blocking call, wait_for_reply, which holds the thread server-side and returns when the reply arrives or the timeout expires. No polling loop and no external scheduler.
What timeout should I use?
Set it to the point where silence becomes information: a day or two for a warm lead, a week for a candidate, hours for someone mid-troubleshooting. Then send at most one follow-up.
Should I poll for new mail instead?
Only if you cannot receive webhooks. Polling costs latency and wasted requests, and it makes you store state that the thread already holds.
How do I resume a conversation after the run ends?
Fetch the thread, look up the contact, and read your own system for current state. Write anything else worth knowing back to the contact record at the end of each run so the next resume is cheap.
What stops the agent replying twice?
Idempotency keyed on the message id, plus making the send the final step of the run. Webhook deliveries retry by design, so a handler that is not idempotent will eventually double-send.
How should the agent handle an out of office reply?
Recognise it and keep waiting rather than treating it as an answer, extending the wait past the return date when the message gives one.

Give your agent an address it can answer from.

Create an inbox