Node Types
Configuration reference for all 16 node types available in .swirls files.
What it is. The steps inside a workflow: the verbs of the language. Each node has a type field that determines its behavior.
Sixteen types, seven jobs:
| Job | Types |
|---|---|
| Think | agent, ai |
| Compute | code |
| Reach out | http, email, scrape, bucket |
| Branch and repeat | switch, map, while, parallel, wait |
| Remember | stream, disk, postgres |
| Compose | workflow |
| Ask a person | review (a config on any node, see Reviews) |
This page documents all 16 node types, their required and optional fields, and examples. The complete list of valid type values: ai, agent, bucket, code, disk, email, workflow, http, map, parallel, postgres, scrape, stream, switch, wait, while.
For how nodes connect to each other, see Workflows. For type-safe access to node outputs, see Context. For per-node credit costs on Swirls Cloud, see Credits.
Shared optional fields
Every node accepts these fields in addition to its type-specific fields:
| Field | Description |
|---|---|
label | Display name. Defaults to the node name. |
description | Longer description. |
secrets | Object literal mapping secret block names to var arrays: { block: [VAR1] }. |
review | Human-in-the-loop review config. See Reviews. |
failurePolicy | Retry, skip, or fallback on failure. See Failure policies. |
format | Output format hint for the Portal: markdown, html, text, image, video, audio, mixed, or json. |
code
Run TypeScript in an isolated sandbox.
| Field | Type | Required | Description |
|---|---|---|---|
| code | @ts { } block | Yes | TypeScript to execute. Access context.nodes for upstream data. Return value becomes output. |
| schema | @json { } block | No | Output schema (JSON Schema). Use outputSchema on the root node only. |
node normalize {
type: code
label: "Normalize email"
schema: @json {
{ "type": "object", "required": ["email"], "properties": { "email": { "type": "string" } }, "additionalProperties": false }
}
code: @ts {
const email = context.nodes.root.input.email ?? ""
return { email: email.toLowerCase().trim() }
}
}ai
Invoke an AI model. Supports multiple kinds: text, object, image, video, and embed.
| Field | Type | Required | Description |
|---|---|---|---|
| kind | string | Yes | One of: text, object, image, video, embed. |
| provider | string | No | Which API to call: openrouter (default), anthropic, openai, or google. |
| model | string | No | Model identifier (e.g. "anthropic/claude-3.5-sonnet", "openai/dall-e-3"). The platform picks a default when omitted. |
| prompt | @ts { } block | No | TypeScript expression returning the prompt string. |
| temperature | number | No | Sampling temperature. |
| maxTokens | number | No | Max output tokens. |
| options | object | No | Kind-specific options (e.g. { n: 1, size: "1024x1024" } for image). |
| schema | @json { } block | No | Output schema. Required for the object kind. |
Example (object kind):
node classify {
type: ai
label: "Classify intent"
kind: object
model: "anthropic/claude-3.5-sonnet"
prompt: @ts {
return `Classify this message: ${context.nodes.root.input.message}`
}
schema: @json {
{
"type": "object",
"required": ["intent", "confidence"],
"properties": {
"intent": { "type": "string" },
"confidence": { "type": "number" }
}
}
}
}Example (image kind):
node generate_image {
type: ai
label: "Generate image"
kind: image
model: "openai/dall-e-3"
prompt: @ts {
return `A professional illustration of: ${context.nodes.root.input.topic}`
}
options: {
n: 1
size: "1024x1024"
}
}switch
Conditional routing. Returns a case name to determine which branch executes.
| Field | Type | Required | Description |
|---|---|---|---|
| cases | string array | Yes | List of case names. |
| router | @ts { } block | Yes | TypeScript expression returning one of the case names. |
Use labeled edges in the flow block: route -["case_name"]-> target.
node route {
type: switch
label: "Route by priority"
cases: ["high", "medium", "low"]
router: @ts {
const score = context.nodes.root.output.score ?? 0
if (score > 80) return "high"
if (score > 40) return "medium"
return "low"
}
}http
Make HTTP requests to external APIs. Use an auth block for OAuth, API key, basic, or bearer auth instead of building Authorization headers by hand: avoid hyphenated header keys like Content-Type in literal headers objects (they break the parser; see Syntax).
| Field | Type | Required | Description |
|---|---|---|---|
| url | @ts { } block or string | Yes | Request URL. |
| method | string | No | HTTP method. Defaults to GET. |
| auth | identifier | No | Name of a top-level auth block. Only http nodes may set auth:. |
| headers | object | No | Request headers. Avoid keys with hyphens. |
| body | @ts { } block | No | Request body. |
| schema | @json { } block | No | Output schema. |
auth api_key_ex {
type: api_key
secrets: api_k
key: API_KEY
header: "X-Api-Key"
}
secret api_k {
vars: [API_KEY]
}
node fetch_data {
type: http
label: "Fetch from API"
method: "POST"
auth: api_key_ex
url: @ts {
return "https://api.example.com/data/" + context.nodes.root.input.id
}
body: @ts {
return JSON.stringify({ query: context.nodes.root.input.query })
}
schema: @json {
{ "type": "object", "properties": { "results": { "type": "array" } } }
}
}Send email via Resend with dynamic content.
| Field | Type | Required | Description |
|---|---|---|---|
| from | @ts { } block or string | Yes | Sender address. |
| to | @ts { } block or string | Yes | Recipient address. |
| subject | @ts { } block or string | Yes | Subject line. |
| text | @ts { } block or string | No | Plain text body. |
| html | @ts { } block or string | No | HTML body. |
| replyTo | @ts { } block or string | No | Reply-to address. |
The output shape is vendor-managed. Setting schema: on an email node will be rejected by the validator.
node send_email {
type: email
label: "Send notification"
from: @ts { return "[email protected]" }
to: @ts { return context.nodes.root.output.email }
subject: @ts { return "Your request has been processed" }
text: @ts {
return `Hello ${context.nodes.root.output.name}, your request is complete.`
}
}stream
Read persisted rows from a top-level stream block in the same file, pinned to one version. See Streams for stream { workflow: , version:, versions: { vN { schema, condition?, prepare } } } and context.output in switch workflows.
| Field | Type | Required | Description |
|---|---|---|---|
| stream | identifier | Yes | Name of a top-level stream block. |
| version | v1, v2, … | Yes | Which versions: entry to read from that stream. |
| filter | @ts { } block | Yes* | Filter object for rows. Operators: eq, ne, gt, gte, lt, lte, like, in. Return {} for no filter. |
| schema | @json { } block | Recommended | Typically type: "array" of row objects. |
*Required in practice; use return {} when unconstrained.
postgres
Query or write to a user-managed PostgreSQL database declared in a top-level postgres block. Use select: for reads and insert: for writes. Each node references exactly one of select or insert.
| Field | Type | Required | Description |
|---|---|---|---|
| postgres | string | Yes | Name of a top-level postgres block. |
| select | @sql { } block | Yes* | Read-only SELECT (or WITH CTE) query. Mutually exclusive with insert. |
| insert | @sql { } block | Yes* | INSERT statement. Mutually exclusive with select. |
| params | @ts { } block | Yes for insert; optional for select if SQL has {{key}} placeholders | Returns an object whose keys match {{key}} tokens in the SQL. |
| condition | @ts { } block | No | Insert only: if present, insert runs only when this returns true. |
| schema | @json { } block | Recommended for select | JSON Schema for result rows (typically an array of objects). |
*Exactly one of select or insert is required.
Select example:
node load_rows {
type: postgres
label: "Load active rows"
postgres: my_db
select: @sql {
SELECT id, name FROM items WHERE status = {{status}} LIMIT 10
}
params: @ts {
return { status: context.nodes.root.input.status }
}
schema: @json {
{
"type": "array",
"items": {
"type": "object",
"properties": { "id": { "type": "string" }, "name": { "type": "string" } }
}
}
}
}Insert example:
node save_row {
type: postgres
label: "Insert row"
postgres: my_db
insert: @sql {
INSERT INTO items (name, score) VALUES ({{name}}, {{score}})
}
params: @ts {
return {
name: context.nodes.classify.output.name,
score: context.nodes.classify.output.score
}
}
}Declare the database and tables in a top-level postgres block: see Postgres.
workflow
Execute another workflow as a subgraph.
| Field | Type | Required | Description |
|---|---|---|---|
| workflow | identifier | Yes | Name of the workflow to execute (bare identifier, not a quoted string). |
| input | @ts { } block | Yes | TypeScript expression mapping input for the subgraph. |
node run_enrichment {
type: workflow
label: "Run enrichment"
workflow: enrich_contact
input: @ts {
return { email: context.nodes.root.input.email, name: context.nodes.root.input.name }
}
}Subgraph output is available as context.nodes.<workflowNodeName>.output.<leafNodeName> where leafNodeName is a leaf node in the child workflow.
map
Run a child workflow once per element returned from items. Requires maxItems (positive cap). Use either inline subgraph { ... } (no colon after subgraph) or workflow: named_workflow: exactly one.
Inside the child workflow and on the map node’s items block, context.iteration includes index, item (typed from the subgraph root inputSchema), total, and previous. Output is an array of child workflow results (one entry per item).
| Field | Type | Required | Description |
|---|---|---|---|
| items | @ts { } block | Yes | Returns an array to iterate. |
| maxItems | number | Yes | Maximum items to process. |
| concurrency | number | No | Reserved for engine chunking. |
| subgraph | block | One of subgraph or workflow | Inline child workflow with root, optional nodes, and flow. |
| workflow | identifier | One of subgraph or workflow | Reference to a named workflow in the same file. |
node per_ticket {
type: map
label: "Process each ticket"
items: @ts {
return context.nodes.root.output.tickets
}
maxItems: 100
subgraph {
root {
type: code
label: "Normalize"
inputSchema: @json {
{ "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } }, "additionalProperties": false }
}
outputSchema: @json {
{ "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } }, "additionalProperties": false }
}
code: @ts {
const item = context.iteration.item
return { id: item.id.trim() }
}
}
}
}while
Run a child workflow repeatedly until condition is false or maxIterations is reached. Requires input, condition, update, and maxIterations. Same subgraph { } vs workflow: choice as map.
context.iteration includes index, input (threaded state from root inputSchema), and previous (last iteration’s leaf outputs). Output shape is { iterations: number; lastOutput: ... }.
| Field | Type | Required | Description |
|---|---|---|---|
| input | @ts { } block | Yes | Initial state for the loop. |
| condition | @ts { } block | Yes | If true, another iteration runs (after update). |
| update | @ts { } block | Yes | Produces the next input for the child workflow. |
| maxIterations | number | Yes | Hard cap on iterations. |
| subgraph | block | One of subgraph or workflow | Inline child workflow. |
| workflow | identifier | One of subgraph or workflow | Named workflow in the same file. |
node refine_loop {
type: while
label: "Refine draft"
input: @ts {
return { draft: context.nodes.merge.output.text }
}
condition: @ts {
return context.iteration.index < 2
}
update: @ts {
return { draft: context.iteration.previous?.polish?.text ?? context.iteration.input.draft }
}
maxIterations: 5
subgraph {
root {
type: code
label: "Polish"
inputSchema: @json {
{ "type": "object", "required": ["draft"], "properties": { "draft": { "type": "string" } }, "additionalProperties": false }
}
outputSchema: @json {
{ "type": "object", "required": ["text"], "properties": { "text": { "type": "string" } }, "additionalProperties": false }
}
code: @ts {
return { text: context.iteration.input.draft + " (refined)" }
}
}
}
}Downstream code can read context.nodes.refine_loop.output.lastOutput.<leafName>.
scrape
Fetch and extract content from web pages via Firecrawl.
| Field | Type | Required | Description |
|---|---|---|---|
| url | @ts { } block | Yes | URL to scrape. |
| onlyMainContent | boolean | No | Strip navigation and boilerplate when true. |
| formats | array of strings | No | Response formats (e.g. markdown). |
| maxAge | number | No | Cache max age hint. |
| parsers | array of strings | No | Parser hints for the API. |
The output shape is vendor-managed. Setting schema: on a scrape node will be rejected by the validator.
parallel
Web search and URL extraction via Parallel.ai. See the Parallel guide.
| Field | Type | Required | Description |
|---|---|---|---|
| operation | search | extract | findall | Yes | API mode. |
| objective | @ts { } block | Yes | Return a string describing the goal. |
| searchQueries | @ts { } block | Search | Return string[] of keyword queries. |
| urls | @ts { } block | Extract | Return string[] of URLs. |
| mode | string | No | Search: one-shot, agentic, or fast (default fast). |
| excerptsMaxCharsPerResult | number | No | Search: excerpt size hint per result. |
| excerptsMaxCharsTotal | number | No | Search: total excerpt budget. |
| excerpts | boolean | No | Extract: include excerpts. |
| fullContent | boolean | No | Extract: include full content. |
| entityType | @ts { } block | FindAll | Entity type label. |
| matchConditions | @ts { } block | FindAll | Match rules array. |
| matchLimit | number | No | FindAll: max matches. |
| pollInterval | number | No | FindAll: polling interval. |
| pollTimeout | number | No | FindAll: max wait before timeout. |
See Parallel for full FindAll fields.
wait
Pause execution for a duration.
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | No | Numeric delay amount. |
| unit | string | No | One of: seconds, minutes, hours, days. Use with amount. |
bucket
Upload or download files from project storage.
| Field | Type | Required | Description |
|---|---|---|---|
| operation | string | Yes | One of: upload, download. |
| path | @ts { } block or string | No | Object path within the bucket. |
| schema | @json { } block | No | Output schema. |
disk
Execute a shell command on a remote Archil-backed disk. Declare the disk resource in a top-level disk block first.
| Field | Type | Required | Description |
|---|---|---|---|
| disk | identifier | Yes | Name of a top-level disk block. |
| command | @ts { } block | Yes | Returns the shell command string to run on the mounted disk. |
disk project_disk {
label: "Project disk"
id: "dsk-abc123"
secrets: disk_creds
}
secret disk_creds {
vars: [ARCHIL_API_KEY]
}
node list_files {
type: disk
label: "List files"
disk: project_disk
command: @ts {
return "ls /mnt/" + context.nodes.root.input.path
}
}agent
Invoke a top-level agent block. The agent runs a tool-call loop: it calls tool workflows and built-in workspace tools until it produces a final answer or reaches maxSteps.
For the full treatment (the agent block, profiles, sandbox configuration, structured output, tool workflow contract, and common mistakes), see Agents.
| Field | Type | Required | Description |
|---|---|---|---|
agent | identifier | Yes | Name of a top-level agent block. |
prompt | @ts { } block | Yes | Returns the user message string. |
profile | string | No | Name of a profile declared in the agent block. |
system | @ts { } block | No | Overrides the system prompt for this invocation. |
tools | identifier array | No | Narrows the effective tool set. Must be a subset of the profile's or agent's tools. |
schema | @json { } block | No | Structured output schema. Use schema, not outputSchema. |