SWIRLS_
Language

Common mistakes

The most common errors when writing .swirls files, with corrections and the exact validator messages each one produces.

Run swirls doctor to validate your .swirls files. This page explains what to fix when it reports errors or when graphs silently disappear from the output.

Wrong node type names

There are 15 node types. Each one has an exact name. The validator rejects anything else.

type: resend should be type: email

// Incorrect
node notify {
  type: resend
  from: @ts { return "[email protected]" }
  to: @ts { return context.nodes.root.output.email }
  subject: @ts { return "Done" }
}

Validator error: Invalid node type "resend". Must be one of: agent, ai, bucket, code, disk, email, graph, http, map, parallel, postgres, scrape, stream, switch, wait, while

// Correct
node notify {
  type: email
  label: "Send notification"
  from: @ts { return "[email protected]" }
  to: @ts { return context.nodes.root.output.email }
  subject: @ts { return "Done" }
}

type: firecrawl should be type: scrape

// Incorrect
node fetch_page {
  type: firecrawl
  url: @ts { return "https://example.com" }
}

Validator error: Invalid node type "firecrawl". Must be one of: ...

// Correct
node fetch_page {
  type: scrape
  label: "Fetch page"
  url: @ts { return "https://example.com" }
}

Other invalid aliases

WrongCorrect
type: api, type: request, type: fetchtype: http
type: delay, type: sleeptype: wait
type: llm, type: prompt, type: chattype: ai
type: subgraph, type: call, type: childtype: graph
type: db, type: database, type: sqltype: postgres
type: storage, type: file, type: s3type: bucket
type: mapUse type: map (valid) or type: code for simple transforms
type: loop, type: retrytype: while (for loops), failurePolicy (for retry)

TypeScript blocks

Code written outside a @ts block

// Incorrect
node process {
  type: code
  label: "Process"
  code: return { value: 1 }
}

All executable code must be inside a @ts { } block.

// Correct
node process {
  type: code
  label: "Process"
  code: @ts { return { value: 1 } }
}

Double-quote characters inside @ts blocks

Literal " characters inside @ts { } blocks confuse the parser's string boundary detection. The block appears to parse, but all subsequent graphs and resources in the file are silently dropped. swirls doctor reports success with fewer graphs than expected.

// Incorrect — causes silent drop
code: @ts {
  return '"' + value + '"'
}
// Correct — use String.fromCharCode(34)
code: @ts {
  const Q = String.fromCharCode(34)
  return Q + value + Q
}

Use String.fromCharCode(34) wherever you need a literal double-quote character in a @ts block.

Nested template literals inside @ts blocks

Template literals inside ${} interpolation break @ts parsing. The inner backtick is mistaken for the end of the outer template literal. Everything after the broken block may be silently dropped.

// Incorrect
prompt: @ts {
  return `Results:\n${data.map(r => `${r.name}: ${r.score}`).join("\n")}`
}
// Correct — use string concatenation for the inner expression
prompt: @ts {
  return "Results:\n" + data.map(r => r.name + ": " + r.score).join("\n")
}

$${} interpolation in @ts blocks

A literal $ immediately before a ${...} interpolation (for example, formatting currency as $${amount}) breaks @ts parsing.

// Incorrect
code: @ts {
  return `Total: $${amount.toFixed(2)}`
}
// Correct
code: @ts {
  return "Total: $" + amount.toFixed(2)
}

fetch, import, and require inside @ts blocks

Code nodes run in a sandboxed environment with no network access, no file system, and no module imports.

// Incorrect
node call_api {
  type: code
  label: "Call API"
  code: @ts {
    const res = await fetch("https://api.example.com/data")
    return await res.json()
  }
}

Use dedicated node types for external calls: http for API requests, ai for LLM calls, email for email, scrape for web scraping.

// Correct
node call_api {
  type: http
  label: "Call API"
  url: @ts { return "https://api.example.com/data" }
}

Schema placement

outputSchema on a non-root node

// Incorrect
node process {
  type: code
  label: "Process"
  code: @ts { return { value: 1 } }
  outputSchema: @json { { "type": "object" } }
}

Validator error: Use "schema" instead of "outputSchema" in node blocks

Non-root nodes use schema, not outputSchema. Root nodes use inputSchema and outputSchema.

// Correct
node process {
  type: code
  label: "Process"
  code: @ts { return { value: 1 } }
  schema: @json { { "type": "object" } }
}

inputSchema on a non-root node

// Incorrect
node enrich {
  type: code
  label: "Enrich"
  inputSchema: @json { { "type": "object" } }
  code: @ts { return {} }
}

Parser error: inputSchema is only allowed in root { } blocks

The parser drops the entire node silently. Downstream edges referencing enrich will then fail with Edge references non-existent source node "enrich".

inputSchema belongs only on the root { } block. Non-root nodes receive their input from upstream node outputs via context.nodes.

Edges and routing

Chaining edges on one line

// Incorrect
flow {
  root -> validate -> process -> notify
}

Parser error: Edge declarations must be inside a flow { } block (or the chain silently fails to parse)

Each edge must be its own line.

// Correct
flow {
  root -> validate
  validate -> process
  process -> notify
}

Conditional routing without a switch node

// Incorrect — conditional edges do not exist in the DSL
flow {
  root -> process
  process -> notify if process.output.shouldNotify
  process -> skip if !process.output.shouldNotify
}

Conditional routing requires a switch node with a router function and labeled edges.

// Correct
node route {
  type: switch
  label: "Route"
  cases: ["notify", "skip"]
  router: @ts {
    if (context.nodes.process.output.shouldNotify) return "notify"
    return "skip"
  }
}

flow {
  root -> process
  process -> route
  route -["notify"]-> notify
  route -["skip"]-> skip
}

Edges outside the flow block

// Incorrect
graph my_graph {
  label: "My Graph"
  root { type: code label: "Entry" code: @ts { return {} } }
  node step { type: code label: "Step" code: @ts { return {} } }
  root -> step
}

Parser error: Edge declarations must be inside a flow { } block

// Correct
graph my_graph {
  label: "My Graph"
  root { type: code label: "Entry" code: @ts { return {} } }
  node step { type: code label: "Step" code: @ts { return {} } }
  flow {
    root -> step
  }
}

Resources and triggers

Using an array for secrets:

// Incorrect
node call_api {
  type: http
  url: @ts { return "https://api.example.com" }
  secrets: [API_KEY]
}

Parser error: Expected { after secrets:

The secrets: field is an object literal mapping secret block names to arrays of var names.

// Correct
node call_api {
  type: http
  url: @ts { return "https://api.example.com" }
  secrets: {
    my_creds: [API_KEY]
  }
}

Using process.env instead of context.secrets

// Incorrect
headers: @ts { return { Authorization: "Bearer " + process.env.API_KEY } }

Code nodes run in a sandbox. process.env is not available. Declare secrets with a secret block and bind them on the node.

// Correct
secret my_creds {
  vars: [API_KEY]
}

node call_api {
  type: http
  url: @ts { return "https://api.example.com" }
  headers: @ts { return { Authorization: "Bearer " + context.secrets.my_creds.API_KEY } }
  secrets: {
    my_creds: [API_KEY]
  }
}

Multiple bindings in one trigger block

// Incorrect
trigger on_forms {
  form:contact -> process_contact
  form:support -> process_support
}

Each trigger block accepts exactly one binding line.

// Correct
trigger on_contact {
  form:contact -> process_contact
}

trigger on_support {
  form:support -> process_support
}

Inventing trigger types

// Incorrect
trigger on_event {
  agent:my_agent -> my_graph
}

Only three resource types are valid in trigger bindings: form, webhook, and schedule.

// Correct
trigger on_event {
  webhook:my_webhook -> my_graph
}

Hyphenated resource names

// Incorrect
form contact-form {
  label: "Contact"
}

Validator error: Form name: Name must contain only letters, numbers, and underscores

All resource names (forms, graphs, nodes, streams, secrets, auth blocks, postgres blocks, triggers) must match ^[a-zA-Z0-9_]+$.

// Correct
form contact_form {
  label: "Contact"
}

Removed constructs

The persistence { } block

// Incorrect
graph submissions {
  label: "Submissions"
  persistence {
    enabled: true
    condition: @ts { return true }
  }
  root {
    type: code
    label: "Entry"
    code: @ts { return context.nodes.root.input }
  }
}

Parser error: persistence { } blocks have been removed — use a top-level stream block instead

Use a top-level stream { } block to persist graph output.

// Correct
graph submissions {
  label: "Submissions"
  root {
    type: code
    label: "Entry"
    outputSchema: @json {
      { "type": "object", "properties": { "message": { "type": "string" } } }
    }
    code: @ts { return context.nodes.root.input }
  }
}

stream submission_log {
  label: "Submission log"
  graph: submissions
  schema: @json {
    { "type": "object", "properties": { "message": { "type": "string" } } }
  }
  condition: @ts { return true }
  prepare: @ts {
    return { message: context.output.root.message }
  }
}

query and querySql on stream nodes

// Incorrect
node recent {
  type: stream
  stream: "submissions"
  query: @sql { SELECT * FROM {{table}} LIMIT 10 }
}

Validator error: querySql and query are no longer supported on stream nodes; use filter (@ts returning a filter object)

// Correct
node recent {
  type: stream
  label: "Recent submissions"
  stream: submissions
  filter: @ts {
    return { score: { gte: 50 } }
  }
}

Note: the stream: field takes a bare identifier, not a quoted string.

Parser pitfalls (silent drops)

These patterns cause the parser to silently stop processing the rest of the file. swirls doctor may report success but with fewer graphs than you defined.

Unicode in comments

Non-ASCII characters in // comments cause the parser to miscount lines. Everything after the comment may be dropped.

// Incorrect
// Graph: get_token → fetch OAuth
// ──────────────────────────────
graph get_token { ... }
// Correct
// Graph: get_token - fetch OAuth
// -----------------------------------
graph get_token { ... }

Characters to avoid in comments: , , , , , , , and any other non-ASCII character.

Hyphenated header keys in object literals

// Incorrect — causes silent drop
headers: { Content-Type: "application/json" }

The parser treats Content-Type as a subtraction expression and consumes everything to end-of-file.

// Correct — use a @ts block
headers: @ts {
  return { "Content-Type": "application/json" }
}

HTTP nodes also accept a top-level auth block for Authorization and API key headers, which avoids the problem entirely.

Detecting silent drops

When swirls doctor reports fewer graphs, forms, or triggers than you defined, a parser bug is dropping content.

swirls doctor

Compare the counts in the output to what you wrote. If they differ, the problem is always in or before the first missing item.

To isolate it: comment out half the file, run swirls doctor, repeat. The bug is in whichever half causes the missing count.

See Syntax for safe patterns, and How execution works for the broader execution model.

On this page