Use case
An AI agent that follows up until someone replies.
SendRaven gives an AI agent what an email follow-up needs: the first message and every nudge in one thread, a flag that turns true when a person answers and stays false for an out-of-office, and a transcript to count the nudges from. Your code decides whether to follow up. The model only decides what to say.
200 { "awaiting_reply": false,
"handled_at": null,
"messages": [
{ "direction": "outbound",
"status": "delivered", … }] }{ "reply_to_message_id": "0d87…", … }
202 { "status": "sent",
"thread_id": "1276…" }The problem
“Follow up if they don’t answer” is three questions a send API cannot answer.
Did a person reply, or only their autoresponder? How many nudges have gone out already? Did a colleague answer from their own inbox while the agent was waiting? Get any of them wrong and the agent either goes quiet on someone who is still waiting, or chases someone who already said yes.
With SendRaven the answers are fields on the thread. Replies arrive on your domain and join the conversation they answer, automated mail is recognised and left out of the flag, and every message in both directions is in one transcript the agent reads on each pass. Nothing has to be remembered between runs.
The decision
Code decides whether to act, from the transcript alone.
The example reads the thread on every pass and returns one state. Every state comes from a field the API returns, never from local memory, so a lost state file cannot cause a second send.
The model never picks the recipient, the thread or the count. The tools it calls are bound to one task, and the follow-up tool checks the state again before it sends, because the person may have replied while the model was writing.
- replied:
awaiting_replyis true. Stop, and read the answer. - handled:
handled_atis set, or an outbound message the agent did not send is in the thread. Someone else answered; hand it back. - undeliverable: the last message’s status is
bouncedorcomplained. - pending: an outbound message is
queuedorscheduled, held for approval or waiting its turn. - waiting: the delay since the last message has not passed.
- exhausted: the maximum number of follow-ups has gone out. Hand it to a person.
- due: none of the above. Write the next follow-up.
Technical proof
The follow-up is a reply to your own message.
POST /v1/emails
Authorization: Bearer sk_live_…
Idempotency-Key: followup-4763e36a-1
{ "from": "Sam at Acme <sam@mail.acme.com>",
"to": ["lee@example.org"],
"subject": "Re: Thursday at 10:00?",
"text": "Just checking whether Thursday still works…",
"reply_to_message_id": "0d87…" }
202 { "id": "047c…",
"status": "sent",
"thread_id": "1276…",
"scheduled_at": null,
"skipped": false,
"reason": null,
"approval_id": null }reply_to_message_id is the id of the agent’s last message, so the nudge sits under the original in the recipient’s mail client instead of arriving as a new conversation.
The idempotency key names the task and the follow-up’s number. A polling run and a webhook that both decide follow-up 1 is due send it once. The second request gets the first one’s response replayed, or, if the model wrote different words, 422 idempotency_key_reused. Either way, nothing goes out twice.
If the recipient is suppressed, the send answers skipped: true and status: "rejected" with a reason, instead of mailing someone who opted out or hard-bounced.
What stops it
A person’s reply stops it. An autoresponder does not.
- An out-of-office is not an answer. Automated mail (out-of-office replies, bounce reports, delivery notices) is recognised from its headers and recorded on the thread without setting
awaiting_reply. The follow-ups carry on, and the agent never reads the autoresponder as a yes. - A colleague’s answer counts. If someone replies from the dashboard or marks the thread handled,
awaiting_replygoes false with a message the agent did not send, orhandled_atset. The agent hands the thread back rather than nudging a person who already has an answer. - A bounce ends it. A hard bounce suppresses the address, and the transcript shows the status. There is no one to follow up with.
- The count is a hard stop. After the last allowed follow-up the task is exhausted and goes to a person. For a ceiling the code cannot get wrong, give the key a daily send limit and a recipient allowlist; the API enforces both.
Polling or webhooks
The webhook says something arrived. The thread says what.
Run the check from cron every fifteen minutes, or react to the inbound webhook the moment a reply lands. Either way the decision reads the thread. The webhook fires for automated mail too, and its payload does not say which it was, so treat it as a prompt to re-read the thread and never as proof of a reply.
Keep a timer even in webhook mode. A reply that never comes produces no event, and silence is exactly the case a follow-up exists for. Webhook deliveries are signed with X-CN-Signature; the example verifies it before doing anything. Webhooks.
Or schedule it up front
A follow-up that goes out even when the agent is not running.
POST /v1/emails
{ "reply_to_message_id": "0d87…",
"scheduled_at": "in 3 days",
… }
202 { "status": "scheduled", … }
# the answer arrived first:
DELETE /v1/emails/047c…Send the follow-up at the same time as the first message, scheduled for later, and cancel it when the reply comes in. It goes out on time even if your agent is down for three days, and a scheduled message does not mark the thread answered while it waits.
The trade-off is that the nudge is written before anyone knows whether it will be needed or what it should say. Follow up when nobody replies walks through both ways.
Where it fits
Any task that waits on one person’s answer.
- Scheduling: a meeting time proposed and not yet confirmed.
- Collections and paperwork: a document, a signature or a form someone said they would send.
- Sales and research: a question to a lead or an expert, answered or handed back after two nudges.
- Vendor and partner requests: a quote, a confirmation, a date. The reply is read and summarised, and the task carries on with the answer.
The example uses the OpenAI Agents SDK, and the calls are plain REST, so the same loop works in any framework, or over the MCP server.
Questions
How do I make an AI agent follow up on an email when there is no reply?
Send the first email through SendRaven, keep its thread_id, and check GET /v1/threads/:id on a timer. If awaiting_reply is still false and the delay since the last outbound message has passed, send a follow-up with POST /v1/emails and reply_to_message_id set to your last message, so it lands in the same thread. Stop when awaiting_reply turns true or you reach your maximum number of follow-ups.
How does the agent know a person replied and not an autoresponder?
It reads awaiting_reply on the thread. SendRaven recognises out-of-office replies and bounce reports from their headers and records them on the thread without setting awaiting_reply, so the flag turns true only for mail a person wrote. The inbound webhook fires for both, which is why it should trigger a re-read of the thread rather than count as a reply.
How do I stop an AI agent from sending too many follow-ups?
Count the follow-ups from the thread's transcript on every pass, never from memory, and derive each send's Idempotency-Key from the task and the follow-up number so two runs cannot both send the same one. For a limit the agent's code cannot get wrong, give its API key a daily send limit and a recipient allowlist, which the API enforces on every send.
Can the follow-up be scheduled so it goes out even if the agent is not running?
Yes. Send it right after the first email with reply_to_message_id and scheduled_at, for example "in 3 days", and cancel it with DELETE /v1/emails/:id if the reply arrives first. The scheduled message does not mark the thread as answered while it waits.
Does this work with the OpenAI Agents SDK?
Yes. SendRaven's public examples include a follow-up agent built on the OpenAI Agents SDK, with plain Python deciding when to act and the model writing the first email, each follow-up and the summary of the reply. It runs by polling or from the inbound webhook, and the same REST calls work from any framework or through the MCP server.
Related docs
The pages behind each step.

Send once. Nudge twice. Stop when they answer.
Verify a domain, publish its inbound record so the replies come back, and run the follow-up example against it. Inbound mail is included on every plan.

