Skip to main content
Build complex workflows on Roe directly from Claude Code, Cursor, Codex, or any AI assistant connected to the Roe MCP server — describe the pipeline you want in plain language and the assistant creates it with the standard agent tools (create_agent, create_agent_version). A workflow chains your existing Roe agents into a pipeline: feed one agent’s output into the next, loop over arrays with parallel iterations, branch on conditions, merge branches back together, and reshape fields between steps. It’s the same engine behind the visual workflow builder, so anything your assistant creates opens — and stays editable — on the canvas in the Roe app.

Install the skill

Workflow authoring has real nuances (reference syntax, loop/branch configs, validation rules) that aren’t discoverable from the API schema alone. This one skill file teaches your assistant all of them. Copy the block below with its copy button, then install it for your client:
Save the block to ~/.claude/skills/roe-workflow-authoring/SKILL.md (or .claude/skills/roe-workflow-authoring/SKILL.md inside a project to scope it to that repo) and restart Claude Code. It triggers automatically whenever you work on Roe workflows.

The skill

---
name: roe-workflow-authoring
description: Author or edit Roe workflows (WorkflowOrchestrator agents) — DAGs of agents with loops, if-branches, merges — via the Roe MCP tools or public API. TRIGGER when the user wants to create, modify, debug, or review a Roe workflow / multi-step agent pipeline. SKIP for running existing agents or non-Roe orchestration.
---

# Roe workflow authoring

A Roe workflow is an agent with `engine_class_id: "WorkflowOrchestrator"`.
Author it with `create_agent` / `create_agent_version`; wire **existing**
agents as nodes (create/version children first — a node always uses the child
agent's current version's config, resolved at workflow save time; the workflow
cannot override it).

Sequence: `list_agent_engine_types``list_agents` +
`get_current_agent_version` (collect `base_agent_id`s and child
`input_definitions`) → `create_agent` → one test run.

## engine_config you submit

```json
{
  "nodes": [...],
  "output_node_id": "<node id whose output is the result>",
  "start_connected_nodes": ["<root agent node ids>"]
}
```

Omit `initial_inputs`, top-level `input_definitions`, `layout`, `edges` — the
backend derives/handles them. Agent node minimal shape:
`{ "id", "node_type": "agent", "base_agent_id", "input_mapping" }`. Do not
fill `agent_version_id` / `agent_name` / `workflow_type` even though the
schema marks them required — the backend recomputes them from `base_agent_id`.
Reserved node ids: `initial`, `__start__`, `__end__`. Min 2 nodes, ≥1 agent
node.

## Reference syntax (input_mapping values)

- `initial.<key>` — workflow input; `<node_id>` — full upstream output dict;
  `<node_id>.<field>` — one field (nested ok: `a.b.c`).
- `"{node.field}"` alone preserves the original type; placeholders embedded in
  a longer string interpolate (dicts/lists JSON-encoded). Anything else is a
  literal string.
- File page slicing: append `|pages=<spec>%` to a file reference to pass only
  those pages — `"{initial.pdf_files}|pages=1-3%"`. Specs are 1-indexed: `1`,
  `1-3`, `1,2,5-7`. Placeholders interpolate first, so loop items work:
  `"{initial.pdf_files}|pages={per_chunk.item}%"`. `pages` is the only
  supported modifier; a malformed spec fails at runtime, not at save.
- Skipped if-branch nodes resolve to null.

## Workflow inputs

`initial.*` keys come ONLY from the `input_definitions` of agents listed in
`start_connected_nodes` (plus `${var}` templates in their configs). Each input
gets a `<key>__file_name` companion. Referencing an undeclared initial key
fails validation. The workflow's run inputs = the referenced keys.

`${var}` in child configs: auto-mapped to `initial.<var>` on Start-connected
nodes; everywhere else they must be satisfied via explicit `input_mapping` or
the save fails.

## CRITICAL ordering rule

Execution order is derived ONLY from `input_mapping` references (plus implicit
if→branch edges). Utility-node `config` refs do NOT order the graph. Always
mirror them as synthetic entries:

- loop: `"input_mapping": { "__loop_source": "<config.source_field>" }`
- if: `"input_mapping": { "__if_cond_0": "<conditions[0].field>", ... }`
- merge inputs: `"__merge_0"`, `"__merge_1"`, ...

## Utility nodes (`node_type`, `config`)

- **loop** — `{ source_field: <ref to array>, element_alias: "item",
  parallel: true, body_node_ids: [...], body_output_node_id: <id, default
  last> }`. Body nodes read `<loop_id>.<alias>` and `<loop_id>.index`.
  Downstream, `<loop_id>` is the ARRAY of per-iteration body outputs
  (per-element fields only exist inside the body). Non-list source is wrapped
  as one element. No nested loops. Body nodes never go in
  `start_connected_nodes`.
- **if_condition** — `{ conditions: [{field, operator, value}], combine:
  "all"|"any", true_node_ids: [...], false_node_ids: [...] }`. Operators:
  equals, not_equals, contains (str/list), greater_than, less_than (float
  coercion), is_empty, is_not_empty, is_true, is_false. `value` is a string,
  coerced to the field's type. Output is only `{_branch}` — downstream cannot
  read data fields off an If; branch nodes read upstream nodes directly.
  Skipped branch outputs become null. If BOTH branch lists are non-empty they
  must converge on a shared downstream node (use a merge) or the save fails.
- **merge**`{ mode: "append" | "combine" }`. `append` flattens inputs into
  one array; `combine` shallow-merges dicts (later keys win). Nulls are
  dropped. Output field is exactly `merged` → reference `<merge_id>.merged`.
- **edit_fields**`config: {}`; each `input_mapping` key becomes an output
  field. Use for renames, literals, `<key>__file_name`, final output shaping.

## Example: branch + merge

Extract → score → branch on risk → merge → result. `extract` is the only
Start-connected node, so the workflow's run inputs are exactly `extract`'s
declared inputs (here `document`).

```json
{
  "name": "Document risk review",
  "engine_class_id": "WorkflowOrchestrator",
  "engine_config": {
    "nodes": [
      { "id": "extract", "node_type": "agent",
        "base_agent_id": "<uuid>",
        "input_mapping": { "document": "initial.document" } },
      { "id": "score_agent", "node_type": "agent",
        "base_agent_id": "<uuid>",
        "input_mapping": { "text": "extract.text" } },
      { "id": "risk_gate", "node_type": "if_condition",
        "config": {
          "conditions": [
            { "field": "score_agent.risk_score", "operator": "greater_than", "value": "0.8" }
          ],
          "combine": "all",
          "true_node_ids": ["deep_review"],
          "false_node_ids": ["auto_approve"]
        },
        "input_mapping": { "__if_cond_0": "score_agent.risk_score" } },
      { "id": "deep_review", "node_type": "agent",
        "base_agent_id": "<uuid>",
        "input_mapping": { "text": "extract.text" } },
      { "id": "auto_approve", "node_type": "edit_fields", "config": {},
        "input_mapping": { "verdict": "approved", "score": "score_agent.risk_score" } },
      { "id": "merge_results", "node_type": "merge",
        "config": { "mode": "append" },
        "input_mapping": { "__merge_0": "deep_review", "__merge_1": "auto_approve" } }
    ],
    "output_node_id": "merge_results",
    "start_connected_nodes": ["extract"]
  }
}
```

## Example: segment → loop → two-pass extract

The canonical big-document pattern, distilled from a production geotech
pipeline that mines 100+-page drilling reports where only some pages hold the
target charts. One agent finds the relevant pages, a parallel loop slices the
PDF per chunk, and a two-pass extractor pulls the data — with a cheap
classifier feeding document-specific hints into the prompts:

```json
{
  "nodes": [
    { "id": "cheatsheet", "node_type": "agent",
      "base_agent_id": "<small-model classifier>",
      "input_mapping": { "pdf_files": "{initial.pdf_files}|pages=1-3%" } },
    { "id": "split", "node_type": "agent",
      "base_agent_id": "<PDFPageSelectionEngine agent>",
      "input_mapping": { "pdf_files": "initial.pdf_files" } },
    { "id": "per_chunk", "node_type": "loop",
      "config": { "source_field": "split.page_ranges",
        "element_alias": "item", "parallel": true,
        "body_node_ids": ["extract_depths", "extract_all"],
        "body_output_node_id": "extract_all" },
      "input_mapping": { "__loop_source": "split.page_ranges" } },
    { "id": "extract_depths", "node_type": "agent",
      "base_agent_id": "<big-model extractor — hard fields only>",
      "input_mapping": {
        "pdf_files": "{initial.pdf_files}|pages={per_chunk.item}%",
        "cheatsheet": "cheatsheet" } },
    { "id": "extract_all", "node_type": "agent",
      "base_agent_id": "<big-model extractor — every field>",
      "input_mapping": {
        "pdf_files": "{initial.pdf_files}|pages={per_chunk.item}%",
        "depths": "extract_depths" } },
    { "id": "final", "node_type": "edit_fields", "config": {},
      "input_mapping": { "results": "per_chunk",
        "file_id": "initial.pdf_files",
        "file_name": "initial.pdf_files__file_name" } }
  ],
  "output_node_id": "final",
  "start_connected_nodes": ["split", "cheatsheet"]
}
```

Why it is built this way:

- **`split`** is a `PDFPageSelectionEngine` agent. It returns `page_ranges`
  an array of 1-indexed range strings (`["88-90", "91"]`). Prompt it to
  segment into the smallest chunks possible, keeping a multi-page chart in
  one range.
- **Page slicing bounds every node's context.** Each iteration reads
  `{initial.pdf_files}|pages={per_chunk.item}%` — only its own pages. Never
  hand the whole document to a per-chunk node.
- **`cheatsheet` runs once, outside the loop.** A cheap-model classifier with
  an enum `output_schema` whose members are the hint strings plus `""` for
  none-of-the-above, so unknown documents degrade gracefully instead of
  hallucinating a hint. The extractors declare a `cheatsheet` input and put
  `${cheatsheet}` inside their instructions — upstream output injected
  straight into a downstream prompt.
- **Two passes, anchor-then-fill.** `extract_depths` extracts ONLY the
  hardest fields with a small focused schema. `extract_all` receives that
  whole output (`"depths": "extract_depths"``${depths}` in its
  instructions), is told to copy those values verbatim, and fills in the
  many easy fields around them. Splitting attention this way beats one agent
  juggling 40 fields.
- **`final` (edit_fields)** shapes the deliverable: the loop's output array
  plus the file-name companion key.
- `start_connected_nodes` lists only the root agents — never the loop or its
  body (body nodes may still reference the `initial.*` keys the roots
  declared).

## Choosing the right construct

- **No workflow at all** — if one agent with a sharper prompt and
  output_schema can do the job, do that. A workflow earns its complexity when
  steps need different models, different page slices, fan-out over an array,
  or isolation of a hard sub-task.
- **Loop vs batch** — loop when the items come from an upstream node's output
  (page ranges, an extracted list). If the caller already holds N independent
  inputs, skip the workflow and use `run_agents_batch`.
- **if_condition vs prompt-level conditionality** — use `if_condition` only
  to SKIP expensive nodes (gate a deep-review agent behind a score
  threshold). Its operators are mechanical, and two-sided branches force a
  converging merge. When both paths feed the same downstream node anyway, or
  the condition is fuzzy/semantic, prefer a cheap checker agent that emits an
  enum or boolean which downstream nodes consume as an ordinary input — no
  branch/merge ceremony.
- **Checker → redo chains** — input_mapping is what orders the graph, so to
  force "extract → verify → re-extract", give the second extractor a declared
  spare input (e.g. `redo`) and map it to the checker's verdict
  (`"redo": "checker.sorry"`). To hand a dict to a text-only checker, embed
  it in a string: `"text": "Result:\n{extract_depths}"` (interpolation
  JSON-encodes).
- **Model tiering** — small model, medium reasoning for gates, checkers, and
  classification; big model, high reasoning only for the heavy extraction
  nodes. Anything constant across chunks (vendor, language, document type)
  belongs BEFORE the loop: loop cost = body cost × N iterations.
- **First page(s) only** — headers, branding, titles, and tables of contents
  live up front; slice them: `"{initial.doc}|pages=1%"` or `|pages=1-3%`.

## Validation & testing

Save-time 400s carry `detail` + per-node `node_errors` (in the MCP error
envelope's `details`) and catch: cycles, orphans (every node must reach
`output_node_id`), bad/self references, undeclared `initial.*`, missing
agents/versions, non-converging two-sided ifs, unresolved `${var}` on
non-Start nodes. NOT caught until runtime: cross-node type mismatches,
references to nonexistent output FIELDS (`extract.txet` → null), if-condition
field typos (conditions aren't validated), stale child agents. So always
test-run: `run_agent` (sync, under ~25s) or `trigger_agent_run` → poll
`get_agent_job_status` (terminal: 3 SUCCESS, 4 FAILURE, 5 CANCELLED,
6 CACHED) → `get_agent_job_result`, and inspect before handing off.

Editing an existing workflow: `get_current_agent_version` → modify the full
`engine_config``create_agent_version` (configs are immutable snapshots;
always resend the whole graph).

## Visual builder round-trip

Omitting `layout`/`edges` is safe — the canvas reconstructs edges from
`input_mapping` and `*_node_ids`, but nodes render stacked at the origin until
a human arranges them once.