Why my scheduled agent kept sending the same digest twice
I set up a scheduled task to send me a digest every morning. Nothing clever: read a few things, summarise them, send the summary. It worked, which was satisfying for about a day.
Then I got two digests.
Not identical, which was the annoying part. Two runs, two independent summaries of overlapping material, both convinced they were the only one. If they had been byte-for-byte identical I would have assumed a duplicate delivery and gone looking at the mail provider. Instead I had two plausible emails and no obvious culprit.
The bit I had not thought about
A scheduled agent starts from nothing. Every run is a fresh context with no recollection of any previous run. It has your instructions, whatever tools you gave it, and no history at all.
This is obvious when you write it down. It is much less obvious when you are writing the prompt, because the prompt reads like a description of an ongoing job — "every morning, send me a digest" — and jobs feel like they have continuity. They do not. The instruction is evaluated fresh each time by something with no memory of having evaluated it before.
So when the schedule fired twice, for whatever mundane infrastructure reason schedules occasionally do, both runs did exactly what they were told. Neither one could have known about the other. There was nothing to know it with.
Things I tried that do not work
The first instinct is to make the agent check its own output. Look in the sent folder, see whether today's digest is already there, stop if it is.
This works right up until it does not. You are asking a model to make a correctness decision based on a fuzzy read of unstructured data, and the failure mode is silent. A subject line changes slightly. The search returns yesterday's message because the date parsing was off by a timezone. The mail tool paginates and today's message is on page two. Each of these produces a confident, wrong answer, and the confident wrong answer sends you a second digest.
The second instinct is to write a file. This is the right shape — one durable bit of state — but a scheduled task running in someone else's infrastructure has no filesystem you can rely on between runs. Whatever you write is gone by the next invocation.
The third instinct is to stand up somewhere to put it. A tiny Redis. A single-table Postgres. Something with a REST endpoint the agent can hit. And this does work, which is the frustrating part, because now you are running and paying for and eventually patching a database in order to store one string.
I did this. The string was "ok".
What the problem actually is
The digest task needs one fact: did I already run today?
That is one boolean, or better, one timestamp. It does not need a schema. It does not need queries. It has no relations to anything. It is a sticky note that has to survive between two conversations that share nothing else.
Once you name it that way, the shape of the answer is obvious. The agent needs somewhere it can write a value and read it back, reachable from wherever it happens to be running, and it needs that to take about four seconds to set up rather than an afternoon.
The fix
Junkt came out of that. It is a key/value store the agent reaches over MCP, and the digest task now starts like this:
get_value("cowork:daily-digest:last_run")
If the key does not exist, or its updated_at is from yesterday, the task runs and then writes:
set_value("cowork:daily-digest:last_run", "ok")
If the timestamp is from today, it stops. No second email.
There is a small design decision in there worth pointing at. The agent does not store a timestamp string. It stores "ok" and reads back the updated_at that the store already keeps. Asking a model to format a timestamp, store it, parse it back and compare it to now is four opportunities to get something subtly wrong, and models are notably shaky about what today's date is. Letting the storage layer own the clock removes all four.
That last point turned into its own feature later — there is a time tool now, for exactly the cases where the model does need to know the actual time rather than guess at it. Its same_day kind does this whole check in one call.
The general version
The digest was one instance of something that keeps coming up as soon as you run agents on a schedule rather than in a chat:
- Did this already run today?
- Have I already replied to this thread?
- How many times has this happened this week?
- Am I in a cooldown after the last failure?
- Where did the last run get to before it stopped?
Every one of those is a small amount of durable state, and none of them is worth a database. They are all one key.
If you have hit the same wall, Junkt is free to start — 25 keys, no card, and about ten minutes from signing up to your agent's first successful call. Or read how it works first.
I still get one digest a day.
Give your agent a memory
Junkt is a hosted key/value store your agents reach over MCP. Free tier, 25 keys, no card, and about ten minutes from signup to a working tool call.