Skip to content

What happens after you press Stop?

A Stop button that ends in one word hides what actually happened. A small synthetic demo of a return summary: one confirmed write, one unknown, one never sent, the actions each one allows, and why a timeout must never turn unknown into failed.

Tim Urista 10 min Stop/return demo
Title card reading: What happens after you press Stop? Subtitle: Stopped is true about your orchestrator; it says nothing about the writes that were already in flight. Stats: 1 confirmed, 1 unknown, 1 never sent, 5 of 5 state tests pass. On the right, the single word Stopped is struck through, and below it three outcome cards from trace.json: SKU-4471 confirmed, storefront returned a change receipt, in a solid teal border; SKU-9102 unknown, no receipt and no cancel acknowledgment, don't retry yet, in a dashed rose border; SKU-2208 never sent, local draft, discard or resume, in a dotted grey border.
On this page
  1. Where the question came from
  2. ”Stopped” answers one question out of three
  3. A small synthetic demo: three steps, three outcomes
  4. What each outcome lets the user do
  5. Unknown has to stay unknown
  6. Reverse is not undo, and compensation is a new action
  7. An approval covers one exact request
  8. What I didn’t check
  9. Seven checks before you ship a Stop button

At a glance

  • A Stop button that ends in the single word "Stopped" is telling the truth about one thing, your own orchestrator, and nothing about writes that were already in flight.
  • In a three-step synthetic demo, Stop left one write confirmed with a receipt, one unknown with no receipt and no cancel acknowledgment, and one never sent. Each needs different words and different buttons.
  • The state model has no "failed" state. timeout_tick() is a deliberate no-op, and a test calls it five times on an unknown write and checks that it's still unknown.
  • Reverse is allowed only against a confirmed effect marked reversible. Anything else is rejected with a reason, and undoing an irreversible effect is filed as compensation: a new action with its own approval.
  • An approval covers one exact request. Changing the version, target or amount invalidates it, and the five state tests check each of those separately plus a no-change control.

Say you asked an agent to update product descriptions for a batch of SKUs across three tools: a storefront editor, a catalog sync service, and a local draft queue it hadn’t reached yet. Partway through, you press Stop.

This is a hypothetical. There’s no real client, storefront or catalog service here. What I care about is what the screen says to you a few seconds later.

Most Stop buttons resolve to one word: Stopped. That word is accurate about exactly one thing, the local orchestrator. It isn’t scheduling new steps. It says nothing about the rest. One of your three writes already landed and the other side sent back a receipt. One is in a state nobody can speak to: not confirmed, not canceled, just unanswered. The third never left your machine. “Stopped” puts all three under the same label, and the person who comes back to that screen has to guess which is which before they decide what to do.

Where the question came from

This started in Medium threads, not in a product. Under Sanjay Dey’s article on AI agent interface design, which covers delegation, autonomy levels, activity feeds, approvals, stop and undo, and a return summary, I replied:

The stop control deserves its own return experience. If an agent is updating those product descriptions across several tools, clicking Stop may prevent the next write while an earlier request is still completing. I would show stopping, stopped, and effects still being reconciled as separate states, with the final log identifying which changes actually landed.

The product-description scenario above is from that reply. Sanjay answered in the same thread that he agreed with explicit feedback about stopped work, ongoing work and reversibility, and that realistic scenario testing matters. That’s agreement with a design idea. It isn’t evidence that anyone tested it, and nothing below is his.

Two other threads fed in. Under a Towards AI piece on Redis for AI agents, I’d argued that an unknown outcome should survive recovery as a pending action. Ray Hu pushed back: keeping the operation ID doesn’t tell you what happened downstream, and without idempotency or a status lookup you need a real reconciliation path. He was right, and that exchange shaped the section on unknown below. And Riya Jawandhiya’s article on billing approval microcopy led to a thread about which click actually commits the user, which shaped the section on approvals.

None of these authors reviewed this piece or endorsed its conclusions.

”Stopped” answers one question out of three

When you stop a multi-step task, there are three separate questions, with separate answers:

  • Did local execution stop? This is about your orchestrator. It isn’t scheduling new steps anymore.
  • Was the external request canceled? This is about the other system acknowledging that it won’t carry out, or stopped carrying out, what you asked.
  • Was the external effect confirmed? Also about the other system, but a different claim: the thing already happened.

A local “stopped” settles the first one only. The storefront editor might have applied your change before the stop reached it. The catalog service might never have heard about the cancel, might still be writing, or might have failed without saying so. And the draft queue might hold work that’s still yours to keep or drop.

The return screen’s job is to answer all three, per step, and to be honest when it can’t answer the third yet.

A small synthetic demo: three steps, three outcomes

To make this concrete, I had a small demo built and then reviewed its behavior against the outcomes I wanted. It’s a Python state model, three authored fixtures, a runner and a static page. No network calls, no real tools, no real customers. Expected outcomes were written by hand into expected.json from the plan before the runner was first run, and the runner compares against them.

The three fixtures are one per outcome:

  1. SKU-4471, storefront tool: confirmed. The storefront acknowledged the write as applied before Stop finished locally, with a change receipt. Marked reversible.
  2. SKU-9102, catalog sync tool: unknown. The write was in flight. The tool’s status endpoint didn’t answer before the connection closed. No receipt, no cancel acknowledgment.
  3. SKU-2208, local draft: never sent. Stop arrived before the write left the draft stage.

Fixtures matched

3 of 3

results.json against expected.json, one recorded run

State tests pass

5 of 5

python3 -m unittest test_state_machine.py

Failed states

0

EXTERNAL_STATES has none, and a test asserts it

Mobile width checked

390px

scrollWidth equals 390, no horizontal scroll

The page reads the trace the run produced and renders one card per step instead of one word. Each card says what happened in plain words, what evidence it rests on, what the next evidence step is, and which actions are open.

The demo's return summary page, light theme, flagged as a synthetic demo. A dashed box notes that a generic screen would have said only Stopped. Below it, two cards. First: Update the product description for SKU-4471 on the storefront tool, badge Confirmed, text This change landed; storefront tool returned effect-applied with a change receipt; buttons Inspect effect and Reverse. Second: SKU-9102 on the catalog sync tool, badge Unknown, text We don't know if this landed, with a next step to poll the status endpoint for a receipt on this operation ID or wait for its webhook, and not to retry the write until one of those resolves it; one button, Wait / reconcile.
The demo's return summary at 800px, first two of three cards. Rendered from trace.json; the data embedded in the page matches that file. Buttons are illustrative and disabled.

The live demo page has all three cards and links to every source file: the state model, fixtures, tests, trace and results.

What each outcome lets the user do

Once local and external state are shown separately, “try again” stops being one button. The actions depend on the outcome:

  • Confirmed: inspect. If the other side says it happened, you can look at what it says happened. That’s a read, not a decision.
  • Confirmed: reverse, if reversible. A true undo only makes sense once you know, with evidence, what happened and that it can be cleanly reversed.
  • Confirmed but irreversible: compensate. If the effect can’t be reversed, “undo” would be a lie. What the user wants is a new action with its own consequences and its own approval.
  • Unknown: wait and reconcile. Unknown isn’t a synonym for failed. Nobody has told you anything yet. The honest move is to get evidence, not to guess.
  • Canceled: inspect what ran, then resume if still wanted. The cancel acknowledgment tells you the other side stopped. It doesn’t tell you nothing happened before it did.
  • Never sent: discard or resume. Nothing external exists to undo. Resuming is still a choice, not a default.
Diagram. Top row: running, stopping, stopped, with a note that Stopped is true about this row and only this row. Below a dashed line, four columns, one per outcome for each step already in flight. Confirmed, solid teal border: inspect effect, reverse only if marked reversible, compensate as a new action with its own approval. Unknown, dashed rose border: wait and reconcile by polling status or awaiting the webhook, retry stays off with a reason on screen, a timeout changes nothing. Canceled, solid grey border: inspect what ran before the cancel landed, resume only if still wanted. Never sent, dotted border: discard draft, resume if the request is unchanged. All columns lead to a renewed-approval gate: reverse, compensate and resume pass through it, and any change in version, target, amount or evidence means ask again.
One local state, four external outcomes, each with a narrower set of safe actions. Canceled is in the model's states but not in the three demo fixtures.

Sanjay’s article separates true undo from compensation. Here’s what the return screen adds to that: the user can choose recovery against the effects that were observed, not against the original plan. The plan said “update three SKUs.” What was observed was one confirmed, one unknown and one draft. Recovery has to start from the second list.

Unknown has to stay unknown

Unknown is the hardest outcome to get right, and the one products most often cheat on. A disabled retry button with no explanation looks like the product is hiding something. A retry button that quietly re-enables after thirty seconds is worse. It treats elapsed time as an answer.

Ray’s pushback is the core of it. My reply to him said:

UNKNOWN is a guard against acting on a false conclusion, not a reconciliation mechanism. The contract needs to say what external evidence can establish success or failure, who can resolve conflicting evidence, and when retry becomes permissible.

In the demo’s state model, that shows up as a function that does nothing on purpose. timeout_tick() represents time passing with no new evidence. It logs that a tick happened and leaves the external state alone. There’s no “failed” state for unknown to fall into. The only ways out are a receipt (confirmed) or an acknowledgment (canceled) from the other side. One test calls timeout_tick() five times on an unknown write, checks it’s still unknown, and then checks that “failed” isn’t in the list of states at all.

The unknown card at 390px wide. Title: Update the product description for SKU-9102 on the catalog sync tool. Badge: Unknown. Text: Stop was pressed while this write was in flight; the status endpoint did not respond before the connection was torn down, so there is no receipt and no cancellation acknowledgment. Summary: We don't know if this landed. Next step: poll the catalog sync tool's status endpoint for a receipt on this operation ID, or wait for its webhook callback; do not retry the write until one of those resolves this. One button: Wait / reconcile.
The unknown card at 390px. It names the missing evidence and the next step, and it offers no retry. Cropped from the demo page; nothing hidden.

The card’s wording matters as much as the state. “We don’t know if this landed” is a complete, honest sentence. “Failed” would be a guess, and “Retry” would bet the user’s catalog on that guess. If the write did land, retrying it is how you get a duplicate. If the other system has no status lookup at all, the screen should say that plainly and keep the step unresolved. It shouldn’t invent a resolution.

Reverse is not undo, and compensation is a new action

reverse() in the model accepts exactly one case: a confirmed effect marked reversible. Everything else is rejected with a reason, not silently ignored:

  • Against pending or unknown, the message says there’s nothing settled to reverse yet and repeats the next evidence step.
  • Against confirmed but not reversible, it says this can’t be a true reversal, and that removing the effect would be compensation: a new action with its own approval and consequences.

The test checks all three rejections, that the irreversible case’s message actually says “compensation”, and that the one allowed case goes through. It sounds pedantic, but the distinction changes what the user is agreeing to. Reversing a description edit puts the old text back. Compensating for a sent email means sending another email, and the recipient sees both. A screen that labels both “Undo” is making a promise it can’t keep for the second one.

An approval covers one exact request

The last thread is Riya’s. Her article is about billing confirmations that feel like traps, and the thread under it turned on a narrower point. In my first reply, I said the handoff needs copy that separates continuing to review from approving the charge, and that “if those details change after the redirect, the difference should be visible.” She replied suggesting the confirmation show the previous and new plan price. I agreed and added that the comparison should stay separate from the amount due now.

I’m not writing about billing here. What carries over to Stop is the shape: an approval is valid for the exact thing it was given for. In the demo, an approval token records version, target and amount. change_request() invalidates it if any of the three changes, and never re-approves on its own. The test checks version, target and amount separately, plus a control case with no change that must stay valid.

That matters after Stop because the world moved while the agent was paused. If the user approved “description v3 for SKU-4471” and comes back to find v4 pending, resuming shouldn’t ride on the v3 approval.

Diagram with two panels split by a dashed line. Left, what the returning user sees: local state, stopped; outcome per step, confirmed, unknown, canceled or never sent; next evidence step in words, for example waiting on the catalog tool's status check; actions allowed for that outcome, where disabled ones say why. Right, what only the system holds: raw status response or webhook; receipt or its absence, the only thing that separates confirmed from unknown; current action record with live version, target and amount; reconciliation attempts. A teal arrow labeled LABEL carries the receipt across the line to the outcome. Below, a renewed-approval gate compares what the user approved with the current action record before any reverse, compensate or resume.
The receipt crosses the line as an outcome label, never as a guess. The approval gate compares what the user approved with what the system holds now.

What I didn’t check

  • No usability test. Nobody used this screen. Whether people read four outcome states correctly under pressure is an open question, and a test that interrupts an agent mid-task and asks what’s still in flight would be the way to answer it.
  • The demo doesn’t render everything the diagram proposes. The state map says a disabled retry should show its reason on screen. The demo’s unknown card simply offers no retry button. The canceled outcome exists in the state model but none of the three fixtures uses it.
  • Reversal is recorded as “canceled”. In state_machine.py, a successful reverse() sets the external state to canceled. Those are different facts (the effect happened and was undone, versus it was stopped before it happened), and a real model should give reversal its own state.
  • Stop is modeled per step. The model calls stop() on each task. A real orchestrator stops once and then has to find out what each in-flight step did, which is where most of the difficulty lives.
  • No real tools, no concurrency. Single process, no network, no concurrent writers, no real status endpoints or webhooks.
  • The duplicate check was bounded. Before drafting, the recent Medium stories and drafts were checked for overlap with my earlier retry and changed-goal pieces. It was a bounded pass, not an exhaustive search of every story.

One problem made it all the way to the draft and was caught in review. The first draft quoted my own Medium replies as if Sanjay, Ray and Riya had written them. The source file stored each thread as one record with one text field, and that field held my reply, not theirs. Every quote now links to the page where those words actually appear. Where I describe what someone else said, I paraphrase and link. The lesson isn’t new: a source record that doesn’t say whose words it holds will get misread.

Seven checks before you ship a Stop button

  1. Show local state and external outcome as separate things. “Stopped” describes the orchestrator only.
  2. Give every step that was in flight its own outcome: confirmed, unknown, canceled or never sent.
  3. Make evidence the only way out of unknown, or an explicit human decision that’s recorded as one. Never a timer.
  4. When retry is disabled, say why and name the next evidence step.
  5. Allow reverse only against a confirmed, reversible effect. Treat everything else as compensation, with its own approval.
  6. Re-check approvals at the moment of action. A change in version, target, amount or evidence means ask again.
  7. If the other system can’t be asked what happened, say so on screen and leave the step unresolved.

If you’re designing how your own agent product behaves when someone presses Stop, here’s how I work.


AI-assisted, human-reviewed.

How this was made: the fixtures and expected outcomes were written before the runner was first run. Everything runs on the Python standard library, and the page is rendered from the run’s trace. Every source file is linked from the demo page.

ai-agentsproduct-designreliabilityux

Questions or feedback?
I reply to every note.

Say hi