SWIRLS_
Language

Node Types

Configuration reference for every node type available in .swirls files.

Nodes are the building blocks of a graph. Each node has a type field that determines its behavior. This page documents all available node types, their fields, and examples.

For how nodes connect to each other, see Graphs. For type-safe access to node outputs, see Context.

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.
modelstringYesModel identifier (e.g. "anthropic/claude-3.5-sonnet", "openai/dall-e-3").
prompt@ts { } blockYesTypeScript 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. See Streams for stream { graph:, condition:, prepare: } and context.output in switch graphs.

FieldTypeRequiredDescription
streamidentifierYesName of a top-level stream block.
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.

graph

Execute another graph as a subgraph.

FieldTypeRequiredDescription
graphstringYesName of the graph to execute.
input@ts { } blockYesTypeScript expression mapping input for the subgraph.
node run_enrichment {
  type: graph
  label: "Run enrichment"
  graph: enrich_contact
  input: @ts {
    return { email: context.nodes.root.input.email, name: context.nodes.root.input.name }
  }
}

Subgraph output is available as context.nodes.<graphNodeName>.output.<leafNodeName> where leafNodeName is a leaf node in the child graph.

map

Run a child graph once per element returned from items. Requires maxItems (positive cap). Use either inline subgraph { ... } (no colon after subgraph) or graph: named_graph: exactly one.

Inside the child graph 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 graph 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 graphInline child graph with root, optional nodes, and flow.
graphidentifierOne of subgraph or graphReference to a named graph 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 graph repeatedly until condition is false or maxIterations is reached. Requires input, condition, update, and maxIterations. Same subgraph { } vs graph: 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 graph.
maxIterationsnumberYesHard cap on iterations.
subgraphblockOne of subgraph or graphInline child graph.
graphidentifierOne of subgraph or graphNamed graph 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

S3-like storage operations.

FieldTypeRequiredDescription
operationstringYesOne of: upload, download, delete.
path@ts { } block or stringNoObject path.
bucketstringNoBucket name.
schema@json { } blockNoOutput schema.

On this page