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
| Wrong | Correct |
|---|---|
type: api, type: request, type: fetch | type: http |
type: delay, type: sleep | type: wait |
type: llm, type: prompt, type: chat | type: ai |
type: subgraph, type: call, type: child | type: graph |
type: db, type: database, type: sql | type: postgres |
type: storage, type: file, type: s3 | type: bucket |
type: map | Use type: map (valid) or type: code for simple transforms |
type: loop, type: retry | type: 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 doctorCompare 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.