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:
gateIdandgateVersion, 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)
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
Anapproval() 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)
npx tsx callback-gate.ts; it runs offline in under a second:
Output
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.
Full file
Full file
examples/callback-gate.ts
Next steps
- A person decides: the step built on this gate, with a no that runs the writer again.
- Proof-bound acceptance and approval: bind the answer to the exact bytes it judged.
- The record: what a resumed run skips, repeats and asks again.