Writing Swirls

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

Booleans

enabled: true
stream: false

Objects

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:

BlockPurpose
schemaReusable JSON Schema (schema name { … })
formForm resource
webhookWebhook resource
scheduleCron schedule resource
secretNamed group of secret variable identifiers
authCredential profile for credentials you hold (oauth, api_key, basic, bearer)
connectionManaged OAuth or self-managed API connection
networkDeclared private connectivity requirements; live named routing is not yet available
connectorSwirls-managed inbound OAuth application that external MCP clients authenticate against (MCP channels)
actionTyped integration operation called by integration nodes via action:
postgresExternal PostgreSQL connection and table schemas
databaseSwirls-managed Postgres declared with a schema: @prisma { } block
migrationOrdered, run-once data transform against a database block
diskRemote disk mount for disk nodes
agentLLM agent definition with tools, profiles, skills, and subagent teams
skillKnowledge-skill package from .agents/skills/<name>/, referenced by an agent's skills: list
mcpRemote MCP server slot referenced by an agent's mcp: list
channelBind an agent to a chat platform (Slack, Linear, Discord, web, MCP, or a shared Swirls app room)
streamPersist workflow output to a typed stream
viewCompose streams into a spreadsheet with mapped and computed columns
queryDeclared read-only database query for an app
workflowWorkflow DAG
triggerBind a resource to a workflow
mockPlaybook side-effect stubs and default mock policy (Testing)
playbookHosted test suite with case / eval entries (Testing)
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
appHosted 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.

On this page