Core concepts
Staged writes
A staged write is a proposal, not an instruction. It is durable, scoped, expiring, and executable exactly once.
Lifecycle
Proposed. The model calls a tool declared
is_write. Tether does not contact your endpoint, except to call the read a write's precondition names. A write your system already refuses is not staged, and the model is told why.Staged. A pending action row is written: the command type, the arguments, the tenant and scope it belongs to, the user it is attributed to, and an expiry. The turn returns a preview envelope.
Surfaced. The caller receives it in
pending_actionsand renders a confirmation card.Confirmed. A separate authenticated call arrives at
POST /v1/actions/{id}/confirm.Guarded. The tenant boundary is re-checked, and any declared risk must be acknowledged.
Rechecked. A write with a precondition has its read called again. If the answer changed while the write waited, it is settled as
precondition_failedand never executed. If it changes in the instant between that read and the write, your imported API is the one that refuses, and a refusal is settled with your reason, not held as indeterminate.Claimed. A conditional update flips
pending → executing. Losing this race is how a double confirm becomes one execution.Executed. Your endpoint is called with
"mode": "execute". Success settles the action; failure releases the claim so it can be retried.Verified. A write with a
verifyhas its read called once more, after the execution is recorded.verifiedorunverifiedis recorded with the reason, and an unverified outcome is handed to a person. Nothing is executed again.
The preview envelope
What the tool call returns to the model, and what surfaces to you:
{
"preview": true,
"action_id": "9c1e7b6a-3f42-4a51-9d0e-6b2c8f4a1d33",
"plan_id": "1a7f…",
"command_type": "void_ticket",
"summary": "void ticket — awaiting confirmation.",
"expires_at": "2026-08-20T18:41:07Z",
"confirmation_required": true
}
The model sees this too, which is deliberate — it knows the action is pending rather than done and phrases its answer accordingly.
Execute-once, under contention
Confirmation is not idempotent by convention; it is idempotent by construction.
The claim is a conditional update: flip this action from pending to executing, and tell me
whether you were the one who did it. Exactly one caller wins. The winner proceeds; the loser
gets Replayed or NotFound and never reaches your endpoint.
This matters more than it looks. A confirmation button is exactly the kind of control a user double-taps on a slow connection.
Failure and retry
If your endpoint fails, the execution is recorded and the claim is released — the action
returns to pending and can be confirmed again. It has not been consumed by a failed attempt.
The exception is a timeout. Tether reports that the write may or may not have landed, and does not retry, because it cannot tell the difference between "never arrived" and "applied, but the response was lost". Your idempotency key is what makes a retry safe on your side, where you can actually check.
The guard
Two things are enforced before anything runs.
The tenant boundary. The action was staged against one tenant; the credential confirming it must be that tenant. This is the check only Tether can make — without it, a valid key for one customer could confirm another customer's staged write.
The risk acknowledgement. A tool registered elevated or high risk requires
acknowledgeHighRisk: true on the confirm call. So does an action carrying a declared value
at or above the configured threshold — or one whose value could not be read at all.
What the guard cannot do
It does not know your permission model. It cannot know that shift leads may void under $50 and managers may void anything.
Your endpoint sees every write with "is_write": true and can refuse. That is where your
rules belong. If you want Tether to ask you before it dispatches, implement an authorizer —
but the endpoint remains the authority.
Expiry
Pending actions expire. A proposal made against a five-minute-old picture of the world should not be confirmable an hour later, when the ticket has been closed, the stock has moved, and the shift has changed.
Expired actions return NotFound on confirm, and are cleaned up by a retention job.
Related
- Your first write — the same flow, worked end to end
- Building a confirmation UI
- Actions API