Human approval flow

Pause a pipeline for a manual approve/reject decision.

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: success

Save 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-approval

The 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 done

From 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

View this page as Markdown

Predictable workflows from unpredictable AI