SWIRLS_
Language

Multi-file workflows

How Swirls discovers .swirls files, how declarations reference each other across files, and how to organize a workspace as it grows.

A Swirls workspace is all the .swirls files the CLI discovers in your project directory. You can put everything in one file or spread declarations across many files. The engine assembles them at load time.

File discovery

The CLI searches for .swirls files recursively from the project root. It ignores node_modules/ and __fixtures__/ directories.

Two file extensions matter:

  • .swirls: DSL files. Discovered and parsed automatically. Each file can contain any top-level declarations in any order.
  • .ts.swirls: standalone TypeScript files. Loaded only when referenced explicitly from a code: @ts "path.ts.swirls" field. Not parsed as DSL.

There are no imports, no require, and no module declarations. The CLI discovers all .swirls files itself. You do not register them anywhere.

How references work across files

Declarations reference each other by name. Names are resolved across all files in the workspace as a single namespace.

A trigger in triggers.swirls that references form:contact and graph:process_contact works as long as a form contact { } declaration and a graph process_contact { } declaration exist somewhere in the workspace, regardless of which files they are in.

The same resolution applies to every reference type:

Reference inResolved by name
trigger: form:<name> -> <graph>form <name> and graph <name> anywhere in workspace
trigger: webhook:<name> -> <graph>webhook <name> and graph <name> anywhere in workspace
trigger: schedule:<name> -> <graph>schedule <name> and graph <name> anywhere in workspace
stream: { graph: <name> }graph <name> anywhere in workspace
node { type: graph, graph: <name> }graph <name> anywhere in workspace
node { type: stream, stream: <name> }stream <name> anywhere in workspace
node { type: postgres, postgres: <name> }postgres <name> anywhere in workspace
node { auth: <name> }auth <name> anywhere in workspace
schema: <name> (bare identifier)schema <name> anywhere in workspace
secrets: { <blockName>: [...] }secret <blockName> anywhere in workspace

Names must be unique within their kind across the entire workspace. Two graphs cannot share a name, even if they are in different files. Two forms cannot share a name. Each kind has its own namespace, so a form and a graph can share the same name without conflict.

.ts.swirls files

A .ts.swirls file holds TypeScript that you reference from a node field:

node normalize {
  type: code
  label: "Normalize"
  code: @ts "./handlers/normalize.ts.swirls"
}

The path is resolved relative to the .swirls file that contains the reference.

The .ts.swirls file contains a TypeScript function body with access to the same context object as an inline @ts { } block:

// handlers/normalize.ts.swirls
const raw = context.nodes.root.input.email ?? ""
return { email: raw.trim().toLowerCase() }

These files are not discovered by the CLI. They are only loaded when referenced.

Workspace layouts

Small workspace: one file

A single file works well for most projects. Keep everything together until it becomes hard to navigate.

my_project/
  workflows.swirls
  swirls.config.ts

Medium workspace: one file per domain

Split by concern when a single file becomes long. Group related graphs, triggers, and their resources.

my_project/
  contacts/
    forms.swirls          // form contact_form { }
    graph.swirls          // graph process_contact { }
    triggers.swirls       // trigger on_contact { form:contact_form -> process_contact }
  billing/
    webhooks.swirls       // webhook stripe_event { }
    graph.swirls          // graph handle_payment { }
    triggers.swirls       // trigger on_payment { webhook:stripe_event -> handle_payment }
  shared/
    secrets.swirls        // secret stripe_creds { } secret openai_creds { }
    auth.swirls           // auth stripe_auth { }
  swirls.config.ts

shared/secrets.swirls declares secret blocks that any graph in the workspace can reference.

Large workspace: cookbook-style nested folders

Organize by feature or product area. Use subfolders freely; the CLI discovers recursively.

my_project/
  workflows/
    onboarding/
      forms.swirls
      steps/
        normalize.swirls
        enrich.swirls
        notify.swirls
      triggers.swirls
    support/
      forms.swirls
      triage.swirls
      triggers.swirls
    research/
      scheduled.swirls
      fetch.swirls
      synthesize.swirls
  handlers/
    normalize.ts.swirls      // referenced from onboarding/steps/normalize.swirls
    format_email.ts.swirls   // referenced from onboarding/steps/notify.swirls
  shared/
    secrets.swirls
    auth.swirls
    postgres.swirls
  swirls.config.ts

In this layout, handlers/*.ts.swirls files are reusable TypeScript referenced explicitly by multiple nodes. Shared infrastructure (secrets, auth, postgres connections) lives in shared/ and is available to every graph.

Cross-file reference example

This example spans two files. They work together because names are resolved globally.

// shared/secrets.swirls

secret openai_creds {
  vars: [OPENAI_API_KEY]
}
// research/fetch.swirls

graph fetch_research {
  label: "Fetch research"

  root {
    type: code
    label: "Entry"
    inputSchema: @json {
      { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" } } }
    }
    outputSchema: @json {
      { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" } } }
    }
    code: @ts { return { query: context.nodes.root.input.query } }
  }

  node search {
    type: parallel
    label: "Search"
    operation: search
    objective: @ts { return "Research: " + context.nodes.root.output.query }
    searchQueries: @ts { return [context.nodes.root.output.query] }
  }

  node summarize {
    type: ai
    label: "Summarize"
    kind: object
    model: "anthropic/claude-3.5-sonnet"
    prompt: @ts {
      return "Summarize these results: " + JSON.stringify(context.nodes.search.output)
    }
    schema: @json {
      { "type": "object", "required": ["summary"], "properties": { "summary": { "type": "string" } } }
    }
    secrets: {
      openai_creds: [OPENAI_API_KEY]
    }
  }

  flow {
    root -> search
    search -> summarize
  }
}
// research/scheduled.swirls

schedule weekly_research {
  label: "Weekly research"
  cron: "0 9 * * 1"
  timezone: "UTC"
}

trigger on_weekly_research {
  schedule:weekly_research -> fetch_research
  enabled: true
}

Three files, one workspace. trigger in scheduled.swirls references fetch_research defined in fetch.swirls. The summarize node in fetch.swirls references openai_creds defined in shared/secrets.swirls. All three resolve at load time.

Running swirls doctor on a multi-file workspace

swirls doctor validates all files at once. Run it from the project root:

swirls doctor

It parses every .swirls file, resolves all cross-file references, and reports diagnostics. A graph that references an undefined auth block or a trigger that names a nonexistent form will produce an error here.

See Common mistakes for a reference to every validator diagnostic and how to fix it.

Further reading

On this page