Syntax
Comments, identifiers, values, and embedded @ts / @json / @sql blocks.
This page covers the foundational syntax of the Swirls DSL: how to write comments, name resources, express values, and embed TypeScript, JSON, and SQL with @ts { }, @json { }, and @sql { }.
Comments
Single-line comments start with //.
// This is a comment
form contact {
label: "Contact" // inline comment
}Multi-line comments use /* ... */.
/*
This is a
multi-line comment
*/
form contact {
label: "Contact"
}A /* ... */ comment placed immediately before a declaration becomes a doc comment. The LSP shows it on hover.
/* Collects a visitor's email address for lead capture. */
form contact {
label: "Contact"
enabled: true
}Identifiers
Identifiers name resources (top-level blocks), workflows, and nodes within a workflow. They match the pattern ^[a-zA-Z0-9_]+$. Names may start with a digit.
form contact_form { ... }
workflow process_contact { ... }Names must be unique within their kind. Two forms cannot share the same name, but a form and a workflow can.
Values
Strings
Strings use double quotes.
label: "Process Contact"Numbers
Numbers are integers or floats.
temperature: 0.7
maxTokens: 512Booleans
enabled: true
stream: falseObjects
Objects use { key: value, ... } syntax. Prefer keys without hyphens: hyphenated keys in object literals (for example header names) are parsed incorrectly and can silently break the rest of the file. For HTTP auth, use a top-level auth block and auth: on http nodes instead of raw Content-Type / Authorization headers.
options: { n: 1, size: "1024x1024" }Arrays
tags: ["ai", "lead-gen", "email"]Embedded code (@ts, @json, @sql)
Use @ts { ... }, @json { ... }, or @sql { ... } to embed code in field values. Braces are balanced: inner { / } in TypeScript or JSON are allowed. The closing } ends the block.
TypeScript (@ts)
TypeScript blocks hold executable expressions for node code fields, LLM prompt fields, email fields, and top-level stream block condition / prepare fields. The context object is in scope and typed by the LSP based on the schemas in the same file.
code: @ts {
const email = context.nodes.root.input.email ?? ""
return { email: email.toLowerCase() }
}You can move the same body into a standalone file with extension .ts.swirls and reference it from the workflow: ````swirls
code: @ts "./handlers/entry.ts.swirls"
Paths are resolved **relative to the directory of the `.swirls` file** that contains the reference. The file must use the `.ts.swirls` suffix. Semantics match an inline `@ts { }` block: the file contents are the body of an expression function, so you typically use `return …`.
In the editor, standalone `.ts.swirls` files use the Swirls language server with the same `context` typing as inline blocks, as long as the file is referenced from a parent `.swirls` file.
### JSON (`@json`)
JSON blocks hold structured content, primarily JSON Schema objects for `schema`, `inputSchema`, and `outputSchema` fields.
````swirls
schema: @json {
{
"type": "object",
"required": ["email"],
"properties": { "email": { "type": "string" } },
"additionalProperties": false
}
}SQL (@sql)
SQL blocks hold queries for type: postgres nodes (select: / insert:). Use {{key}} placeholders in the SQL with matching keys in the node's params: block: see Postgres.
select: @sql {
SELECT id, email, created_at
FROM users
WHERE email = {{email}}
}Top-level structure
Optionally declare version: once at the top. The rest of the file may contain any number of these top-level blocks in any order:
| Block | Purpose |
|---|---|
schema | Reusable JSON Schema (schema name { … }) |
form | Form resource |
webhook | Webhook resource |
schedule | Cron schedule resource |
secret | Named group of secret variable identifiers |
auth | Authentication profile for your own credentials (oauth, api_key, basic, bearer) |
connection | Swirls-brokered OAuth integration, referenced by name (connection name { provider: slack }) |
postgres | External PostgreSQL connection and table schemas |
disk | Remote disk mount (Archil-backed) |
agent | LLM agent definition with tools, profiles, and subagent teams |
channel | Bind an agent to a chat platform (Slack, Linear, Discord, web) |
stream | Persist workflow output to a typed stream |
workflow | Workflow DAG |
trigger | Bind a resource to a workflow |
role | Access role derived from verified identity attributes (role name { match { … } }) |
policy | Role-to-agent grants (allow <role> -> agent <name>); grants flip the project to deny by default |
version: 1
schema contact_payload { ... }
secret creds { ... }
form contact { ... }
workflow process_contact { ... }
trigger on_contact { ... }Block order does not matter. The compiler resolves references by name. Names must be unique within their kind across the entire file.
Named schemas
Declare a schema once and reference it anywhere a schema is accepted (form / webhook / stream, root inputSchema / outputSchema, node outputSchema / schema, review schema):
schema contact_payload {
label: "Contact payload"
schema: @json {
{
"type": "object",
"required": ["email"],
"properties": { "email": { "type": "string" } },
"additionalProperties": false
}
}
}
form contact {
label: "Contact"
schema: contact_payload
}Inline object-literal schemas
Besides @json { … }, you can write JSON Schema as an inline object with unquoted keys (DSL object syntax). It produces the same AST as @json.
root {
type: code
label: "Entry"
inputSchema: {
type: "object"
required: ["title", "body"]
properties: {
title: { type: "string" }
body: { type: "string" }
}
additionalProperties: false
}
code: @ts { return context.nodes.root.input }
}