Approval
A function with approval: true never runs by itself. What happens when
the code calls one depends on the run's approval option.
deny (default)
The call throws ApprovalRejected into the code without running. Nothing
marked ever runs by omission.
pause: one decision per call, the run waits
await sandbox.run({
code, functions,
approval: {
mode: 'pause',
onApproval: async ({ fn, input, seq }) => (await confirmDialog(`${fn}`, input)) ? 'approve' : 'reject',
},
});The worker waits on the call; the run clock pauses while the human decides;
approve runs it and returns the value, reject throws ApprovalRejected
at the call site. Uncaught, the run ends with that error.
collect: one decision per batch, after the run was cut
let result = await sandbox.run({ code, functions, approval: { mode: 'collect' } });
while (result.status === 'pending') {
// one card, N rows: result.pending = [{ callId, seq, fn, input }, …]
const decisions = await showBatchCard(result.pending); // { all: 'approve' } | { approve: [ids], reject: [ids] }
result = await result.resume(decisions);
}A marked call does not run; the code receives a pending value instead
and goes on. Reads keep executing, writes keep accumulating. The run is cut
at the first observation of a pending value (a property read, a
coercion, a spread, JSON.stringify, a call) or at the end of the pass
with calls still pending. resume re-runs the same code with a replay:
calls already made return their recorded result without executing again,
approved ones execute, rejected ones throw ApprovalRejected. A call that
differs from what was shown ends the run with ReplayMismatch, and nothing
runs. In collect mode calls are serialised in creation order so the
replay is deterministic (a Promise.all still works, queued).
A loop of sixteen deletes is therefore one card with sixteen rows. A second card happens only when the code needed the result of a marked call to continue and then reached another marked call.
With a batch hook the library owns the loop and run never resolves
pending:
await sandbox.run({
code, functions,
approval: { mode: 'collect', onApproval: (pending) => showBatchCard(pending) },
});The two hooks differ on purpose: pause decides one call with a string
while the worker waits; collect decides a batch with Decisions after the
run was cut, and you can approve eight of sixteen.