Chapter 6 of 9 · 8 min read
An undo the user cannot reach in time is not an undo
On this page
The claims console sent about 41,000 letters a month, and every send raised a toast in the bottom-left corner reading Letter sent with an Undo link beside it. A toast is the small notification strip that appears at the edge of the screen after an action and removes itself without ever being acknowledged, which makes it the cheapest place in an interface to put something and the least likely place for anyone to find it later. This one dismissed itself after five seconds. Across the quarter, the undo link was pressed 61 times. In the same quarter the support desk logged 347 tickets whose text amounted to I sent the wrong letter, can you stop it.
Reconstructing the timing from those tickets — comparing the send timestamp on the letter to the timestamp of the first support message — put the median at 47 seconds, with a long tail out past ten minutes. What happened in those 47 seconds was usually that the handler moved to the next claim, glanced back at the previous one, and noticed the settlement figure. The undo affordance was gone by second five, and the ticket was the only remaining door. Engineering had built reversibility — the letter was queued through a dispatcher that could cancel — and had exposed it for five seconds in a corner of a screen the handler was no longer looking at.
The window comes from the distribution, not from a round number
Undo-send as a product idea comes from Gmail Labs in 2009, which held outbound mail briefly before handing it to the transport, and shipped with a window measured in seconds because the design constraint was that the sender would notice immediately. That constraint holds when the sender is composing and reading their own words at the moment they press send. It does not hold when the text was generated, because the mistake the handler is going to notice is one they never wrote, and noticing it requires re-reading something they believed they had already checked.
Choosing a window means looking at the distribution of time-to-realisation for the specific product. On this traffic the recovered figures were: 22% of realisations inside 30 seconds, 38% inside 60, 68% inside 120, and 84% inside 300. Setting the hold at 120 seconds captured two thirds of the cases and cost a two-minute delay in delivery that no policyholder could perceive; letters are read hours later. Setting it at 300 seconds would have captured more and started to interfere with the handful of flows where a handler sends a letter and immediately telephones the policyholder about it, so the team took 120 and made the pending state visible rather than pushing the number higher.
The pending state is the part that gets skipped. A letter held for two minutes is in a state the handler has never seen before, and if the interface still says Sent, then the handler who telephones and says "you should have my letter" is wrong, while an interface that says Sending… for two minutes reads as a system that is broken. What worked was naming it: Sending in 1:47 — Cancel, shown on the claim itself rather than only in a toast, with a plain statement that the letter has not left yet.
The too_late branch is the one that must exist in code and in the interface. On this system 4% of cancel attempts raced dispatch, and an interface that optimistically reports undone on a cancel it did not win has told a handler that a letter is not in a policyholder's inbox when it is. That is a worse outcome than no undo, because the handler stops acting. Undo has to report what actually happened, including failure, and where it fails it should immediately offer the compensating action — the forward-going step that cancels out the effect of something that cannot be recalled, such as a correction letter after a wrong one has been delivered — rather than leaving the person to work out what to do next.
Where undo lives after the toast
The toast is a shortcut, and treating it as the mechanism is the error that produced 347 tickets. The mechanism is a record of the action attached to the object it affected, and the toast is one of several ways to reach it.
On the claim page that meant an activity strip listing what the assistant and the handler had done, each entry carrying its own reversal control if one was available and a plain statement if it was not. A handler who returns to claim CLM-88421 four minutes later sees Letter sent 14:22 — cannot be recalled; send a correction in the place where, ninety seconds earlier, they would have seen Sending in 0:31 — Cancel. The affordance degrades rather than disappearing, and the path to it is the object, which is where the person already is when they realise.
The hold window also has to survive the handler closing the tab, which is the case that separates a real outbox from an animation. An implementation that waits two minutes in the browser and then issues the request has built a delay rather than a queue: the letter never goes out if the handler logs off, and it goes out twice if they have the console open in two tabs. Enqueuing server-side with the action's own identifier as the idempotency key — the value the dispatcher uses to recognise a repeated request as the same action it has already accepted, so that sending it twice sends one letter — makes both cases behave, and it makes the pending state available to anyone who opens the claim, including a supervisor who receives a telephone call about it from a policyholder's broker before the letter has been delivered. That last scenario is worth designing for explicitly, because the person who needs to cancel is not always the person who sent.
A global activity feed is a poor substitute, because at 41,000 letters a month a handler's own feed is dense and the entry they want is identified by the claim it belongs to. Keyboard undo has the same problem in a different form: the browser's undo stack is scoped to a text field, and a handler pressing the shortcut after sending gets a paragraph restored in a composer that is no longer relevant, which teaches them the shortcut does not work here.
Not everything is reversible, and the differences matter
Undo as a word covers three quite different mechanisms, and conflating them produces interfaces that promise more than the system can perform.
| Action | Reversible? | Mechanism | What the other side saw |
|---|---|---|---|
| Assistant rewrote a draft paragraph | yes | restore the prior revision | nothing |
| Letter queued, within the hold | yes | cancel dispatch | nothing |
| Letter delivered | no | send a correction letter | the original, and now a correction |
| Payment request raised, pending approval | yes | withdraw the request | an entry in the approver's queue |
| Payment approved and released | no | reversal entry in the ledger | the credit, then the reversal |
| Claim closed | partially | reopen | the closure is permanent in the audit trail |
| Webhook fired to a partner's system | no | nothing the console controls | whatever their system did with it |
The rows that read no are the rows that need work at the other end of the interaction. If an action cannot be recalled, the statement of that fact belongs before the commit, in the confirmation, in terms of the actual consequence rather than a generic warning. This will be emailed to Mr Adeyemi immediately and cannot be recalled is a different sentence from Are you sure? and produces different behaviour: on the claims console, replacing the generic confirmation with the specific one on the irreversible subset dropped unsend tickets by 31% over two months, and the drop came from handlers going back to re-read before confirming rather than from any change to what happened afterwards.
Typed confirmations — retype the claim number to proceed — are usually theatre, because people become fluent at typing the string. What worked better on the payment path was restating the two facts that would matter if the action were wrong, the amount and the recipient, in the confirmation itself, pulled from the record rather than from the generated text. A handler about to release £4,820 to the wrong claimant sees the name in the button's own sentence.
The action record
Making any of this work means actions are records with lifecycles, not function calls.
type ActionRecord = {
id: string; // idempotency key for the dispatcher
kind: "send_letter" | "raise_payment" | "close_claim" | "notify_partner";
claim_id: string;
created_by: { user_id: string; assisted_by_answer?: string };
payload_revision: string; // the exact text or values committed
dispatch_at: string; // now + hold window
state: "queued" | "dispatched" | "cancelled" | "failed";
reversal: {
kind: "cancel" | "compensate" | "none";
available_until?: string; // absent for compensate and none
compensation?: "correction_letter" | "ledger_reversal" | "reopen";
};
};Two fields do most of the work. assisted_by_answer links the action back to the answer envelope from chapter 1, which is what let the team find all 1,140 letters affected by a single rewrite behaviour rather than discovering them one complaint at a time. And reversal.kind is the field the interface renders from, so the affordance shown to the user is derived from what the system can actually do rather than from what a component author assumed.
What undo does not buy
It does not license a lower bar on the model. Teams reach for the argument that an action is safe to take automatically because it can be undone, and that holds only where the counterparty sees nothing during the window. A letter cancelled at second 90 costs nothing; a letter delivered and then corrected costs a confused policyholder and a complaint, and no reversal mechanism unsends the impression. Where the damage is informational — a wrong figure quoted on a telephone call, a partner's system that has already acted — reversibility in the console is a bookkeeping operation and the user-visible harm is permanent.
Undo is also not a replacement for the diff of chapter 5. Reviewing before commit costs about eleven seconds and prevents the event; reversing after commit costs a support interaction, a correction, and whatever the counterparty did in between. The two are ordered, not alternative, and a product that leans on undo because the review step hurt conversion has moved a cost from a place where it is measured to a place where it is not.
One useful side effect is that the record above is a quality signal. A cancelled send and a compensated action are unambiguous statements that something the assistant produced was wrong enough to stop, they are attached to the answer that produced them, and they cost the user nothing to emit. That property — a signal generated as a by-product of ordinary work — is what the feedback chapter is built on.
Both of the protections so far spend time: two minutes of hold, eleven seconds of review. Time is also what users spend before the answer arrives at all, and it is the dimension where teams most often optimise the wrong number.
AI product UX — read the current version at https://daviddominguez.dev/learn/en/ai-product-ux/an-undo-the-user-cannot-reach-in-time-is-not-an-undo