Use this when you want a deterministic automated pipeline to stop, ask a real person to confirm, and only continue if they say yes. It's the canonical shape for refund approvals over a threshold, expense sign-off, release publishing, and any "draft, review, ship" loop where the wrong answer is expensive.
The workflow#
A human node sits between two script nodes. The first script step
prepares some output, the human reviews it, and the second script step only
runs if they approve.
name: refund-approval
version: 1
description: Demonstrates a human approval pause/resume between two script steps.
initial: prepare
states:
prepare:
type: script
label: Draft the refund decision
script: |
console.log('Refund request is 3 days past the 30-day window, amount $640');
export default true;
validator:
kind: boolean
on:
'true': review
'false': done
review:
type: human
label: Approve refund over $500
prompt: Review the refund request and approve or reject it.
on:
approve: process
reject: done
process:
type: script
label: Process refund
script: |
console.log('Refund processed');
export default true;
validator:
kind: boolean
on:
'true': done
'false': done
done:
type: end
label: Done
outcome: successSave it as refund-approval.workflow.yaml in ~/.roscoe/workflows/. It's the
same shape as the approval starter that ships with roscoe init
(script → human → script → end), just relabeled for a refund scenario.
Running it#
From the CLI:
roscoe run refund-approvalThe command prints a run id and exits. The run is now paused at review.
Resume it with the chosen transition:
roscoe resume <run-id> approve # → process step runs
roscoe resume <run-id> reject # → straight to doneFrom the web UI, the run page surfaces two buttons (approve and reject)
when execution reaches the human node. From Claude Code via MCP, the run
pauses with a pendingStep describing the prompt and valid transitions.
Claude is required to display these to the user and wait for their reply
before calling resume_run.
Why these node types#
script with validator: kind: boolean routes on the value the script
returns: 'true' when truthy, 'false' when falsy. This is the simplest
gate; any step that can fail in a way you want to surface should be a
boolean script. Notice the on-keys are quoted strings: YAML would otherwise
parse true: as a real boolean key. human pauses the run and waits for a
transition key; the keys in on are arbitrary, so pick verbs that read well
as buttons (approve, reject, defer, escalate). end with
outcome: success is the terminal node that sets the recorded run outcome;
use failure for the rejected branch if you want the run marked red in
history.
Variations#
If a rejection should count as failure, split the terminal into two end
nodes, one outcome: success and one outcome: failure, and wire
reject: rejected-end so the run is recorded as a failure. For a three-way
decision, add a defer transition pointing back to prepare (or to a
separate requeue step); the web UI renders one button per on key
automatically. To show the draft to the human, reference
{{ prepare.stdout }} in the human prompt (the prepare step already
writes its output there) so reviewers see what they're approving.
See also#
/docs/nodes/script— the value it returns, validators, environment passthrough/docs/recipes/conditional-branching— a non-human alternative when the decision can be expressed as a rule