Build
Swarm Canvas
A swarm is a directed graph of nodes. Output flows along the edges, shared state flows through all of them. This page documents all eighteen node kinds and every field on each.
Open Build → Agent Swarms. Drag a node from the palette, click it to open the inspector on the right, and configure it there. Connect nodes by dragging from one handle to another.
Flow state and templating
Every node can read from and write to a shared key/value flow state. This is what makes a graph more than a chain.
| Field | Present on | Meaning |
|---|---|---|
outputVar | Every node | Name the variable this node writes its result to. Without it, the result is only available to the immediately next node. |
inputs | Every node | Names of variables this node reads from state. A merge node uses this to decide what to combine. |
Anywhere a field accepts a template, these forms resolve:
| Template | Resolves to |
|---|---|
{{input}} | The value arriving on the incoming edge |
{{myVar}} | A named variable from flow state |
{{myVar.path.to.field}} | A nested field of a JSON value in state |
{{secret:NAME}} | A stored secret. Resolved server-side only — the value never reaches the browser. |
Why it works this way
{{secret:NAME}} is deliberately left untouched by the client-side interpolator and substituted on the server at call time. That is what lets an HTTP node authenticate to a third-party API without the credential ever being sent to, or visible in, the canvas.All eighteen node kinds
input
The entry point. Optionally renders a typed form in the Run panel instead of one free-text box — each field's value is seeded into flow state under its own name.
| Field | Values | Notes |
|---|---|---|
inputFields[].name | string | The variable name it writes to state |
inputFields[].label | string | Shown above the field |
inputFields[].type | text / textarea / number / select | select also needs options |
inputFields[].options | string[] | For select |
inputFields[].placeholder | string | — |
inputFields[].required | boolean | Blocks the run until filled |
agent
The workhorse: one LLM call, with optional tools, knowledge and memory.
| Field | Values | Notes |
|---|---|---|
agentId | saved agent | Link a saved agent to inherit its whole configuration. The fields below then override it. |
systemPrompt | template | Supports {{var}} templating |
provider | provider id | — |
model | model id | Per-node — a router can use a cheap model while a writer uses a strong one |
temperature | 0 – 2 | Same guidance as the agent builder: 0 for anything mechanical |
knowledgeBaseId | KB id | Grounds this node's call |
reranker | {provider, model} | Optional retrieval re-ranker |
enabledTools | tool ids | When set, ONLY these tools are exposed to this node |
skillIds | skill ids | Prepends a "Skills available to you" block |
toolConfigs | object | Per-tool config: web provider+key, n8n ids, MCP server names, SQL table allow-list |
guardrails | partial | Merged OVER the linked agent's — a node can be stricter, never looser |
memory | object | See memory scope below |
Node memory scope
| ltm_scope | Behaviour |
|---|---|
agent | Default. Shares long-term memory with the agent's normal sessions. |
swarm | Isolated to this swarm run — the run id is the conversation key. |
none | Long-term memory disabled for this node. |
condition
Two-way branch. The LLM answers a yes/no question and the matching edge is taken.
| Field | Notes |
|---|---|
conditionPrompt | A question whose YES/NO answer chooses the edge. Write it so the answer is unambiguous. |
Does the customer's message describe a fault with a physical product
(as opposed to a billing, delivery or account question)?router
N-way branch. The LLM picks one of the outgoing edge labels; the matching branch runs and the others are marked skipped.
| Field | Notes |
|---|---|
routerPrompt | Instruction for choosing. Name the categories exactly as you labelled the edges. |
Label your edges
billing, technical, other.loop
| Field | Default | Notes |
|---|---|---|
maxIters | — | Hard ceiling on iterations. ALWAYS set this; an unbounded loop is the classic runaway cost. |
foreach
Maps this node's agent body over each element of an array.
| Field | Notes |
|---|---|
foreachInput | Variable holding the array |
foreachItemVar | Name each element is exposed under inside the body |
approval
Pauses the run until a human decides.
| Field | Values | Notes |
|---|---|---|
approvalTitle | string | What the approver sees |
approvalRisk | low / medium / high | Shown to the approver as context |
approvalTimeoutMs | number | 0 or unset = wait indefinitely |
approverUserIds | user ids | Who is notified and may decide |
approverGroupIds | IAM group ids | Same, by group |
Note
evaluate
LLM-as-a-judge scoring. Five metrics ship enabled, with these default weights:
| Metric | Weight | Checks |
|---|---|---|
| Faithfulness | 0.30 | Are all claims grounded in the provided context? Catches hallucinations. |
| Answer Relevancy | 0.25 | Does the answer address the question, or is it tangential? |
| Completeness | 0.20 | Does it cover all parts of the question? |
| Coherence | 0.15 | Is it logically structured and clear? |
| Harmlessness | 0.10 | Free of harmful, biased or toxic content? |
| Field | Notes |
|---|---|
evalMetrics | Enable/disable each and set its weight (0–1) |
evalRubric | Free-form rubric the judge must follow |
evalCustomInstructions | Extra instructions for the judge |
evalPassThreshold | 0–1. The weighted overall score must meet this to pass |
evalReferenceInput | Variable holding the original question/context to judge against |
function
Sandboxed JavaScript. Receives ctx = { input, vars } and must return a value.
// Normalise a messy list into the shape the next node expects.
const rows = ctx.input.items ?? [];
return rows
.filter((r) => r.status === "open")
.map((r) => ({ id: r.id, owner: r.assignee?.name ?? "unassigned" }));| Field | Default | Notes |
|---|---|---|
functionTimeoutMs | 2000 | Hard timeout. The code runs in an isolated Worker with no network or DOM access. |
set_var
Writes named keys into flow state. Each value is a template resolved against current state.
| Field | Notes |
|---|---|
stateAssignments | Array of { key, value } — value supports {{var}} and {{var.path}} |
http
A deterministic outbound request — no LLM involved.
| Field | Values | Notes |
|---|---|---|
httpMethod | GET / POST / PUT / PATCH / DELETE | — |
httpUrl | template | Supports {{var}} and {{secret:NAME}} |
httpHeaders | {key, value}[] | Same templating — put tokens in {{secret:…}} |
httpBody | template | — |
httpResponsePath | JSON path | Extract one field from the response instead of passing the whole body on |
httpTimeoutMs | number | — |
Method: POST
URL: https://api.example.com/v1/tickets
Headers: Authorization: Bearer {{secret:SUPPORT_API_TOKEN}}
Content-Type: application/json
Body: {"subject": "{{summary}}", "body": "{{input}}", "priority": "{{priority}}"}
Path: data.idCareful
tool
Runs one built-in tool deterministically, with arguments you supply — no LLM decides.
| Field | Notes |
|---|---|
toolId | One of the ten swarm tool ids (kb_search, sql_query, web_search, …) |
toolArgs | Record of argument name → template, resolved against flow state |
Why it works this way
tool node instead of an agent node whenever the call is not a judgement call. If you always want the same query run, having a model decide to run it is pure cost and a source of variance.extract
LLM structured output. Produces a JSON object matching a schema you declare.
| Field | Notes |
|---|---|
extractSchema | Array of { name, type, description }. type is string | number | boolean | array. |
name: customer_name type: string "Full name as written"
name: order_id type: string "Order reference, e.g. NW-10482"
name: refund_amount type: number "Amount in GBP, 0 if none requested"
name: is_urgent type: boolean "True if they mention a deadline"merge
Combines this node's declared inputs into one value.
| mergeMode | Result |
|---|---|
concat | Joined text, separated by mergeSeparator (default two newlines) |
array | A JSON array of the input values |
object | A JSON object keyed by variable name |
first | The first non-empty input — useful after a router where only one branch ran |
retrieve
Standalone knowledge-base retrieval with no LLM call.
| Field | Default | Notes |
|---|---|---|
knowledgeBaseId | — | Which collection to search |
retrieveQuery | {{input}} | Template for the search query |
retrieveTopK | — | How many chunks to return |
subswarm
Runs another saved swarm as a single node; its final output becomes this node's output. It executes in isolation with the input you gather for it.
| Field | Notes |
|---|---|
subSwarmId | The saved swarm to run |
a2a_remote
Delegates to a remote agent server speaking the A2A protocol.
| Field | Notes |
|---|---|
a2aEndpoint | Remote server URL |
a2aAgentCard | The fetched agent card describing its skills |
a2aSkillId | Which advertised skill to invoke |
a2aAuthHeader | Auth header value — use {{secret:NAME}} |
a2aStreaming | Stream the remote response |
output
Terminal node. Its value is the swarm's result — what an API run returns and what a callback delivers.
Error handling
These apply to agent, http, tool, foreach, extract, evaluate, a2a_remote, function and loop:
| Field | Default | Effect |
|---|---|---|
retryCount | 0 | Retries on transient failure |
retryDelayMs | — | Wait between retries |
onError | fail | fail aborts the run. continue writes errorFallback to the output variable and carries on. |
errorFallback | — | The value used when onError is continue |
nodeTimeoutMs | 0 = default | Per-node call timeout override |
Note
onError: continue with a fallback on enrichment steps — a failed lookup shouldn't kill a run that can still produce something useful. Keep fail on anything whose result the rest of the graph depends on.Worked example — a support triage swarm
input ──▶ extract ──▶ router ─┬─[billing]──▶ agent(billing) ──┐
(customer, │ │
order_id, ├─[technical]▶ agent(tech KB) ────┤
refund_amount) │ │
└─[refund]───▶ approval ──▶ http ─┤
▼
merge ──▶ output
(mode: first)- 1
input
One field:message, typetextarea, required. - 2
extract
Schema as shown above.outputVar=ticket. Now{{ticket.refund_amount}}is available downstream. - 3
router
Three outgoing edges labelledbilling,technical,refund. routerPrompt: "Classify this message as billing, technical or refund." Model: a small fast one — this is classification, so temperature 0. - 4
agent nodes
Each links a saved agent and sets its ownknowledgeBaseId. All write tooutputVar=reply. - 5
approval
Title "Approve refund", riskhigh, approverGroupIds = your finance group. No timeout, so it waits. - 6
http
POST to your refunds API withAuthorization: Bearer {{secret:REFUND_API_TOKEN}}. onErrorfail— a silently failed refund is worse than a stopped run. - 7
merge → output
mergeModefirst, since exactly one branch ran.
Running and observing
- Run panel — fills the input form, streams node status live (idle, running, done, error, waiting, skipped) and shows each node's last output.
- Recent runs — history with inputs and results.
- Traces — per-node steps with prompts, tool calls, tokens and cost. See Logs & traces.
- Versions — the graph is snapshotted on save; diff and restore.
Deploying
Deploy gives a swarm an API key so your own systems can run it, and Chat exposes it as a conversational surface. A schedule can run it unattended. Full detail in API & webhooks and Web embedding.
The canvas edits a draft; deployed runs execute the published snapshot. Creating the first key or schedule publishes the current graph, and after that your saves stay private until you press Publish — so you can rewrite a prompt at 3am without changing what a live integration receives. The Deploy dialog shows Draft ahead whenever the canvas has moved on, and API & webhooks covers the states.
Custom components
A Function node holds a snippet used once. Components are the reusable form: author a snippet with a declared parameter schema in the palette’s My components → Manage, and it appears in the palette of every swarm you build.
| Piece | What it is |
|---|---|
| Parameters | Declared per component (text, number, boolean, select — with labels, defaults and required flags). Each becomes a field on the node and arrives in the code as ctx.params, typed: a number parameter is a number, not "5". |
| Code | Runs in the same sandboxed Worker as a Function node — no DOM, no network, no storage, hard timeout. ctx.input is the upstream value, ctx.vars the flow state. |
| Test harness | Runs your snippet with the same sandbox and the same parameter coercion the canvas uses, so a passing test means a passing node. |
| Versions | Saving bumps the component's version. Nodes carry a SNAPSHOT of the code they were built with, so editing the library never silently changes a swarm that already works — and a deleted component leaves working swarms working. |
Note
ctx.input, ctx.vars and ctx.params either way.Careful
docker compose --profile sandbox up -d --build. Until an operator starts it, deployed and scheduled runs refuse custom code rather than executing it beside the server’s credentials — and the Deploy dialog tells you so, for this instance specifically, before you deploy. Custom code never runs in the application process either way.File inputs
A start-form field of type file lets whoever runs the swarm attach a PDF, DOCX or text document. The file is converted to text in the browser and that text is seeded into flow state under the field’s name — so downstream nodes read it like any other variable, and no document is ever uploaded to the server.
| Limit | Value | What happens at the edge |
|---|---|---|
| File size | 10 MB | Larger files are refused before parsing |
| Extracted text | 200,000 characters | Longer documents are truncated, with a visible notice in the field and in the text itself — never silently |
| Scanned PDFs | — | A PDF with no text layer yields nothing and is refused with a message pointing at OCR |
Batch evaluations
A swarm that works on the prompt you tried it with can still regress on the other forty. Evaluations (under Experiment in the sidebar) runs a whole dataset of test cases through a swarm headlessly and scores every output, so a prompt tweak is measured rather than guessed at.
| Piece | What it is |
|---|---|
| Dataset | Named collection of cases. A case is an input, optional typed start-form values, and an optional expected answer. Import a CSV — columns beyond name/input/expected become start-form values. |
| Evaluator | How every output in the run is scored: an LLM judge (weighted metrics, 0–1, with a pass threshold), or a deterministic check — contains, exactly equals, or a regex. |
| Run | One dataset × one swarm × one evaluator. Cases execute on the same headless engine as a deployed API run, two at a time; progress, pass rate, average score and model spend are recorded. |
| Comparison | Pick an earlier run on the same dataset with the same evaluator and every case is paired: improved, regressed or unchanged, with the score delta. |
Note
Runs are resumable and cancellable: cancelling is enforced server-side, and a case that already has a verdict is never scored twice, so “run remaining” picks up exactly where it stopped. Approval nodes are auto-rejected by default — leave that on unless the swarm is safe to auto-approve in a batch. Each result links to its full execution trace.
Export
| Target | Fidelity |
|---|---|
| LangGraph | Full topology — branches, loops and conditional edges survive |
| Strands | GraphBuilder with the node graph |
| OpenAI Agents SDK | Agents plus post-assigned handoffs |
| CrewAI | Sequential process |
Note
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Router always picks the same branch | Unlabelled or similarly-labelled edges | Give every outgoing edge a short distinct label; set temperature 0 on the router. |
| A branch's output is empty at merge | Only one branch ran | Use mergeMode first. |
| Run never finishes | Loop without maxIters, or an approval with no approver | Set maxIters; check approverUserIds/approverGroupIds. |
| Variable is empty downstream | outputVar not set on the producing node | Set outputVar, and list it in the consumer's inputs. |
| HTTP node 401s | Secret not resolving | Use {{secret:NAME}} exactly; a raw token in the header is sent as literal text. |
| Function node times out | Loop or heavy work in sandbox | Raise functionTimeoutMs, or move the work to an http node. |
| Node ignores its guardrails | Node guardrails only tighten | They merge OVER the linked agent's; they cannot loosen what the agent enforces. |