SWIRLS_
Writing Swirls

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: 512

Booleans

enabled: true
stream: false

Objects

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:

BlockPurpose
schemaReusable JSON Schema (schema name { … })
formForm resource
webhookWebhook resource
scheduleCron schedule resource
secretNamed group of secret variable identifiers
authAuthentication profile for your own credentials (oauth, api_key, basic, bearer)
connectionSwirls-brokered OAuth integration, referenced by name (connection name { provider: slack })
postgresExternal PostgreSQL connection and table schemas
diskRemote disk mount (Archil-backed)
agentLLM agent definition with tools, profiles, and subagent teams
channelBind an agent to a chat platform (Slack, Linear, Discord, web)
streamPersist workflow output to a typed stream
workflowWorkflow DAG
triggerBind a resource to a workflow
roleAccess role derived from verified identity attributes (role name { match { … } })
policyRole-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 }
}

On this page