Skip to content

Asking a human

Some decisions are not the agent’s to make. suspend stops the step and hands the question to a person, using the workflow engine’s own suspension primitive — so the pause is exactly as durable as any other pause the engine takes.

const HITL_01 = Presets.requireHumanBefore(P.stepStart('applyRemediation'), {
id: 'HITL-01',
description: 'the applyRemediation step requires a completed human approval',
mode: 'suspend',
})
const workflow = guardWorkflow(createMonitor({ rules: [HITL_01] }), incidentWorkflow)

requireHumanBefore is written against the run’s own lifecycle rather than against a tool: what counts as an approval is a run.resume, which is something no tool call can fake.

const started = await run.start({ inputData: { amount: 4_000 } })
// started.status === 'suspended'

It carries what blocked it. Show that to a person, and answer:

await run.resume({
step: 'applyRemediation',
resumeData: { [VIOLATION_KEY]: detail, proceed: true },
})

proceed: true runs the step and commits its start event, so the trace records that a human authorised a violation rather than hiding it. proceed: false bails the run naming the rules that stopped it — the step never starts, and nothing it would have done happens.

The incident workflow at event 12, the suspension. applyRemediation is held, HITL-01 is what is holding it, and the run is waiting on an answer that could arrive in a minute or tomorrow. Step forward to watch the approval land and the remediation run. Note events 4 through 9: runDiagnostics is a loop, and the back edge counts its own repetitions.

A callback would make the pause live in your process. The workflow engine’s suspension is a snapshot: the run can be resumed by a different process, after a deploy, days later. That is what makes suspend usable for a decision that a person has to actually think about, rather than one they have to answer before a timeout.

There is one honest caveat. The suspension is durable because the engine snapshots it; the monitor’s own state lives in memory. A process that resumes a run it did not start is monitoring against an empty history. The human decision is still honored — that path reads the resume payload, not the monitor — but a rule that something earlier in the run had already satisfied is satisfied no longer. When that matters, replay the run’s recorded events through monitor.step before resuming, which rebuilds exactly the state the old process held.

Both the step map and the execution graph. A committed workflow holds direct step references in its graph, so replacing the map alone would leave the engine running the originals — which is the kind of guard that looks installed and is not.

The guarded step’s suspend and resume schemas are widened to admit one extra variant alongside whatever the step already declared. Everything else the step accepts is still validated.

A step that suspends for its own reasons is unaffected: the guard notices the body reached for suspend, records a pause rather than a finish, and does not announce a second start when it resumes.