Imported from robomotionio/agent-skills (
skills/creating-flow/SKILL.md). Install upstream withnpx skills add robomotionio/agent-skills --skill creating-flow. Copyright stays with the author.
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>.tsfilename, 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.versionmust be concrete ('latest'is rejected) and must exist in the package's publishedversionslist.namespacemust exist inhttps://packages.robomotion.io/stable/index.json. Runrobomotion get packages <ns>orrobomotion describe package <ns>to resolve real versions before callingaddDependency. Never invent a version.- Terminal nodes (
Debug,Log,Stop,GoTo,End,WaitGroup.Done) have 0 outputs — wire TO them viaf.edge(), never.then()from them. - Every
Core.Flow.GoToreferences aCore.Flow.Labelid 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. Everyin*/out*port takes a scope helper (Custom('…')/Message()); a bare literal there is silently dropped. Foropt*fields, don't guess from the name — checkget_node_schema: a field typedobject+variableType(even a numeric one) takes a scope helper; a plainnumber/boolean/enumfield takes a bare literal and must NOT be wrapped. The very same property can differ by node:Core.Browser.OpenLinkoptTimeoutis a plain number →optTimeout: 32, butCore.Browser.WaitElementoptTimeoutisvariableType: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 likeoptUrl/optDownloadDir/optNofBranchesalways takeCustom()/Message(). funcis a literal string (NOTJS()).- Common runtime props also take raw values:
delayBefore: 2,delayAfter: 0.5,continueOnError: true. - ES5-only inside
func: no=>, no template literals, noconst/let, no destructuring. Norequire()/fs/Buffer/process(pure JS sandbox). - Loops:
Label → ForEach → body → GoTo.Stopis standalone, wired viaf.edge()on ForEach port 1. - Library projects use
library.create(id, name, fn)withBegin/Endnodes (no.start()). Inline subflows usesubflow.create(name, fn). - Every flow ends with
.start(). Every flow has aCore.Flow.Stopnode. Core.*packages (Core.Trigger,Core.Browser,Core.Programming,Core.CSV,Core.Flow,Core.Vault,Core.Net,Core.Excel, …) are embedded in the robot — NEVER callf.addDependency('Core.*', …). The Designer auto-loads them. Only callf.addDependency(ns, ver)for non-Core.*packages. When updating an existing flow, NEVER bump existingaddDependencyversions; only add missing ones.- Comments & canvas layout —
Core.Flow.Commentnodes (with anoptTextmarkdown string) title the flow and fence its logical phases; the visual arrangement — nodepositions, comment box colors/sizes, and Sugiyama-style layering — lives inmain.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)ormcp__browser__*afterToolSearchwarmup). Don't guess selectors.Core.Browser.*element nodes (ClickElement/TypeText/GetValue/SetValue/WaitElement/Select) defaultinSelectorto XPath — translate CSS handles you find (#email,input[type="email"]) to XPath (//input[@id='email']) and omitinSelectorType; use a CSS string ONLY withinSelectorType: 'css'(plain literal —inSelectorTypeis an enum, so NEVERCustom('css')). A CSS string with the default engine fails at runtime with "element not found". Never writeinSelectorType: 'xpath'(invalid; the value isxpath:position). Also: enum/dropdown opts (optBrowser,optProxy,optProxyAuth,optClickType) take a PLAIN string/boolean — NEVERCustom(); wrapping an enum inCustom()emits a{name,scope}object and the robot rejects the node at load withConfig 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.mdBEFORE 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.WriteCSVwithinFilePath+inTable; write Sheets =Robomotion.GoogleSheets.SetRange; etc.) and the{columns: [...], rows: [{key: value}]}format (never{header: ...}, never rows-as-arrays). Do NOTunified_search/searchfor 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, useget_node_schemafor 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.ChatAssistantflow in conversational mode — read./docs/patterns/conversational-chat.mdBEFORE writing it. One user message is oneChatIn → ChatOutrun andChatOutis the only thing that unlocks the composer, so a branch that ends anywhere else (an error, a missingChatOut) 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 Instream_delta→Streaming Text, and noTextnode repeating the answer) and the attachments wiring (GetAttachments, becausemsg.payload.filesis names and versions, never files on disk). - Validate BEFORE save —
save_flowonly 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-templatesis 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), 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 withrobomotion describe node, thenWritemain.ts(and anysubflows/<id>.ts). For browser flows: explore live first. - Validate — call
validate_flowMCP tool. Pspec-checks AND dependency-checks. MUST run BEFORE save. - Save —
save_flowif registered (Designer / pi); elsegit commit && git pushfrom 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 therunning-flowskill.
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 helpfor the full verb list.robomotion-browser-mcp— MCP server for interactive browser exploration (used byexploring-browserandmcp__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 validationtesting-flow— behavioral testsrunning-flow— execute on robotsearching-packages— find packages, nodes, templatesexploring-browser— interactive browser automationreversing-network— convert a browser flow to HTTP after capturing traffic