Skip to main content
Stop the run at one question, send it to whoever answers, and carry on from that exact step when the answer comes back in the shape you asked for. This is the mechanism under a person’s decision, human-in-the-loop as a step, and it serves an agent or a service the same way. For the idea of a check that fails a step and runs it again, read Feedback loops; this page is the gate itself. The smallest gate posts one question, claims it, answers it and replays the history:
examples/callback-gate.ts (excerpt)
createCallbackGate turns the definition into a request with an identity, and post puts it on the client. A gate needs:
  • gateId and gateVersion, which name the question across runs.
  • decisionText, the question a person or a router reads.
  • responseSchema, the shape every answer must have. The run branches on the typed answer and never asks a model what it meant.

Route the question

A router takes the question to whoever answers it: a surface in a browser for a person, an engine call for an agent, an API call for a service. Two routers can’t hold one question at once:
examples/callback-gate.ts (excerpt)
The first router claims the request and the second is refused. When the first releases it with its claim token, the second claims and answers it. directRouter is the simplest router: it claims, asks the function you give it, and submits. On submit the run checks that the required fields are there and have the right types, then records the answer. Check any deeper rule in your host before it submits. If a router dies between claim and answer, the question is still there and still claimed until the claim is released.

Wait attended or unattended

An approval() step waits in one of two ways, and both record the question the same way. Attended: a person is at the run, so the process stays up and waits. Set onCallback: 'wait' for this; it’s never the default, and a run exits unless you ask it to wait, whichever client it has. Unattended: nobody is attached, so the run records the question and exits. A schedule starts the same file again with resume: true, the run reads its record first, and if the answer has arrived the step carries on. If it hasn’t, the run exits again with the same recorded pause and asks nothing twice. The answer arrives through the stored callbacks client, made with createStoredCallbackClient and passed to run() as callbacks. Without it, run() gives each run a fresh in-memory client, and its questions end with the run. Nothing in the record says which mode asked the question. A team review that uses a callback gate waits only unattended. It posts its question and pauses straight away, even with onCallback: 'wait'. Start the run again with resume: true after the answer arrives.

Replay the history

The client’s history is the record of the question. Replaying it rebuilds the pauses, so a fresh process knows what is still waiting:
examples/callback-gate.ts (excerpt)
The request digest covers the gate id and version, the question text, the response schema and the input. Change any of them and it’s a new question, so an answer to the old one doesn’t carry over. Presentation hints, such as a theme or a pane placement, are outside it: changing only how a question is shown keeps it the same question. Run the file with npx tsx callback-gate.ts; it runs offline in under a second:
Output
The same question with a new theme kept its id; the same question about a new revision got another. The second router was blocked while the first held the claim, then answered after the release. The replay left nothing pending.

Add a person’s decision

approval wraps the gate for the common case of one question in a workflow. It asks through the run’s callbacks client. A yes passes the step. A no goes to the step you name as its target, with the person’s note as the finding. No answer pauses the run with the request pending. A person decides shows it in a running file, and a host answers through the same client, routers and stored history. Put a gate where a person must say yes: before a release, before a destructive change, before spending money. Give it a surface and the run pauses with the question open in a browser. For a decision about a proposed change, bind the approval to the evidence with proof acceptance: an approval gate can bind its answer to proof, input artifacts, output bytes, permissions and a workspace anchor, and the host checks those still match before it acts.

Failure

  • An answer of the wrong shape is refused at submit, and the question stays open.
  • A router that dies leaves the question claimed. Another router can take it once the claim is released with its token.
  • An answer doesn’t restart an exited run. The host reads the decision first, so it can decide an approval needs a second look, then resumes.
examples/callback-gate.ts

Next steps