Robomotion Flow Builder
Robomotion is an RPA platform with a TypeScript SDK and a visual node editor. This skill is a thin index over the reference docs in ./docs/. Read the relevant doc when a topic comes up — don't try to memorize it from this file.
Hard Rules (SDK rejects violations at validate time)
The SDK enforces these. Violations throw at robomotion validate / build with descriptive messages, so the agent never silently produces broken flows:
- Node IDs MUST be 6-char lowercase hex —
/^[0-9a-f]{6}$/. f.node(), .then(), f.edge() reject non-hex IDs ('begin', 'label', 'maps', uppercase) at registration. Pick fresh hex per node. See ./docs/reference/id-format.md.
- Subflow node ID =
subflows/<id>.ts filename, exactly. Both must be 6-hex. The Designer's "enter subflow" UX depends on the match.
f.addDependency(namespace, version) is validated against the live package index. version must be concrete ('latest' is rejected) and must exist in the package's published versions list. namespace must exist in https://packages.robomotion.io/stable/index.json. Run robomotion get packages <ns> or robomotion describe package <ns> to resolve real versions before calling addDependency. Never invent a version.
- Terminal nodes (
Debug, Log, Stop, GoTo, End, WaitGroup.Done) have 0 outputs — wire TO them via f.edge(), never .then() from them.
- Every
Core.Flow.GoTo references a Core.Flow.Label id that exists in the same flow file.
Required First Line
Every flow file (main.ts and every subflows/*.ts) starts with this exact import — copy verbatim, including helpers you don't currently use (Bun won't flag dead imports, but missing ones become runtime ReferenceError):
import { flow, Message, Custom, JS, Global, Flow, Credential, AI } from '@robomotion/sdk';
For library files swap flow for library / subflow. Full reference: ./docs/reference/imports.md.
Builder grammar
f.node(id, type, name, props) — param order. Only emit non-default props (Go runtime fills defaults from pspec).
.then() for sequential, .edge() for multi-port wiring.
Message(name) for variables · Custom(value) for literals · JS(expr) for one-line JS · Credential({vaultId, itemId}) for secrets.
- A field takes a scope helper based on its TYPE IN THAT NODE, not its
in*/opt* name — and the SAME name can differ across nodes. Every in*/out* port takes a scope helper (Custom('…') / Message()); a bare literal there is silently dropped. For opt* fields, don't guess from the name — check get_node_schema: a field typed object + variableType (even a numeric one) takes a scope helper; a plain number/boolean/enum field takes a bare literal and must NOT be wrapped. The very same property can differ by node: Core.Browser.OpenLink optTimeout is a plain number → optTimeout: 32, but Core.Browser.WaitElement optTimeout is variableType:Integer → optTimeout: Custom('30'). Mismatching either way VALIDATES but FAILS TO LOAD on the robot (flow_error: failed / Config parse error, no nodes run): wrapping a plain field sends an object to a scalar; leaving a variable-backed field bare sends a scalar into a {scope,name} slot. Enums/booleans are always plain (optBrowser: 'chrome', optMethod: 'post', optInsecure: true); in* ports and value fields like optUrl/optDownloadDir/optNofBranches always take Custom()/Message().
func is a literal string (NOT JS()).
- Common runtime props also take raw values:
delayBefore: 2, delayAfter: 0.5, continueOnError: true.
- ES5-only inside
func: no =>, no template literals, no const/let, no destructuring. No require() / fs / Buffer / process (pure JS sandbox).
- Loops:
Label → ForEach → body → GoTo. Stop is standalone, wired via f.edge() on ForEach port 1.
- Library projects use
library.create(id, name, fn) with Begin/End nodes (no .start()). Inline subflows use subflow.create(name, fn).
- Every flow ends with
.start(). Every flow has a Core.Flow.Stop node — except an app backend: a flow triggered by Robomotion.Apps.Action is a long-lived service behind a Robomotion App's screens and must never stop, so it has no Stop and no End. See the building-app skill.
Core.* packages (Core.Trigger, Core.Browser, Core.Programming, Core.CSV, Core.Flow, Core.Vault, Core.Net, Core.Excel, …) are embedded in the robot — NEVER call f.addDependency('Core.*', …). The Designer auto-loads them. Only call f.addDependency(ns, ver) for non-Core.* packages. When updating an existing flow, NEVER bump existing addDependency versions; only add missing ones.
- Comments & canvas layout —
Core.Flow.Comment nodes (with an optText markdown string) title the flow and fence its logical phases; the visual arrangement — node positions, comment box colors/sizes, and Sugiyama-style layering — lives in main.designer.ts. Layout is cosmetic (never affects runtime) but it's what makes a flow readable. See ./docs/patterns/comments-and-layout.md.
Full grammar: ./docs/sdk-grammar.md. Architecture: ./docs/architecture.md.
Diagnostic map
Map an error symptom to the doc that fixes it. When validate_flow fails, look up the symptom here before reading the full failure trace.
| Symptom |
Likely cause |
Fix |
[SDK] Invalid node ID '<x>' in f.node('<x>', …) |
Semantic / non-hex ID |
./docs/reference/id-format.md — pick 6-hex |
[SDK] Invalid subflow filename '<x>.ts' |
Subflow filename non-hex |
Rename file + update parent SubFlow node ID to match |
version must be concrete; 'latest' and empty are not allowed |
f.addDependency(ns, 'latest') |
robomotion describe package <ns> → pin a real version |
package '<ns>' not found in repository |
Hallucinated namespace |
robomotion get packages <kw> → use the real namespace |
version '<v>' is not published for <ns> |
Wrong version pinned |
Pick from available_versions returned by validator |
Cannot chain from node (outputs=0) |
.then() after Debug/Log/Stop/GoTo/End |
Wire TO terminals via f.edge(), never FROM them |
Invalid input port 0. Node has 0 input(s) on a Label |
Wired into Core.Flow.Label (Label has 0 inputs in some pspecs) |
Use Core.Flow.GoTo with optNodes.ids: [<labelId>] to jump to the Label |
Vault has to be selected at runtime |
Missing optCredentials on Core.Vault.GetItem |
./docs/patterns/credentials.md |
Property 'optCredentials' requires vault credentials but has empty/placeholder values |
An OPTIONAL credential prop (e.g. Core.Excel.Open for password-protected files) set with _/blank placeholders |
Omit optCredentials entirely unless you have a real vault reference — ./docs/patterns/credentials.md |
inSelectorType invalid value 'xpath' (allowed: xpath:position, css) |
Wrote inSelectorType: 'xpath' — not a valid enum value |
For XPath just OMIT inSelectorType (it's the default); the XPath enum literal is xpath:position, never xpath. CSS ⇒ inSelectorType: 'css'. ./docs/patterns/browser.md |
inLabel property not found on GoTo |
Wrong property |
optNodes: { ids: [...], type: 'goto', all: false } |
Core.Programming.If not found |
Node doesn't exist |
Core.Programming.Function with outputs: 2 (./docs/patterns/conditions.md) |
Wrong node name (e.g. Core.CSV.Read, Browser.Click) |
Common naming mistake |
./docs/reference/node-naming.md |
inPath: Custom('$Home$/file') literal not resolved |
System variables only resolve in Function nodes |
global.get('$Home$') + '/file' (./docs/reference/system-variables.md) |
Flow VALIDATES but FAILS TO LOAD on robot (flow_error: failed / Config parse error, no nodes run) |
An opt* value shape mismatches its per-node type: a plain number/bool/enum field wrapped in Custom(), OR a variableType/object field left as a bare literal |
Check get_node_schema per node. Enums/bools are plain (optBrowser: 'chrome'). Numeric fields depend on the node: OpenLink.optTimeout: 32 (plain number) vs WaitElement.optTimeout: Custom('30') (variableType:Integer). in* ports + optUrl/optDownloadDir/optNofBranches always take Custom()/Message(). |
| Any CSV / Excel / Sheets / SQLite / Pandas / Airtable / DOMParser / DataTable node in scope |
Custom data shape is wrong (e.g. {header: [...]}, rows as arrays) |
MANDATORY read ./docs/patterns/data-tables.md — the format is {columns: [...], rows: [{key: value}]} with row keys matching column names |
Write produces empty cells / ErrFilePath / "table not recognized" |
header instead of columns, or rows are arrays not objects |
./docs/patterns/data-tables.md — the property is columns, never header; rows are objects keyed by column name, never positional arrays |
Drift-prone reminders before every Write / Edit of flow code:
- Never output TypeScript as chat text — always use
Write / Edit. Plans and explanations stay in chat.
- Hex IDs from the start. Cross-references (
optNodes.ids, Catch.optNodes.ids, subflow filenames) must use the same hex.
- For browser flows: explore the live page first (
Skill(exploring-browser) or mcp__browser__* after ToolSearch warmup). Don't guess selectors. Core.Browser.* element nodes (ClickElement/TypeText/GetValue/SetValue/WaitElement/Select) default inSelector to XPath — translate CSS handles you find (#email, input[type="email"]) to XPath (//input[@id='email']) and omit inSelectorType; use a CSS string ONLY with inSelectorType: 'css' (plain literal — inSelectorType is an enum, so NEVER Custom('css')). A CSS string with the default engine fails at runtime with "element not found". Never write inSelectorType: 'xpath' (invalid; the value is xpath:position). Also: enum/dropdown opts (optBrowser, optProxy, optProxyAuth, optClickType) take a PLAIN string/boolean — NEVER Custom(); wrapping an enum in Custom() emits a {name,scope} object and the robot rejects the node at load with Config parse error (flow never starts). Custom()/Message() are only for variable value fields (selectors/URLs/text/paths). See ./docs/patterns/browser.md.
- For any flow that READS or WRITES tabular data (CSV / Excel / Google Sheets / Excel 365 / SQLite / Airtable / Pandas / DataTable / DOMParser) — read
./docs/patterns/data-tables.md BEFORE adding the node, both for the Function that builds the table AND for the reader/writer node. That doc names the exact node and shows its properties (e.g. write CSV = Core.CSV.WriteCSV with inFilePath + inTable; write Sheets = Robomotion.GoogleSheets.SetRange; etc.) and the {columns: [...], rows: [{key: value}]} format (never {header: ...}, never rows-as-arrays). Do NOT unified_search / search for data-output nodes — search returns TEMPLATES, not nodes, and looping on it wastes the turn. The node names are in data-tables.md; once you know the node, use get_node_schema for its exact properties. When a search returns templates instead of the node you need, stop searching and read the relevant pattern doc.
- For any
Robomotion.ChatAssistant flow in conversational mode — read ./docs/patterns/conversational-chat.md BEFORE writing it. One user message is one ChatIn → ChatOut run and ChatOut is the only thing that unlocks the composer, so a branch that ends anywhere else (an error, a missing ChatOut) freezes the chat until the page is reloaded — the most common bug in these flows, and it looks like a product fault rather than a flow fault. That doc also carries the streaming wiring (Callback In stream_delta → Streaming Text, and no Text node repeating the answer) and the attachments wiring (GetAttachments, because msg.payload.files is names and versions, never files on disk).
- Validate BEFORE save —
save_flow only compiles, it does NOT pspec-validate.
Pattern reference
Read these docs before writing the corresponding code:
| Pattern |
Doc |
| Loops (Label → ForEach → body → GoTo) |
./docs/patterns/loops.md |
Conditions (Function with outputs: N) |
./docs/patterns/conditions.md |
| Credentials (vault + categories) |
./docs/patterns/credentials.md |
| Browser automation (incl. proxy) |
./docs/patterns/browser.md |
| Exception handling (Catch, continueOnError) |
./docs/patterns/exceptions.md |
| Branches & parallel (ForkBranch, WaitGroup) |
./docs/patterns/branches.md |
| Subflows (Begin/End, multi-output) |
./docs/patterns/subflows.md |
Data tables (CSV / Excel / Sheets / SQLite / Pandas / Airtable / DOMParser / DataTable) — MANDATORY before writing any code that produces or consumes msg.table |
./docs/patterns/data-tables.md |
| Captcha solving |
./docs/patterns/captcha.md |
Migrating a legacy Robomotion.Assistant flow → Robomotion.ChatAssistant |
./docs/patterns/assistant-migration.md |
| Conversational Chat Assistant (turn contract, Stop, streaming, attachments) — MANDATORY before writing any conversational-mode chat flow |
./docs/patterns/conversational-chat.md |
Comments, grouping & Sugiyama layout (title box, colored phase headers + description text, box sizing, main.designer.ts) |
./docs/patterns/comments-and-layout.md |
References:
| Topic |
Doc |
| Imports (every scope helper + example) |
./docs/reference/imports.md |
| Node ID format (the hex rule) |
./docs/reference/id-format.md |
System variables ($Home$, $TempDir$) |
./docs/reference/system-variables.md |
| Node naming (wrong → correct) |
./docs/reference/node-naming.md |
| Credential categories (field layouts) |
./docs/reference/credential-categories.md |
For schemas, examples, and package docs, use the robomotion CLI (it's already on PATH, call it by bare name):
| Need |
Command |
| Cross-source fuzzy/semantic search |
robomotion search <query> |
| Find packages |
robomotion get packages [query] |
| Find nodes |
robomotion get nodes [query] [--in <ns>] |
| Find templates |
robomotion get templates [query] [--category <name>] [--tag <name>] |
| Full node schema + docs + example |
robomotion describe node <type>[,<type>...] |
| Package info (incl. published versions) |
robomotion describe package <namespace> |
| Template source |
robomotion describe template <slug> |
| Package docs (llms.txt) |
robomotion docs <namespace> [--grep <pattern>] |
| List vaults / vault items |
robomotion get vaults · robomotion get vault-items <vault-id> |
| List robots |
robomotion get robots |
Public templates repo: github.com/robomotionio/robomotion-templates is the canonical source. Prefer cloning/forking a matching template over building from scratch.
Workflow
Full step-by-step: ./docs/workflow.md. Outline:
- Gather requirements (interactive only) — credentials (commit to a vault-item pick, don't quiz the user; never ask for the secret itself —
vault_picker, or ask them to add it to Vault first: ./docs/patterns/credentials.md), URLs, files, iteration, error handling.
- Discover —
robomotion search, robomotion get nodes, robomotion docs <namespace> (MANDATORY for every non-Core.* package).
- Plan — output plan as chat text, then
AskUserQuestion(["Build it", "Modify plan"]).
- Write — read 1-2 relevant
./docs/patterns/*.md, verify property names with robomotion describe node, then Write main.ts (and any subflows/<id>.ts). For browser flows: explore live first.
- Validate — call
validate_flow MCP tool. Pspec-checks AND dependency-checks. MUST run BEFORE save.
- Save —
save_flow if registered (Designer / pi); else git commit && git push from inside the flow dir. This is the terminal step. Stop here and report success — do NOT chain into running the flow. Running is a separate user request handled by the running-flow skill.
If invoked in direct mode ("Write main.ts for X", "Generate a flow that does Y"), skip 0-2 and jump to 3.
Browser caveat: if code changed after the initial exploration (different selectors, new actions), re-verify selectors against the live page before saving. Selectors are owned by Step 3, not a post-save step.
Canonical example (simple chain)
For loop / conditional / subflow / catch examples, see the corresponding pattern docs — they have richer working snippets.
import { flow, Message, Custom } from '@robomotion/sdk';
flow.create('main', 'Simple Flow', (f) => {
f.node('42ec21', 'Core.Trigger.Inject', 'Start', {})
.then('7dbafc', 'Core.Programming.Function', 'Setup', {
func: `msg.url = 'https://example.com'; return msg;`
})
.then('a06926', 'Core.Browser.Open', 'Open Browser', {
outBrowserId: Message('browser_id')
})
.then('8e1c4b', 'Core.Browser.OpenLink', 'Navigate', {
inBrowserId: Message('browser_id'),
inUrl: Message('url'),
outPageId: Message('page_id')
})
.then('d52f73', 'Core.Browser.Close', 'Close', {
inBrowserId: Message('browser_id')
})
.then('b9a841', 'Core.Flow.Stop', 'Stop', {});
}).start();
CLI & MCP
robomotion — self-sufficient CLI. Builds, validates, runs, searches, inspects. robomotion help for the full verb list.
robomotion-browser-mcp — MCP server for interactive browser exploration (used by exploring-browser and mcp__browser__* tools).
The robomotion CLI shells out to robomotion-sdk-mcp internally for search-backed commands and calls api.robomotion.io directly for run/stop/vault/robot operations. No additional MCP servers required.
Regression suite
This skill ships with an automated eval suite at ./evals/ — Tier A pinpoint regressions (handcrafted fixtures, one rule each) plus Tier B integration tests (live main.ts from the public robomotion-templates repo). Run bun run skills/creating-flow/evals/run-evals.ts from the agent-skills root before committing edits to this SKILL.md or the ./docs/ files. See ./evals/README.md for adding new cases and the assertion grammar.
Related skills
validating-flow — schema validation
testing-flow — behavioral tests
running-flow — execute on robot
searching-packages — find packages, nodes, templates
exploring-browser — interactive browser automation
reversing-network — convert a browser flow to HTTP after capturing traffic
1---2name: creating-flow3description: Creates Robomotion automation flows with the @robomotion/sdk TypeScript builder. Owns the full lifecycle: requirements → plan → build → validate → deploy. Also use when the user has a plan ready and wants the flow code written.4---56# Robomotion Flow Builder78Robomotion is an RPA platform with a TypeScript SDK and a visual node editor. This skill is a thin index over the reference docs in `./docs/`. Read the relevant doc when a topic comes up — don't try to memorize it from this file.910## Hard Rules (SDK rejects violations at validate time)1112The SDK enforces these. Violations throw at `robomotion validate` / `build` with descriptive messages, so the agent never silently produces broken flows:13141. **Node IDs MUST be 6-char lowercase hex** — `/^[0-9a-f]{6}$/`. `f.node()`, `.then()`, `f.edge()` reject non-hex IDs (`'begin'`, `'label'`, `'maps'`, uppercase) at registration. Pick fresh hex per node. See `./docs/reference/id-format.md`.152. **Subflow node ID = `subflows/<id>.ts` filename, exactly.** Both must be 6-hex. The Designer's "enter subflow" UX depends on the match.163. **`f.addDependency(namespace, version)` is validated against the live package index.** `version` must be concrete (`'latest'` is rejected) and must exist in the package's published `versions` list. `namespace` must exist in `https://packages.robomotion.io/stable/index.json`. Run `robomotion get packages <ns>` or `robomotion describe package <ns>` to resolve real versions before calling `addDependency`. Never invent a version.174. **Terminal nodes (`Debug`, `Log`, `Stop`, `GoTo`, `End`, `WaitGroup.Done`) have 0 outputs** — wire TO them via `f.edge()`, never `.then()` from them.185. **Every `Core.Flow.GoTo` references a `Core.Flow.Label` id that exists in the same flow file.**1920## Required First Line2122Every flow file (`main.ts` and every `subflows/*.ts`) starts with this exact import — copy verbatim, including helpers you don't currently use (Bun won't flag dead imports, but missing ones become runtime `ReferenceError`):2324```ts25import { flow, Message, Custom, JS, Global, Flow, Credential, AI } from '@robomotion/sdk';26```2728For library files swap `flow` for `library` / `subflow`. Full reference: `./docs/reference/imports.md`.2930## Builder grammar3132- `f.node(id, type, name, props)` — param order. Only emit non-default props (Go runtime fills defaults from pspec).33- `.then()` for sequential, `.edge()` for multi-port wiring.34- `Message(name)` for variables · `Custom(value)` for literals · `JS(expr)` for one-line JS · `Credential({vaultId, itemId})` for secrets.35- **A field takes a scope helper based on its TYPE IN THAT NODE, not its `in*`/`opt*` name — and the SAME name can differ across nodes.** Every `in*`/`out*` port takes a scope helper (`Custom('…')` / `Message()`); a bare literal there is silently dropped. For `opt*` fields, don't guess from the name — check `get_node_schema`: a field typed `object` + `variableType` (even a numeric one) takes a scope helper; a plain `number`/`boolean`/`enum` field takes a bare literal and must NOT be wrapped. **The very same property can differ by node:** `Core.Browser.OpenLink` `optTimeout` is a plain number → `optTimeout: 32`, but `Core.Browser.WaitElement` `optTimeout` is `variableType:Integer` → `optTimeout: Custom('30')`. Mismatching either way VALIDATES but FAILS TO LOAD on the robot (`flow_error: failed` / `Config parse error`, no nodes run): wrapping a plain field sends an object to a scalar; leaving a variable-backed field bare sends a scalar into a `{scope,name}` slot. Enums/booleans are always plain (`optBrowser: 'chrome'`, `optMethod: 'post'`, `optInsecure: true`); `in*` ports and value fields like `optUrl`/`optDownloadDir`/`optNofBranches` always take `Custom()`/`Message()`.36- `func` is a literal string (NOT `JS()`).37- Common runtime props also take raw values: `delayBefore: 2`, `delayAfter: 0.5`, `continueOnError: true`.38- ES5-only inside `func`: no `=>`, no template literals, no `const`/`let`, no destructuring. No `require()` / `fs` / `Buffer` / `process` (pure JS sandbox).39- Loops: `Label → ForEach → body → GoTo`. `Stop` is standalone, wired via `f.edge()` on ForEach port 1.40- Library projects use `library.create(id, name, fn)` with `Begin`/`End` nodes (no `.start()`). Inline subflows use `subflow.create(name, fn)`.41- Every flow ends with `.start()`. Every flow has a `Core.Flow.Stop` node — **except an app backend**: a flow triggered by `Robomotion.Apps.Action` is a long-lived service behind a Robomotion App's screens and must never stop, so it has no `Stop` and no `End`. See the `building-app` skill.42- `Core.*` packages (`Core.Trigger`, `Core.Browser`, `Core.Programming`, `Core.CSV`, `Core.Flow`, `Core.Vault`, `Core.Net`, `Core.Excel`, …) are **embedded in the robot** — NEVER call `f.addDependency('Core.*', …)`. The Designer auto-loads them. Only call `f.addDependency(ns, ver)` for non-`Core.*` packages. When updating an existing flow, NEVER bump existing `addDependency` versions; only add missing ones.43- **Comments & canvas layout** — `Core.Flow.Comment` nodes (with an `optText` markdown string) title the flow and fence its logical phases; the visual arrangement — node `positions`, comment box colors/sizes, and Sugiyama-style layering — lives in `main.designer.ts`. Layout is cosmetic (never affects runtime) but it's what makes a flow readable. See `./docs/patterns/comments-and-layout.md`.4445Full grammar: `./docs/sdk-grammar.md`. Architecture: `./docs/architecture.md`.4647## Diagnostic map4849Map an error symptom to the doc that fixes it. When `validate_flow` fails, look up the symptom here before reading the full failure trace.5051| Symptom | Likely cause | Fix |52|---|---|---|53| `[SDK] Invalid node ID '<x>' in f.node('<x>', …)` | Semantic / non-hex ID | `./docs/reference/id-format.md` — pick 6-hex |54| `[SDK] Invalid subflow filename '<x>.ts'` | Subflow filename non-hex | Rename file + update parent SubFlow node ID to match |55| `version must be concrete; 'latest' and empty are not allowed` | `f.addDependency(ns, 'latest')` | `robomotion describe package <ns>` → pin a real version |56| `package '<ns>' not found in repository` | Hallucinated namespace | `robomotion get packages <kw>` → use the real namespace |57| `version '<v>' is not published for <ns>` | Wrong version pinned | Pick from `available_versions` returned by validator |58| `Cannot chain from node (outputs=0)` | `.then()` after `Debug`/`Log`/`Stop`/`GoTo`/`End` | Wire TO terminals via `f.edge()`, never FROM them |59| `Invalid input port 0. Node has 0 input(s)` on a Label | Wired into `Core.Flow.Label` (Label has 0 inputs in some pspecs) | Use `Core.Flow.GoTo` with `optNodes.ids: [<labelId>]` to jump to the Label |60| `Vault has to be selected` at runtime | Missing `optCredentials` on `Core.Vault.GetItem` | `./docs/patterns/credentials.md` |61| `Property 'optCredentials' requires vault credentials but has empty/placeholder values` | An OPTIONAL credential prop (e.g. `Core.Excel.Open` for password-protected files) set with `_`/blank placeholders | Omit `optCredentials` entirely unless you have a real vault reference — `./docs/patterns/credentials.md` |62| `inSelectorType` invalid value `'xpath'` (allowed: `xpath:position`, `css`) | Wrote `inSelectorType: 'xpath'` — not a valid enum value | For XPath just OMIT `inSelectorType` (it's the default); the XPath enum literal is `xpath:position`, never `xpath`. CSS ⇒ `inSelectorType: 'css'`. `./docs/patterns/browser.md` |63| `inLabel` property not found on GoTo | Wrong property | `optNodes: { ids: [...], type: 'goto', all: false }` |64| `Core.Programming.If` not found | Node doesn't exist | `Core.Programming.Function` with `outputs: 2` (`./docs/patterns/conditions.md`) |65| Wrong node name (e.g. `Core.CSV.Read`, `Browser.Click`) | Common naming mistake | `./docs/reference/node-naming.md` |66| `inPath: Custom('$Home$/file')` literal not resolved | System variables only resolve in Function nodes | `global.get('$Home$') + '/file'` (`./docs/reference/system-variables.md`) |67| Flow VALIDATES but FAILS TO LOAD on robot (`flow_error: failed` / `Config parse error`, no nodes run) | An `opt*` value shape mismatches its per-node type: a plain `number`/`bool`/`enum` field wrapped in `Custom()`, OR a `variableType`/object field left as a bare literal | Check `get_node_schema` per node. Enums/bools are plain (`optBrowser: 'chrome'`). Numeric fields depend on the node: `OpenLink.optTimeout: 32` (plain `number`) vs `WaitElement.optTimeout: Custom('30')` (`variableType:Integer`). `in*` ports + `optUrl`/`optDownloadDir`/`optNofBranches` always take `Custom()`/`Message()`. |68| Any CSV / Excel / Sheets / SQLite / Pandas / Airtable / DOMParser / DataTable node in scope | Custom data shape is wrong (e.g. `{header: [...]}`, rows as arrays) | **MANDATORY** read `./docs/patterns/data-tables.md` — the format is `{columns: [...], rows: [{key: value}]}` with row keys matching column names |69| Write produces empty cells / `ErrFilePath` / "table not recognized" | `header` instead of `columns`, or rows are arrays not objects | `./docs/patterns/data-tables.md` — the property is `columns`, never `header`; rows are objects keyed by column name, never positional arrays |7071Drift-prone reminders before every `Write` / `Edit` of flow code:7273- Never output TypeScript as chat text — always use `Write` / `Edit`. Plans and explanations stay in chat.74- Hex IDs from the start. Cross-references (`optNodes.ids`, `Catch.optNodes.ids`, subflow filenames) must use the same hex.75- For browser flows: explore the live page first (`Skill(exploring-browser)` or `mcp__browser__*` after `ToolSearch` warmup). Don't guess selectors. **`Core.Browser.*` element nodes (`ClickElement`/`TypeText`/`GetValue`/`SetValue`/`WaitElement`/`Select`) default `inSelector` to XPath — translate CSS handles you find (`#email`, `input[type="email"]`) to XPath (`//input[@id='email']`) and omit `inSelectorType`; use a CSS string ONLY with `inSelectorType: 'css'` (plain literal — `inSelectorType` is an enum, so NEVER `Custom('css')`). A CSS string with the default engine fails at runtime with "element not found". Never write `inSelectorType: 'xpath'` (invalid; the value is `xpath:position`). Also: enum/dropdown opts (`optBrowser`, `optProxy`, `optProxyAuth`, `optClickType`) take a PLAIN string/boolean — NEVER `Custom()`; wrapping an enum in `Custom()` emits a `{name,scope}` object and the robot rejects the node at load with `Config parse error` (flow never starts). `Custom()`/`Message()` are only for variable value fields (selectors/URLs/text/paths). See `./docs/patterns/browser.md`.**76- For any flow that READS or WRITES tabular data (CSV / Excel / Google Sheets / Excel 365 / SQLite / Airtable / Pandas / DataTable / DOMParser) — read `./docs/patterns/data-tables.md` BEFORE adding the node, both for the Function that builds the table AND for the reader/writer node. That doc names the exact node and shows its properties (e.g. write CSV = `Core.CSV.WriteCSV` with `inFilePath` + `inTable`; write Sheets = `Robomotion.GoogleSheets.SetRange`; etc.) and the `{columns: [...], rows: [{key: value}]}` format (never `{header: ...}`, never rows-as-arrays). **Do NOT `unified_search` / `search` for data-output nodes — search returns TEMPLATES, not nodes, and looping on it wastes the turn. The node names are in data-tables.md; once you know the node, use `get_node_schema` for its exact properties.** When a search returns templates instead of the node you need, stop searching and read the relevant pattern doc.77- For any `Robomotion.ChatAssistant` flow in **conversational** mode — read `./docs/patterns/conversational-chat.md` BEFORE writing it. One user message is one `ChatIn → ChatOut` run and **`ChatOut` is the only thing that unlocks the composer**, so a branch that ends anywhere else (an error, a missing `ChatOut`) freezes the chat until the page is reloaded — the most common bug in these flows, and it looks like a product fault rather than a flow fault. That doc also carries the streaming wiring (Callback In `stream_delta` → `Streaming Text`, and **no `Text` node repeating the answer**) and the attachments wiring (`GetAttachments`, because `msg.payload.files` is names and versions, never files on disk).78- Validate BEFORE save — `save_flow` only compiles, it does NOT pspec-validate.7980## Pattern reference8182Read these docs before writing the corresponding code:8384| Pattern | Doc |85|---|---|86| Loops (Label → ForEach → body → GoTo) | `./docs/patterns/loops.md` |87| Conditions (Function with `outputs: N`) | `./docs/patterns/conditions.md` |88| Credentials (vault + categories) | `./docs/patterns/credentials.md` |89| Browser automation (incl. proxy) | `./docs/patterns/browser.md` |90| Exception handling (Catch, continueOnError) | `./docs/patterns/exceptions.md` |91| Branches & parallel (ForkBranch, WaitGroup) | `./docs/patterns/branches.md` |92| Subflows (Begin/End, multi-output) | `./docs/patterns/subflows.md` |93| Data tables (CSV / Excel / Sheets / SQLite / Pandas / Airtable / DOMParser / DataTable) — **MANDATORY** before writing any code that produces or consumes `msg.table` | `./docs/patterns/data-tables.md` |94| Captcha solving | `./docs/patterns/captcha.md` |95| Migrating a legacy `Robomotion.Assistant` flow → `Robomotion.ChatAssistant` | `./docs/patterns/assistant-migration.md` |96| Conversational Chat Assistant (turn contract, Stop, streaming, attachments) — **MANDATORY** before writing any conversational-mode chat flow | `./docs/patterns/conversational-chat.md` |97| Comments, grouping & Sugiyama layout (title box, colored phase headers + description text, box sizing, `main.designer.ts`) | `./docs/patterns/comments-and-layout.md` |9899References:100101| Topic | Doc |102|---|---|103| Imports (every scope helper + example) | `./docs/reference/imports.md` |104| Node ID format (the hex rule) | `./docs/reference/id-format.md` |105| System variables (`$Home$`, `$TempDir$`) | `./docs/reference/system-variables.md` |106| Node naming (wrong → correct) | `./docs/reference/node-naming.md` |107| Credential categories (field layouts) | `./docs/reference/credential-categories.md` |108109For schemas, examples, and package docs, use the `robomotion` CLI (it's already on `PATH`, call it by bare name):110111| Need | Command |112|---|---|113| Cross-source fuzzy/semantic search | `robomotion search <query>` |114| Find packages | `robomotion get packages [query]` |115| Find nodes | `robomotion get nodes [query] [--in <ns>]` |116| Find templates | `robomotion get templates [query] [--category <name>] [--tag <name>]` |117| Full node schema + docs + example | `robomotion describe node <type>[,<type>...]` |118| Package info (incl. published versions) | `robomotion describe package <namespace>` |119| Template source | `robomotion describe template <slug>` |120| Package docs (llms.txt) | `robomotion docs <namespace> [--grep <pattern>]` |121| List vaults / vault items | `robomotion get vaults` · `robomotion get vault-items <vault-id>` |122| List robots | `robomotion get robots` |123124> **Public templates repo:** [`github.com/robomotionio/robomotion-templates`](https://github.com/robomotionio/robomotion-templates) is the canonical source. Prefer cloning/forking a matching template over building from scratch.125126## Workflow127128Full step-by-step: **`./docs/workflow.md`**. Outline:1291300. **Gather requirements** (interactive only) — credentials (commit to a vault-item pick, don't quiz the user; **never ask for the secret itself** — `vault_picker`, or ask them to add it to Vault first: `./docs/patterns/credentials.md`), URLs, files, iteration, error handling.1311. **Discover** — `robomotion search`, `robomotion get nodes`, `robomotion docs <namespace>` (MANDATORY for every non-`Core.*` package).1322. **Plan** — output plan as chat text, then `AskUserQuestion(["Build it", "Modify plan"])`.1333. **Write** — read 1-2 relevant `./docs/patterns/*.md`, verify property names with `robomotion describe node`, then `Write` `main.ts` (and any `subflows/<id>.ts`). For browser flows: explore live first.1344. **Validate** — call `validate_flow` MCP tool. Pspec-checks AND dependency-checks. MUST run BEFORE save.1355. **Save** — `save_flow` if registered (Designer / pi); else `git commit && git push` from inside the flow dir. **This is the terminal step.** Stop here and report success — do NOT chain into running the flow. Running is a separate user request handled by the `running-flow` skill.136137If invoked in **direct mode** ("Write main.ts for X", "Generate a flow that does Y"), skip 0-2 and jump to 3.138139**Browser caveat:** if code changed after the initial exploration (different selectors, new actions), re-verify selectors against the live page before saving. Selectors are owned by Step 3, not a post-save step.140141## Canonical example (simple chain)142143For loop / conditional / subflow / catch examples, see the corresponding pattern docs — they have richer working snippets.144145```typescript146import { flow, Message, Custom } from '@robomotion/sdk';147148flow.create('main', 'Simple Flow', (f) => {149 f.node('42ec21', 'Core.Trigger.Inject', 'Start', {})150 .then('7dbafc', 'Core.Programming.Function', 'Setup', {151 func: `msg.url = 'https://example.com'; return msg;`152 })153 .then('a06926', 'Core.Browser.Open', 'Open Browser', {154 outBrowserId: Message('browser_id')155 })156 .then('8e1c4b', 'Core.Browser.OpenLink', 'Navigate', {157 inBrowserId: Message('browser_id'),158 inUrl: Message('url'),159 outPageId: Message('page_id')160 })161 .then('d52f73', 'Core.Browser.Close', 'Close', {162 inBrowserId: Message('browser_id')163 })164 .then('b9a841', 'Core.Flow.Stop', 'Stop', {});165}).start();166```167168## CLI & MCP169170- `robomotion` — self-sufficient CLI. Builds, validates, runs, searches, inspects. `robomotion help` for the full verb list.171- `robomotion-browser-mcp` — MCP server for interactive browser exploration (used by `exploring-browser` and `mcp__browser__*` tools).172173The `robomotion` CLI shells out to `robomotion-sdk-mcp` internally for search-backed commands and calls `api.robomotion.io` directly for run/stop/vault/robot operations. No additional MCP servers required.174175## Regression suite176177This skill ships with an automated eval suite at `./evals/` — Tier A pinpoint regressions (handcrafted fixtures, one rule each) plus Tier B integration tests (live `main.ts` from the public `robomotion-templates` repo). Run `bun run skills/creating-flow/evals/run-evals.ts` from the agent-skills root before committing edits to this SKILL.md or the `./docs/` files. See `./evals/README.md` for adding new cases and the assertion grammar.178179## Related skills180181- `validating-flow` — schema validation182- `testing-flow` — behavioral tests183- `running-flow` — execute on robot184- `searching-packages` — find packages, nodes, templates185- `exploring-browser` — interactive browser automation186- `reversing-network` — convert a browser flow to HTTP after capturing traffic