Skip to content

Sent is not published: eight handovers nothing had read back

“Sent” is a fact about your request. “Published” is a fact about someone else’s system. Only a read-back turns one into the other. The first time we checked, eight handovers had none: seven were live, and the eighth had failed authentication on a dead connector that told us nothing.

ai-agents · reliability · idempotency · integrations · process

The short answer

A success code from an external service tells you one thing: the service accepted your request. It does not tell you the thing happened.

“Sent” is a fact about your side. “Published”, “paid” and “delivered” are facts about the other side. The only way to turn the first into the second is to read the result back from where it was supposed to land.

We found out how big that gap was in our own pipeline, and it took one minute.

What we found

Our internal publishing pipeline hands finished releases to an external scheduling platform. For a while, the handover was where our record stopped. The platform took the request, and our system wrote that down.

Then we added a watchdog for exactly this gap: anything handed over that nothing had confirmed. In its first minute, it found eight releases that had been handed over hours earlier. Not one had a verification job.

We read all eight back from the platform.

  • Seven were live, with real permalinks and the platform’s own timestamps.
  • One had been rejected on the platform’s side with “authentication failure”. That channel’s connector was dead, and nothing in our system had said so.

In a single sweep, rows in the published state went from 6 to 13. The seven had been fine all along. We just hadn’t known it. The eighth was the one that mattered, and it made no noise.

A category, not an accident

Other teams have written this up too. Several 2026 write-ups describe the same failure: an agent reports a task as done because a tool returned success, and the destination never received anything.

  • AWS Builder Center puts it structurally: “a tool can return success while the write didn’t actually persist… Reading the backend after the fact is the only reliable check.” And, more bluntly: “you can’t prompt your way around a tool that lies.”
  • A June 2026 taxonomy of agent failures from Telerik files it under silent failure, where “every span looks successful” and the error rate never moves.
  • One widely shared post makes the point with a launch calendar: a 202 Accepted confirms the server received the request, not that the posts were created. Its author labels the story fictional, so we borrow the wording and not the evidence.

Our view: the silent version is worse than a crash. A crash wakes someone up. A dead connector that reports nothing looks exactly like a quiet day.

Start with one question

Before building anything, count. Run something like this against your own database. The table and column names are placeholders. The shape is what matters.

-- everything handed to an external service that nothing has read back
select id, destination, handed_over_at,
       now() - handed_over_at as age
from   outbound_actions
where  status = 'sent'
  and  confirmed_at is null
order  by handed_over_at asc;

The row count is the size of the hole. The age of the oldest row is how long you’ve been taking the other system’s word for it. We expect (and this is our opinion, not a measurement) that for most teams the count is not zero.

What we changed

Every handover now schedules its own verification. Here is the policy, in plain terms:

verification policy
  trigger    every handover schedules one check
  outcomes   5 distinct outcomes, not pass/fail
  retry      every 30 minutes
  give up    at 6 hours -> explicit failure state
  sweep      picks up anything handed over without a check
  watchdog   raises anything still unconfirmed past 6 hours

Our view: the give-up line matters most. Without a deadline, “pending” quietly turns into a permanent state, and a permanent pending is just hope with a status column.

The external write-ups add a definition of done that is worth putting in a spec as-is. It has five steps, and the last one produces a receipt:

done means all five:
  1. submit the request
  2. track the asynchronous job
  3. wait for a terminal state
  4. check the real destination
  5. read the result back and keep a receipt:
       platform · account · post id · scheduled time · final status

If a step has no field to record it in, the step isn’t really happening.

The third outcome

Reading back is half the problem. The other half is what you do when you can’t tell what happened.

An outward action has three outcomes, not two: it worked, it failed, or we don’t know. We found that out the hard way as well. Our failure screen said “connection reset” above one button, and that button would have posted the same release a second time. The duplicate guard relied on an external id read out of the platform’s reply, and we had captured that reply’s shape for only one of our three channels. On the other two, a release stuck mid-flight would have called the platform again.

These are the rules that replaced it:

timeout              -> unknown, not "not sent"
empty external id    -> never re-sent automatically
failed row           -> two exits, side by side:
                          paste the link if it went out
                          retry if it did not

The second rule is the one people push back on. From the inside, “the call never happened” and “the call worked, but the reply had an unfamiliar shape” look identical. An automatic retry turns that ambiguity into a duplicate. A person who opens the account settles it in seconds.

An outside practitioner’s write-up this quarter reached the same place independently. It treats agent tool retries as a distributed-systems boundary, not a prompt problem. When a provider offers neither an idempotency key nor a reliable lookup, it says, there may be no automatic answer, and the safe branch is a human, a compensating action, or accepting an ambiguous outcome. It is a public post, not a study, so we cite it as framing, not as measurement.

FAQ

If the API is reliable, isn’t a 2xx response enough? It confirms that the service accepted the request. In our case, seven of eight handovers really were live. The eighth was an authentication failure on the platform’s side, and the handover response gave no sign of it. You only find the eighth by reading back.

Why retry every 30 minutes and stop at six hours? Those are the numbers we chose for our platform. In our view the exact values matter less than having them at all: a fixed retry interval stops you hammering the platform, and a hard deadline stops “pending” from becoming permanent.

Why not retry automatically when we’re unsure? Because with an empty external id, you can’t tell “never sent” from “sent, with an odd reply”. An automatic retry settles that uncertainty by publishing twice. A human checking the account settles it with nothing sent twice.

What should I check first in my own system? Run the query above and write down two numbers: how many rows, and the age of the oldest one. Then check whether you have captured the response shape for every destination you send to, or only the one you tested against. For us, it was one of three.

Related case

The same discipline runs through RE Intelligence, where the claim being checked is a price forecast rather than a handover. Forecasts are archived, graded against what actually happened, and the accuracy is shown to the user instead of hidden. The case is on iloblique.com.

If you have something worth building, we’d like to hear about it.