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.

FieldPresent onMeaning
outputVarEvery nodeName the variable this node writes its result to. Without it, the result is only available to the immediately next node.
inputsEvery nodeNames 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:

TemplateResolves 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.

FieldValuesNotes
inputFields[].namestringThe variable name it writes to state
inputFields[].labelstringShown above the field
inputFields[].typetext / textarea / number / selectselect also needs options
inputFields[].optionsstring[]For select
inputFields[].placeholderstring
inputFields[].requiredbooleanBlocks the run until filled

agent

The workhorse: one LLM call, with optional tools, knowledge and memory.

FieldValuesNotes
agentIdsaved agentLink a saved agent to inherit its whole configuration. The fields below then override it.
systemPrompttemplateSupports {{var}} templating
providerprovider id
modelmodel idPer-node — a router can use a cheap model while a writer uses a strong one
temperature0 – 2Same guidance as the agent builder: 0 for anything mechanical
knowledgeBaseIdKB idGrounds this node's call
reranker{provider, model}Optional retrieval re-ranker
enabledToolstool idsWhen set, ONLY these tools are exposed to this node
skillIdsskill idsPrepends a "Skills available to you" block
toolConfigsobjectPer-tool config: web provider+key, n8n ids, MCP server names, SQL table allow-list
guardrailspartialMerged OVER the linked agent's — a node can be stricter, never looser
memoryobjectSee memory scope below

Node memory scope

ltm_scopeBehaviour
agentDefault. Shares long-term memory with the agent's normal sessions.
swarmIsolated to this swarm run — the run id is the conversation key.
noneLong-term memory disabled for this node.

condition

Two-way branch. The LLM answers a yes/no question and the matching edge is taken.

FieldNotes
conditionPromptA question whose YES/NO answer chooses the edge. Write it so the answer is unambiguous.
conditionPrompt
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.

FieldNotes
routerPromptInstruction for choosing. Name the categories exactly as you labelled the edges.

Label your edges

A router chooses among edge labels. Unlabelled edges give it nothing to pick, and it will route arbitrarily. Label every outgoing edge with a short distinct word: billing, technical, other.

loop

FieldDefaultNotes
maxItersHard 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.

FieldNotes
foreachInputVariable holding the array
foreachItemVarName each element is exposed under inside the body

approval

Pauses the run until a human decides.

FieldValuesNotes
approvalTitlestringWhat the approver sees
approvalRisklow / medium / highShown to the approver as context
approvalTimeoutMsnumber0 or unset = wait indefinitely
approverUserIdsuser idsWho is notified and may decide
approverGroupIdsIAM group idsSame, by group

Note

With both approver lists empty, only the person who started the run can decide it. The runner is emailed only if they explicitly appear in the lists — picked individually, or via a group they belong to.

evaluate

LLM-as-a-judge scoring. Five metrics ship enabled, with these default weights:

MetricWeightChecks
Faithfulness0.30Are all claims grounded in the provided context? Catches hallucinations.
Answer Relevancy0.25Does the answer address the question, or is it tangential?
Completeness0.20Does it cover all parts of the question?
Coherence0.15Is it logically structured and clear?
Harmlessness0.10Free of harmful, biased or toxic content?
FieldNotes
evalMetricsEnable/disable each and set its weight (0–1)
evalRubricFree-form rubric the judge must follow
evalCustomInstructionsExtra instructions for the judge
evalPassThreshold0–1. The weighted overall score must meet this to pass
evalReferenceInputVariable holding the original question/context to judge against

function

Sandboxed JavaScript. Receives ctx = { input, vars } and must return a value.

functionCode
// 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" }));
FieldDefaultNotes
functionTimeoutMs2000Hard 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.

FieldNotes
stateAssignmentsArray of { key, value } — value supports {{var}} and {{var.path}}

http

A deterministic outbound request — no LLM involved.

FieldValuesNotes
httpMethodGET / POST / PUT / PATCH / DELETE
httpUrltemplateSupports {{var}} and {{secret:NAME}}
httpHeaders{key, value}[]Same templating — put tokens in {{secret:…}}
httpBodytemplate
httpResponsePathJSON pathExtract one field from the response instead of passing the whole body on
httpTimeoutMsnumber
http node — create a ticket
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.id

Careful

Requests to private, loopback and link-local addresses — including cloud metadata endpoints — are refused, so a templated URL cannot be steered into your internal network.

tool

Runs one built-in tool deterministically, with arguments you supply — no LLM decides.

FieldNotes
toolIdOne of the ten swarm tool ids (kb_search, sql_query, web_search, …)
toolArgsRecord of argument name → template, resolved against flow state

Why it works this way

Use a 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.

FieldNotes
extractSchemaArray of { name, type, description }. type is string | number | boolean | array.
extractSchema
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.

mergeModeResult
concatJoined text, separated by mergeSeparator (default two newlines)
arrayA JSON array of the input values
objectA JSON object keyed by variable name
firstThe first non-empty input — useful after a router where only one branch ran

retrieve

Standalone knowledge-base retrieval with no LLM call.

FieldDefaultNotes
knowledgeBaseIdWhich collection to search
retrieveQuery{{input}}Template for the search query
retrieveTopKHow 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.

FieldNotes
subSwarmIdThe saved swarm to run

a2a_remote

Delegates to a remote agent server speaking the A2A protocol.

FieldNotes
a2aEndpointRemote server URL
a2aAgentCardThe fetched agent card describing its skills
a2aSkillIdWhich advertised skill to invoke
a2aAuthHeaderAuth header value — use {{secret:NAME}}
a2aStreamingStream 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:

FieldDefaultEffect
retryCount0Retries on transient failure
retryDelayMsWait between retries
onErrorfailfail aborts the run. continue writes errorFallback to the output variable and carries on.
errorFallbackThe value used when onError is continue
nodeTimeoutMs0 = defaultPer-node call timeout override

Note

Use 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)
Router splits by intent; refunds pass a human gate before the API call.
  1. 1

    input

    One field: message, type textarea, required.
  2. 2

    extract

    Schema as shown above. outputVar = ticket. Now {{ticket.refund_amount}} is available downstream.
  3. 3

    router

    Three outgoing edges labelled billing, technical, refund. routerPrompt: "Classify this message as billing, technical or refund." Model: a small fast one — this is classification, so temperature 0.
  4. 4

    agent nodes

    Each links a saved agent and sets its own knowledgeBaseId. All write to outputVar = reply.
  5. 5

    approval

    Title "Approve refund", risk high, approverGroupIds = your finance group. No timeout, so it waits.
  6. 6

    http

    POST to your refunds API with Authorization: Bearer {{secret:REFUND_API_TOKEN}}. onError fail — a silently failed refund is worse than a stopped run.
  7. 7

    merge → output

    mergeMode first, 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.

PieceWhat it is
ParametersDeclared 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".
CodeRuns 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 harnessRuns your snippet with the same sandbox and the same parameter coercion the canvas uses, so a passing test means a passing node.
VersionsSaving 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

Where custom code runs. On the canvas it runs in your browser, in a Worker with the dangerous globals removed. In deployed (API-key) and scheduledruns there is no browser, so it runs in the JS sandbox — a separate container with no secrets, no filesystem and no route to the internet, giving each call a fresh JavaScript realm that is destroyed afterwards. Your snippet sees exactly the same ctx.input, ctx.vars and ctx.params either way.

Careful

The sandbox is an opt-in service: 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.

LimitValueWhat happens at the edge
File size10 MBLarger files are refused before parsing
Extracted text200,000 charactersLonger documents are truncated, with a visible notice in the field and in the text itself — never silently
Scanned PDFsA 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.

PieceWhat it is
DatasetNamed 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.
EvaluatorHow 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.
RunOne 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.
ComparisonPick 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

The judge’s verdict is recomputed from its per-metric scores and your weights — the model’s own “pass” claim is never trusted. A scorecard that skips a metric is rejected rather than counted as zero, so a lazy judge fails loudly instead of quietly failing your swarm.

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

TargetFidelity
LangGraphFull topology — branches, loops and conditional edges survive
StrandsGraphBuilder with the node graph
OpenAI Agents SDKAgents plus post-assigned handoffs
CrewAISequential process

Note

Frameworks that cannot express a node kind get the graph re-wired around it: agent-to-agent edges are bridged through the dropped node so the topology still connects, rather than emitting a broken graph.

Troubleshooting

SymptomCauseFix
Router always picks the same branchUnlabelled or similarly-labelled edgesGive every outgoing edge a short distinct label; set temperature 0 on the router.
A branch's output is empty at mergeOnly one branch ranUse mergeMode first.
Run never finishesLoop without maxIters, or an approval with no approverSet maxIters; check approverUserIds/approverGroupIds.
Variable is empty downstreamoutputVar not set on the producing nodeSet outputVar, and list it in the consumer's inputs.
HTTP node 401sSecret not resolvingUse {{secret:NAME}} exactly; a raw token in the header is sent as literal text.
Function node times outLoop or heavy work in sandboxRaise functionTimeoutMs, or move the work to an http node.
Node ignores its guardrailsNode guardrails only tightenThey merge OVER the linked agent's; they cannot loosen what the agent enforces.