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 acode: @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 in | Resolved 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.tsMedium 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.tsshared/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.tsIn 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 doctorIt 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
- Syntax: top-level declarations and how names work.
- Graphs: graph structure and edges.
- Common mistakes: parser and validator diagnostics.
- Local development: running the worker against your workspace.