Scheduled follow-ups
How an agent schedules work for later and delivers the result back to the channel it came from — durable tasks, atomic claiming, and recovery.
Some things cannot be answered inside a turn: a check that takes minutes, a reminder for next Tuesday, a renewal conversation to open six weeks from now. The agent can register that work and let the answer arrive when it is ready.
The mechanism
Mid-turn, the agent calls schedule_followup for work that cannot finish inside the turn: a check
that takes minutes, a reminder for next Tuesday, a renewal conversation six weeks out.
The task is a row in Postgres, not a message in a broker. A pending task is committed data, so a redeploy or a crash does not lose it — durability comes free from the store already in use.
A poller wakes every few seconds and looks for tasks that are due. The cost is granularity: a task fires on the next poll rather than to the millisecond, which for follow-ups measured in minutes to weeks is not a meaningful loss.
Due tasks are claimed with FOR UPDATE SKIP LOCKED. One poller does not need that — writing it
correctly now means a second instance later cannot double-send, and a follow-up delivered twice
costs the customer's confidence in the sender.
The result is generated. A task claimed by a process that then dies is returned to pending by a recovery pass rather than sitting in-progress forever.
Delivery goes back to the channel the request came from — a bubble in the web widget, a message in Telegram or WhatsApp. Someone who asked on WhatsApp is answered on WhatsApp.
The tool returns immediately — the customer gets their reply now, and the scheduled result later.
The tool does not block the turn. It writes a row and returns, so the customer gets their reply now and the scheduled result later.
Why the queue is a database table
There is no Redis, no broker. Tasks are rows in Postgres, and that is a deliberate trade:
- Restarts do not lose work. A pending task is a committed row. The process can be redeployed mid-flight and the poller picks the task up on the way back — durability comes free with the data store already in use, instead of from a second system with its own failure modes.
- One fewer thing to operate. For a single-process deployment, a poller loop is enough.
The cost is latency granularity: a task fires on the next poll rather than to the millisecond. For follow-ups measured in minutes to weeks, that is not a meaningful loss.
Claiming is atomic
Due tasks are claimed with FOR UPDATE SKIP LOCKED. One poller cannot need that — but writing it
correctly now means running a second instance later does not double-send. A follow-up delivered
twice is not a cosmetic bug: it is the customer receiving the same "your renewal is due" message twice
and losing confidence in the sender.
SKIP LOCKED also means a slow task does not block the queue behind it; other workers step over the
locked row and take the next one.
Recovery
A task claimed by a process that then dies would otherwise sit in-progress forever. A recovery pass returns stuck tasks to pending so they are retried rather than silently dropped.
Delivery follows the origin
The result is pushed back to the channel the request came from, not to a channel of the platform's choosing. Someone who asked on WhatsApp is answered on WhatsApp. The tenant's own scheduled outreach — renewal reminders, deadline chasers — works the same way, on the cadence you configure.
What this means in practice
- An agent can say "I'll come back to you on this" and actually do it.
- Reminders survive a deploy, a restart, and a crash.
- A follow-up is delivered once, and reaches the person where they were talking to you.
The business case for follow-ups that don't depend on anyone remembering is on Your time back; scheduling is one of the built-in tools described in Tools and MCP.