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:- Claude Code
- Cursor
- Codex
- Any other 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.Save the block to
.cursor/rules/roe-workflow-authoring.mdc, replacing the
name:/description: frontmatter with Cursor’s
rule frontmatter:---
description: Author or edit Roe workflows (WorkflowOrchestrator agents) — DAGs of agents with loops, if-branches, merges — via the Roe MCP tools or public API.
alwaysApply: false
---
Codex reads
AGENTS.md automatically. Append the block’s body (everything
from # Roe workflow authoring down) to your project’s AGENTS.md, or to
~/.codex/AGENTS.md to apply it globally.This page is also served as plain Markdown at
docs.roe-ai.com/mcp/skills/workflow-builder.md
(and listed in docs.roe-ai.com/llms.txt).
Paste it into a system prompt or project instructions, or have the assistant
fetch the URL before authoring a workflow.
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.
Related pages
- MCP Server setup — connect a client and authenticate.
- Introduction to Agents — the child agents workflows are built from.
- Agents API — the underlying REST endpoints.