Syntax
Comments, identifiers, values, and embedded @ts / @json / @sql / @prisma / @openui blocks.
This page covers the foundational syntax of the Swirls DSL: how to write comments, name resources, express values, and embed TypeScript, JSON, SQL, and Prisma schema language with @ts { }, @json { }, @sql { }, @prisma { }, and @openui { }.
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. This applies to every block, including app.
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. Keys must be bare identifiers without hyphens. An unquoted hyphenated key (for example Content-Type) stops the parser and silently drops the rest of the file; a quoted hyphenated key parses to an empty object. For hyphenated header names, use a @ts block that returns the object. For HTTP auth, use a top-level auth block and auth: on http nodes instead of raw Authorization headers.
options: { n: 1, size: "1024x1024" }Arrays
tags: ["ai", "lead-gen", "email"]Embedded code (@ts, @json, @sql, @prisma, @openui)
Use @ts, @json, @sql, @prisma, or @openui blocks to embed code in supported 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:
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.
schema: @json {
{
"type": "object",
"required": ["email"],
"properties": { "email": { "type": "string" } },
"additionalProperties": false
}
}SQL (@sql)
SQL blocks hold queries for type: postgres nodes (select: / insert: / update: / delete:). 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}}
}Prisma (@prisma)
@prisma blocks appear in exactly one place: the schema: field of a top-level database block. The body is verbatim Prisma schema language, models and enums only: see Database.
database app_db {
label: "App database"
schema: @prisma {
model User {
id Int @id @default(autoincrement())
email String @unique
}
}
}OpenUI (@openui)
An app page's render: field accepts @openui { … } to describe its interface. See Apps for the component vocabulary and data bindings.
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. The block kinds are:
| 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 | Credential profile for credentials you hold (oauth, api_key, basic, bearer) |
connection | Managed OAuth or self-managed API connection |
network | Declared private connectivity requirements; live named routing is not yet available |
connector | Swirls-managed inbound OAuth application that external MCP clients authenticate against (MCP channels) |
action | Typed integration operation called by integration nodes via action: |
postgres | External PostgreSQL connection and table schemas |
database | Swirls-managed Postgres declared with a schema: @prisma { } block |
migration | Ordered, run-once data transform against a database block |
disk | Remote disk mount for disk nodes |
agent | LLM agent definition with tools, profiles, skills, and subagent teams |
skill | Knowledge-skill package from .agents/skills/<name>/, referenced by an agent's skills: list |
mcp | Remote MCP server slot referenced by an agent's mcp: list |
channel | Bind an agent to a chat platform (Slack, Linear, Discord, web, MCP, or a shared Swirls app room) |
stream | Persist workflow output to a typed stream |
view | Compose streams into a spreadsheet with mapped and computed columns |
query | Declared read-only database query for an app |
workflow | Workflow DAG |
trigger | Bind a resource to a workflow |
mock | Playbook side-effect stubs and default mock policy (Testing) |
playbook | Hosted test suite with case / eval entries (Testing) |
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 |
app | Hosted interface with navigation, OpenUI pages, review inboxes, channel: pages, and threads: directories |
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 workspace, not just the file: see Multi-file workflows.
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 }
}Decision declarations and executable models
decision <name> { } is a top-level declaration with required literal provider: typesafe, model, bare secrets block reference, inputSchema, and literal questions. See the field contract. Invoke it with type: decision, decision: <name>, and state. On ai, agent, and decision workflow nodes, model accepts quoted text, inline @ts, or a file-backed @ts; declarations themselves retain a static default. Question maps are literal object/@json data, not executable expressions.