Your Inbox Agent Wakes Every 15 Minutes to Find Nothing. Cron or Event Trigger? Decide by What a Wasted Run Costs.
The trigger is the least-discussed design choice in a personal agent, and it quietly decides your bill, your duplicate rate and how stale its answers are. A simple test for choosing between a schedule and an event, and why the best setup usually uses both.
An inbox triage agent that runs on a 15-minute cron wakes up 96 times a day. On a quiet Sunday, most of those runs load the system prompt, list the tool definitions, call list_unread, get back an empty array and exit. Each run is cheap. Ninety-six of them a day, every day, is not, and the cost is the smaller of the two problems.
How an agent gets started rarely gets the attention its prompt or its tools get. But the trigger decides three things you will care about in production: what you pay when nothing is happening, how late the agent is when something does happen, and whether two copies of it ever end up working on the same email.
The two options, stated plainly
Schedule (polling). A timer starts the agent every N minutes. The agent asks "is there anything to do?" and either works or exits.
Event trigger (push). Something outside the agent, such as a mail webhook, a queue message or a file landing in a folder, starts a run and hands it the item that caused it.
Neither is the "advanced" option. They fail in different ways, and the choice comes down to which failure you can afford.
⚠️ How a schedule goes wrong
- You pay for empty runs. Every wake-up pays for the fixed part of the prompt even when there is no work. Suppose that fixed part costs a fraction of a cent per run. Multiply by 96 runs a day and by every user, and you have a line item that buys nothing.
- Latency is built in. With a 15-minute interval, an email that arrives one minute after a run waits about 14 minutes. Shortening the interval cuts the wait and multiplies the empty runs, and you can't have both.
- Runs overlap. A busy Monday morning gives the 9:00 run forty emails. It is still drafting replies at 9:15 when the next run starts, sees the same unread messages, and starts drafting replies to them too. Now the user has two drafts for the same thread, or worse, two sent replies.
The third one is the bug that ships. The first two only show up on the bill and in user complaints.
⚠️ How an event trigger goes wrong
- Bursts fan out. A mailing-list digest drops thirty messages in a second. Thirty events start thirty agent runs in parallel, and each one sees only its own email, so none of them can tell that the thirty belong together.
- Events get lost. Webhooks fail, get retried or arrive out of order. If the agent only runs when it is told to, a dropped event means an email that never gets triaged, and nothing ever notices.
- Events get delivered twice. Most delivery systems promise "at least once." The agent has to treat a repeat as normal, not as a new email.
🔍 The deciding question
Ask one question: when a run happens and there is nothing to do, what did it cost, and when there is something to do, how long can it wait?
- Work is rare and the wait matters (a support escalation agent, an on-call alert summariser) → event trigger. Empty runs would be the norm, and the latency hurts.
- Work arrives steadily and batching helps (a daily expense categoriser, a weekly research digest) → schedule. Most runs find work, and seeing everything at once gives better results than one item at a time.
- Work arrives in bursts and the agent should see related items together → events that feed a queue, with a scheduled drain. Collect for a short window, then run once over the batch.
The inbox agent from the opening sits in the third bucket. Email is bursty, and threads should be triaged together, not one reply at a time.
The setup that holds up: events for speed, a slow sweep as backup
In practice, the reliable answer is usually both. Events give you speed. A slow scheduled sweep catches whatever the events missed. On top of that, two guards make overlapping runs safe no matter what started them.
def run_triage(trigger_items=None):
# Guard 1: only one run owns the inbox at a time.
lease = acquire_lease("inbox-triage", ttl_minutes=20)
if not lease:
return "skipped: another run holds the lease"
try:
# Sweep runs pass nothing and fall back to the watermark.
since = load_watermark("inbox-triage")
items = trigger_items or fetch_unread(since=since)
# Guard 2: skip anything already handled, by message id.
items = [m for m in items if not already_processed(m.id)]
if not items:
return "no work" # cheap exit before any model call
agent.triage(items) # the expensive part
mark_processed([m.id for m in items])
save_watermark("inbox-triage", max(m.received_at for m in items))
finally:
release_lease(lease)
Three details make this work:
- The lease stops the 9:15 run from piling onto the 9:00 run. Give it a time limit, so a crashed run doesn't block everything after it forever.
- The processed-id check makes duplicate events and overlapping sweeps harmless. The same email arriving twice is skipped the second time.
- The "no work" exit comes before any model call. If the agent has to load a prompt just to find out the inbox is empty, the check is in the wrong place. Plain code should decide whether there is work, and only then wake the model.
With these in place, the sweep can be slow, say hourly, because it no longer carries the latency. Its only job is to catch the events that never arrived.
✅ A pre-deploy checklist for any trigger
- Is the "is there work?" check plain code, run before the model is called?
- Can two runs overlap, and if they do, what stops them from both acting on the same item?
- If one event is dropped, what eventually picks that item up?
- If the same event arrives twice, does anything happen twice?
- During a burst, does the agent see related items together or one at a time?
- Does each run log what started it (schedule, event or manual), so a strange result can be traced back to its cause?
If you can't answer the second and third questions, the trigger isn't ready, however good the prompt is.
The takeaway
A schedule is easy to set up and expensive while nothing is happening. An event trigger is cheap while nothing is happening and fragile when something goes wrong. Neither one keeps an agent from acting twice. Choose by what a wasted run costs and how long the work can wait, then add the lease, the processed-id check and a slow sweep regardless. The trigger only decides when the agent wakes up. Those guards decide whether it does the right thing once it's awake.