Skip to content

2.6 Human-in-the-loop

We’ve reached the last failure mode, #6 — waiting for a human. Back in 2.2, the policy gate blocked a dangerous action and asked for approval. But how do you wait for a person? The naive answer breaks everything.

The obvious approach: when the agent needs approval, just… wait. Hold the request open, block the thread, await the human.

That fails twice. A held-open request ties up a server connection for however long the human takes — minutes, hours, a weekend. And the instant the process restarts (a deploy, a crash), the wait is gone — the agent forgets it was ever waiting, and the pending action is lost.

The fix reframes the whole thing. Waiting for a human isn’t the agent pausing — it’s the workflow entering a first-class, durable state called suspended:

  • When approval is needed, the harness suspends the workflow and checkpoints it to the durable log (2.1 again).
  • Nothing is held open. No thread blocks. The workflow simply isn’t running.
  • When a human finally approves (or rejects), the harness resumes the workflow from exactly where it stopped.

Because the suspended state lives in the durable log, it survives restarts. The human can take a week; the process can die and come back a dozen times. The approval is still there, waiting.

Start the task. It’ll hit a payment that needs approval and suspend. Before you approve, press Simulate restart a few times — the process “dies and comes back a day later” — and notice the pending approval is still there. Then decide.

Workflow stateREADY
Press “Start task” to begin.

The workflow sat in SUSPENDED, survived every restart, and resumed the moment you decided — running the payment on approve, skipping it on reject. No thread was ever held open.

async function runStep(call) {
if (policy.check(call) === 'needs_approval') {
await suspend(workflowId, { awaiting: call }); // checkpoint + stop running
return SUSPENDED; // no thread held open
}
return run(call);
}
// Later — could be seconds or days, in a totally different process — a human decides:
async function onDecision(workflowId, decision) {
const state = await load(workflowId); // recovered from the durable log
if (decision === 'approve') await resume(workflowId, run(state.awaiting));
else await resume(workflowId, skip(state.awaiting));
}

suspend and resume are two halves of the same durable machinery from 2.1. Approval didn’t need a new mechanism — it needed durability applied to waiting.

  • Durable-execution engines (Temporal, DBOS, Inngest) expose exactly this: signals/waits that suspend a workflow and resume it on an external event — human approval, a webhook, a timer.

Look back at what you built. Every failure mode from 1.2 now has an answer, and the answers reuse each other:

Failure Fix Builds on
Crash loses progress Durable execution (2.1)
Tools can do anything Sandbox + policy (2.2)
Context grows forever Memory & hydration (2.3) 2.1’s log
One agent guesses wrong Routing & handoffs (2.4)
Serial & fragile Supervision (2.5) 2.2’s isolation
Waiting on a human Human-in-the-loop (2.6) 2.1 + 2.2

That interlocking set of capabilities is the harness. The model still just decides the next step — everything that makes it survive production is the layer you built around it.

© 2026 Clifford Bernard · Content CC BY 4.0 · Code MIT ·Source