SWIRLS_
Workflows

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:

JobTypes
Thinkagent, ai
Computecode
Reach outhttp, email, scrape, bucket
Branch and repeatswitch, map, while, parallel, wait
Rememberstream, disk, postgres
Composeworkflow
Ask a personreview (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:

FieldDescription
labelDisplay name. Defaults to the node name.
descriptionLonger description.
secretsObject literal mapping secret block names to var arrays: { block: [VAR1] }.
reviewHuman-in-the-loop review config. See Reviews.
failurePolicyRetry, skip, or fallback on failure. See Failure policies.
formatOutput format hint for the Portal: markdown, html, text, image, video, audio, mixed, or json.

code

Run TypeScript in an isolated sandbox.

FieldTypeRequiredDescription
code@ts { } blockYesTypeScript to execute. Access context.nodes for upstream data. Return value becomes output.
schema@json { } blockNoOutput 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.

FieldTypeRequiredDescription
kindstringYesOne of: text, object, image, video, embed.
providerstringNoWhich API to call: openrouter (default), anthropic, openai, or google.
modelstringNoModel identifier (e.g. "anthropic/claude-3.5-sonnet", "openai/dall-e-3"). The platform picks a default when omitted.
prompt@ts { } blockNoTypeScript expression returning the prompt string.
temperaturenumberNoSampling temperature.
maxTokensnumberNoMax output tokens.
optionsobjectNoKind-specific options (e.g. { n: 1, size: "1024x1024" } for image).
schema@json { } blockNoOutput 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.

FieldTypeRequiredDescription
casesstring arrayYesList of case names.
router@ts { } blockYesTypeScript 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).

FieldTypeRequiredDescription
url@ts { } block or stringYesRequest URL.
methodstringNoHTTP method. Defaults to GET.
authidentifierNoName of a top-level auth block. Only http nodes may set auth:.
headersobjectNoRequest headers. Avoid keys with hyphens.
body@ts { } blockNoRequest body.
schema@json { } blockNoOutput 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" } } }
  }
}

email

Send email via Resend with dynamic content.

FieldTypeRequiredDescription
from@ts { } block or stringYesSender address.
to@ts { } block or stringYesRecipient address.
subject@ts { } block or stringYesSubject line.
text@ts { } block or stringNoPlain text body.
html@ts { } block or stringNoHTML body.
replyTo@ts { } block or stringNoReply-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.

FieldTypeRequiredDescription
streamidentifierYesName of a top-level stream block.
versionv1, v2, …YesWhich versions: entry to read from that stream.
filter@ts { } blockYes*Filter object for rows. Operators: eq, ne, gt, gte, lt, lte, like, in. Return {} for no filter.
schema@json { } blockRecommendedTypically 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.

FieldTypeRequiredDescription
postgresstringYesName of a top-level postgres block.
select@sql { } blockYes*Read-only SELECT (or WITH CTE) query. Mutually exclusive with insert.
insert@sql { } blockYes*INSERT statement. Mutually exclusive with select.
params@ts { } blockYes for insert; optional for select if SQL has {{key}} placeholdersReturns an object whose keys match {{key}} tokens in the SQL.
condition@ts { } blockNoInsert only: if present, insert runs only when this returns true.
schema@json { } blockRecommended for selectJSON 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.

FieldTypeRequiredDescription
workflowidentifierYesName of the workflow to execute (bare identifier, not a quoted string).
input@ts { } blockYesTypeScript 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).

FieldTypeRequiredDescription
items@ts { } blockYesReturns an array to iterate.
maxItemsnumberYesMaximum items to process.
concurrencynumberNoReserved for engine chunking.
subgraphblockOne of subgraph or workflowInline child workflow with root, optional nodes, and flow.
workflowidentifierOne of subgraph or workflowReference 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: ... }.

FieldTypeRequiredDescription
input@ts { } blockYesInitial state for the loop.
condition@ts { } blockYesIf true, another iteration runs (after update).
update@ts { } blockYesProduces the next input for the child workflow.
maxIterationsnumberYesHard cap on iterations.
subgraphblockOne of subgraph or workflowInline child workflow.
workflowidentifierOne of subgraph or workflowNamed 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.

FieldTypeRequiredDescription
url@ts { } blockYesURL to scrape.
onlyMainContentbooleanNoStrip navigation and boilerplate when true.
formatsarray of stringsNoResponse formats (e.g. markdown).
maxAgenumberNoCache max age hint.
parsersarray of stringsNoParser 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.

FieldTypeRequiredDescription
operationsearch | extract | findallYesAPI mode.
objective@ts { } blockYesReturn a string describing the goal.
searchQueries@ts { } blockSearchReturn string[] of keyword queries.
urls@ts { } blockExtractReturn string[] of URLs.
modestringNoSearch: one-shot, agentic, or fast (default fast).
excerptsMaxCharsPerResultnumberNoSearch: excerpt size hint per result.
excerptsMaxCharsTotalnumberNoSearch: total excerpt budget.
excerptsbooleanNoExtract: include excerpts.
fullContentbooleanNoExtract: include full content.
entityType@ts { } blockFindAllEntity type label.
matchConditions@ts { } blockFindAllMatch rules array.
matchLimitnumberNoFindAll: max matches.
pollIntervalnumberNoFindAll: polling interval.
pollTimeoutnumberNoFindAll: max wait before timeout.

See Parallel for full FindAll fields.

wait

Pause execution for a duration.

FieldTypeRequiredDescription
amountnumberNoNumeric delay amount.
unitstringNoOne of: seconds, minutes, hours, days. Use with amount.

bucket

Upload or download files from project storage.

FieldTypeRequiredDescription
operationstringYesOne of: upload, download.
path@ts { } block or stringNoObject path within the bucket.
schema@json { } blockNoOutput schema.

disk

Execute a shell command on a remote Archil-backed disk. Declare the disk resource in a top-level disk block first.

FieldTypeRequiredDescription
diskidentifierYesName of a top-level disk block.
command@ts { } blockYesReturns 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.

FieldTypeRequiredDescription
agentidentifierYesName of a top-level agent block.
prompt@ts { } blockYesReturns the user message string.
profilestringNoName of a profile declared in the agent block.
system@ts { } blockNoOverrides the system prompt for this invocation.
toolsidentifier arrayNoNarrows the effective tool set. Must be a subset of the profile's or agent's tools.
schema@json { } blockNoStructured output schema. Use schema, not outputSchema.

On this page