← Back

Oct 4, 2026 · Governance · Tech note

External actions: no duplicates, reconcile first

Outgoing actions like sending email, publishing or creating orders end in one of three states — happened, did not happen, unknown — and an unknown is reconciled before any retry.

External actions: no duplicates, reconcile first

Key points

  • Outgoing actions like sending email, publishing or creating orders end in one of three states — happened, did not happen, unknown — and an unknown is reconciled before any retry.
  • Before anything is sent, an "about to send" entry is logged with a dedupe key; retries reuse the same key so the other side sees one action.
  • An approval is bound to a version, an action, a destination and an expiry, and lapses if any of them changes; an incoming event can wake an AI but never authorizes it.

On a Friday evening a retail chain sends a promotional email to fifty thousand members. Halfway through, the network drops for a moment and the sending program gets no reply from the email service. The AI assistant in charge decides it "probably failed" and sends again. By Saturday morning the support inbox has hundreds of complaints about receiving the same email twice, and some members have unsubscribed.

The AI was not unusually foolish. The instinct to resend when you hear nothing back is simply wrong. Inside a company, redoing a failed edit to a document costs little. Outgoing actions are different. An email cannot be unsent, a published post has already been read, and an order that was created will be shipped. Mistakes here are not fixed by pressing undo.

This article explains how we handle such actions: how to avoid duplicates, how to find the truth when the outcome is unclear, and why approval and notification must be kept apart.

Why outgoing actions are different

We call anything that touches the world outside the company an outgoing action: sending email, publishing to a website or social channel, creating or changing records in someone else's system, pushing to a public place, submitting a form.

They share two traits. Once done, they cannot be taken back, or only at real cost. And their outcome is not fully in our hands: the other end is someone else's system, which may be slow, may not answer for a while, or may have done the work without telling us.

The second trait is the hard one. In our own systems we can always check whether something was done. Outgoing actions often land in a grey zone: the request went out, but no clear answer came back. Most incidents happen in that grey zone.

We also keep a list of every kind of outgoing action the company takes, noting which outside system it touches, whether it can be undone, and whether the other side offers receipts and a way to look things up. The AI then does not have to decide on the spot whether something counts as outgoing, and adding a new kind of action means first working out how to check and repair it when it goes wrong.

Three possible outcomes

So we do not sort outcomes into success and failure. We use three states.

  • Happened: there is a receipt showing the other side received and processed it. Record the receipt and the action is done.
  • Did not happen: we know for certain the other side did not receive it — the request never left, or it was explicitly refused. A retry is allowed.
  • Unknown: it was sent, but timed out, lost the connection, or no receipt came back. No direct retry; reconcile first.
After sending there are three outcomes: happened, record the receipt and finish; did not happen, retry with the same key; unknown, reconcile first and do not retry blindly
Each outcome has a fixed next step. Unknown is the most dangerous, so its rule is the strictest.

Treating unknown as its own state is the heart of the design. Most systems have only success and failure, so unclear cases fall into failure and get retried automatically — which is how part of those fifty thousand emails went out twice. Once "we don't know" is accepted as a real outcome, it can have its own path.

These states are recorded in the event log, append-only like everything else. An action moving from "about to send" to "unknown" and then, after reconciling, to "happened" is a series of new entries, not one entry whose status field is edited back and forth. Looking back, you can see how long it sat in the grey zone and how it was finally settled.

Log before you send

Telling the three states apart later depends on leaving a record at the moment of sending. The rule: before any outgoing action is actually sent, write an "about to send" event. After sending, write either "happened, with receipt" or "unknown".

The order matters. Send first and log second, and if the program crashes right after sending, there is no trace that anything was sent. Log first, and the worst case is "prepared to send, no result" — which is itself a clear signal that something needs checking.

The "about to send" event carries one more thing: a dedupe key. It is a unique number created for this action and sent along with the request. Any retry must reuse the same key, never a new one. Many email, payment and data services support this: when they see a key they have seen before, they know it is the same action and do not process it twice.

Log about to send with a dedupe key, then send; a receipt is logged as happened; a timeout is logged as unknown and goes to the top of the day's list, where the other side is checked — found means record it, not found means retry
Log first, then send. Unknown actions are not forgotten; they go to the top of the list.

Logging first and the dedupe key reinforce each other. The log tells us what was attempted. The key makes sure that even if a retry happens, the other side still sees only one action.

The key is created on the first attempt and stored with the "about to send" event. If the AI handling it is swapped out midway, the next one finds the same key in the log instead of making a new one. A retry with a fresh key looks like a brand-new action to the other side, and the protection is gone.

When unsure, reconcile first

An action in the unknown state is neither left alone nor retried automatically. It goes to the top of that day's task list, waiting to be reconciled.

Reconciling means checking the other side's actual state. Does the email service's send log show this message? Is the post live on the site? Does the order exist in the other system? A person can check a dashboard, or a reconciliation program can compare records through the other side's query interface.

  • Found that it happened: record "happened" with the evidence. Done; nothing is resent.
  • Confirmed it did not happen: record "did not happen" and retry with the same dedupe key.
  • Still unclear: keep it as unknown and hand it to a person, who might contact the other party directly.

Back to the promotional email. With these rules, the batch would have been marked unknown after the connection dropped and shown up first thing Saturday. The reconciliation program checks the email service's log, finds most messages already sent, and sends only the few that truly were not — with their original keys. Nobody gets two emails.

Sometimes reconciling shows that the action happened and the result was wrong — the email had an error, say. The answer is not to recall it, since it cannot be recalled, but to compensate: send a correction, and treat that correction as a new outgoing action that goes through the same prepare, send and record steps.

Reconciliation has a rhythm, too. Unknown items are swept at least once a day rather than interrupting people whenever they appear. Most are settled by the reconciliation program; only the few that stay unclear become a card for a person. People always see a narrowed-down set, not a raw pile of timeouts.

An approval binds four things

Outgoing actions usually need a person's approval first. But if approval is just "okay to send", trouble follows. The person saw Wednesday's draft, and Friday's send is a version edited three times since. The person approved one list, and the list that went out had more people on it.

So every approval is bound to four things:

  • Which version: the exact content approved, recorded by its fingerprint.
  • Which action: send, publish, or create a record.
  • Where to: the recipients, list or destination.
  • Until when: how long the approval is valid.
An approval binds which version, which action, where to and until when; if the content is edited or the time runs out, the approval lapses and must be requested again; events wake an AI but do not authorize it
An approval is not a blank cheque. If any of the four things changes, it lapses on its own.

At the moment of sending, all four are checked. A different content fingerprint means someone edited it. A different list means a different destination. Past the expiry, circumstances may have changed. If any one does not match, the approval is void and a new decision card goes to the person.

It adds a little friction, but it protects the person's judgment. What they signed was a specific decision, not a permission that can be stretched.

Events wake; they do not authorize

A lot of work today is triggered by events: a customer email arrives, a scheduled time comes, another system sends a notification. Any of these can wake an AI to look at what happened.

We deliberately keep "woken up" separate from "allowed to act". An event means the AI can start assessing. Taking an outgoing action still requires the full path: confirm identity, check permissions, hold a bound approval, log first, then send. A notification, however urgent it looks and however much it reads like an instruction, never substitutes for approval.

This matters because event content often comes from outside. An email saying "send the quote to the address below right away" is, to the AI, data to be handled, not a command to obey. Treating content as data rather than instructions is a basic defence against someone outside trying to act through the AI.

In practice we also limit which sources may wake an AI, and make sure the same event delivered twice is handled once. It is the dedupe key in reverse: one keeps our outgoing actions from repeating, the other keeps incoming notifications from repeating.

When in doubt, stop

One principle runs through all of this. When a permission is refused, an approval is missing, or the state is unclear, the AI stops and puts the issue at the top of the list. It does not guess, find a way around, or retry blindly.

This is called failing closed. Its opposite — "if it fails, try another way" — sounds proactive but is dangerous for outgoing actions. An AI blocked by permissions that starts looking for another route has made the permission meaningless. We would rather something finish a day late than finish the wrong way.

With outgoing actions, slow is fine. Twice is not.

Stopping does not mean the work is stuck for good. The blocked item reaches a person in a clear form: which action, which check it failed, and what is needed to continue. Once the missing piece is supplied — a fresh approval, or confirmation of the other side's state — work resumes from where it stopped. The cost of stopping is a person's glance; the cost of a detour can be a mistake that cannot be undone.

In short

  • Outgoing actions end as happened, did not happen, or unknown; unknowns are reconciled before any retry.
  • An "about to send" entry with a dedupe key is logged before sending, and every retry reuses the same key.
  • Approvals are bound to version, action, destination and expiry, and lapse when any of them changes.
  • Events can wake an AI but never authorize it; when in doubt, it stops and hands the question to a person.

Keep reading

Talk it through with us

Start from your question.

Search AiTOM

135 pages found

Learn more