Script collect, AI summarise

Capture command output in a script node and ask Claude to summarise it.

Use this when you have a deterministic command that produces text (build logs, test reports, a nightly export, system info) and you want a model to turn that output into a useful summary or classification. It's the simplest bridge between command-line tooling and a model that can explain the output in plain language.

The workflow#

name: demo-2-week-of-dinners
version: 1
initial: setup
states:
  setup:
    type: script
    label: Set up the week
    script: |
      const nights = ['Monday', 'Tuesday', 'Wednesday'];
      console.log(`Planning ${nights.length} dinners: ${nights.join(', ')}.`);
      export default nights;
    validator:
      kind: boolean
    on:
      'true': plan
      'false': couldnt-plan

  plan:
    type: map
    label: Plan each night
    model: claude-haiku-4-5
    over: '{{ setup.result }}'
    itemVar: night
    prompt: |
      Plan one dinner for {{ night }}. Give the dish, a one-line why it's
      a good weeknight pick, and a short ingredient list.
    reduce:
      prompt: |
        Here are the dinners you just planned:

        {{ outputs }}

        Turn them into one shopping list, grouped by aisle.
    on:
      success: done
      failure: couldnt-plan

  done:
    type: end
    label: Menu and list ready
    outcome: success

  couldnt-plan:
    type: end
    label: Couldn't finish the plan
    outcome: failure

This is the shape of demo-2-week-of-dinners, one of the starter templates seeded by roscoe init (trimmed here to a few nights; the real template plans a configurable week and layers in diet and pantry inputs).

Running it#

roscoe run demo-2-week-of-dinners

The script step runs immediately, building the list of nights to plan. The map step then plans each night in parallel and folds the results into one shopping list through its reduce step, and the run finishes at done. The web UI shows every node's output side by side; the merged list is also stored on the run record under variables.plan.reduced.

Why these node types#

script with validator: kind: boolean runs the setup logic and returns true, which routes 'true' into the plan step. If you'd rather skip the AI step when the script fails, return false from the script; that routes 'false' straight to couldnt-plan instead of burning model calls on empty input. map suits the planning step because it's one task (plan a dinner) applied across a whole list, not a single forward edge; anything more decision-shaped (classify into one of N categories, decide whether to escalate) wants ai_judge instead, so a validator can parse the response. The template token {{ setup.result }} works because every script node writes its structured result (whatever it export defaults), along with stdout, stderr, and exitCode, to the variables bag, and any of them is available to reference in a downstream prompt or script. The reduce step sees the collected branch outputs as {{ outputs }}, a JSON array, so its prompt is worded to expect a list rather than a single block of text.

Real-world shapes#

For a daily expense digest, the script exports the day's submitted expenses to a CSV and the AI step turns the rows into a one-paragraph, plain-English summary for a manager to skim. For CI summarisation, the script runs bun run test and the AI step summarises failures into a Slack-ready message; pair it with a downstream script node that posts to a webhook. For a build narrator, the script runs git log --since='1 week ago' --oneline and the AI step writes a release-notes draft; wrap it with a human approval node to make it a publish workflow (see /docs/recipes/approval-flow). The same shape covers incident triage too: run kubectl get events, let the AI step extract the likely root cause and rank severity, then route to page-on-call vs. log-only with a condition or ai_judge node.

Passing output to a later step#

A script node can hand its result to a later script two ways. The tidiest is a template reference to the structured result: {{ setup.result.0 }}.

Upstream outputs are also flattened into the environment as ROSCOE_OUT_<node>_<field>, which is handy when a later step wants an upstream value without templating it into the source:

post:
  type: script
  label: Post summary to Slack
  script: |
    import { $ } from 'bun';
    const summary = process.env.ROSCOE_OUT_plan_reduced ?? '';
    await $`curl -X POST ${process.env.SLACK_WEBHOOK} \
      -H "Content-Type: application/json" \
      -d ${JSON.stringify({ text: summary })}`;
    export default true;
  validator:
    kind: boolean
  on:
    'true': done
    'false': done

Reading from the environment is the safer choice for anything large or untrusted: a ${...} interpolation in Bun.$ is escaped for you, so an upstream value full of quotes or shell metacharacters can't break the command.

Scripts also get two per-run values: $ROSCOE_RUN_DIR (a temporary folder unique to this run) and $ROSCOE_RUN_ID (the run's id). Write a file to $ROSCOE_RUN_DIR in one step and read it in a later one; it's safe even when several runs execute at once, unlike the workflow-wide $ROSCOE_WORKFLOW_DIR. See /docs/nodes/script and /docs/authoring/output-chaining for the full run namespace ({{ run.id }} / {{ run.dir }} work in any node).

Variations#

To add a verifier, chain the AI step into an ai_judge that checks whether the result is faithful to the source (see /docs/recipes/multi-step-analysis). To branch on the script's output, return a string instead of a boolean and switch the validator to kind: enum to route on it, e.g. classify by severity. For structured output, use ai_judge with kind: enum so the model returns a known category that drives the next transition instead of free text.

See also#

  • /docs/nodes/script — return values, validators, env passthrough
  • /docs/nodes/map — fan-out modes, reduce, and per-branch outputs
  • /docs/recipes/multi-step-analysis — verify the summary before publishing

View this page as Markdown

Predictable workflows from unpredictable AI