Context and Type Safety
How the context object works in TypeScript blocks and how the LSP provides type safety.
Every ts block in a .swirls file has a typed context object in scope. The Swirls LSP generates TypeScript types from your schemas so you get autocomplete and type checking without leaving the .swirls file.
context.nodes
context.nodes.root.input: The workflow trigger payload (form, webhook, schedule, etc.), typed from the root node'sinputSchema. Usecontext.nodes.root.input.email,context.nodes.root.input.name, etc.context.nodes.root.output: After the root node runs, its return value (typed from the root node'soutputSchema). Downstream nodes read normalized or derived fields here.context.nodes.<nodeName>.output: Output of an upstream node, typed from that node'sschema. For example:context.nodes.entry.output.email.
Downstream nodes do not define inputSchema in the DSL. Their input is derived from upstream output schemas automatically.
Some node types shape their output differently:
type: workflownodes: output is keyed by the child workflow's leaf node names. Readcontext.nodes.<nodeName>.output.<leafName>.type: mapnodes: output is an array of leaf-keyed objects initemsorder. Each entry is{ <leafName>: <leafOutput> }, not the leaf output directly.type: fanoutnodes: output has the same ordered array shape as map, although independent children run concurrently.type: whilenodes: output is the last iteration's leaf outputs underlastOutput. Readcontext.nodes.<nodeName>.output.lastOutput.<leafName>.type: httpnodes: output is the parsed response body. Status and header metadata live oncontext.nodes.<nodeName>.meta.
context.iteration
Inside type: map, type: fanout, and type: while child workflows, and in a while node's condition and update @ts blocks, context.iteration is in scope:
| Field | Where | Description |
|---|---|---|
item | map, fanout | Current element from items (typed from the subgraph root inputSchema). |
index | map, fanout, while | Zero-based iteration index. |
total | map, fanout | Total item count for this iteration run. |
input | while | The input for this iteration (typed from root inputSchema). Iteration 0 receives the node's input: value; later iterations receive what update returned. |
previous | map, while | Prior iteration’s leaf outputs (shape keyed by leaf node names). undefined on iteration 0, so use optional chaining. |
Inside the child workflow, context.nodes.root.input is the parent workflow's root input, not the current item. Use context.iteration.item (map/fanout) or context.iteration.input (while). Fanout does not expose previous because its iterations overlap.
See Node types (map, fanout, and while) and Workflows.
context.reviews
Available when upstream nodes have review enabled. Keyed by node name.
context.reviews.<nodeName>: The reviewer's submitted form data from a reviewed node.- The current node can also read its own review data:
context.reviews.<currentNodeName>. - When a review defines a schema, the LSP types the review data from that schema.
A review-enabled node routes to review instead of running its own work; on approval, the same submitted data is also recorded as that node's output. Use optional chaining (context.reviews.gate?.approved) since the shape is schema-dependent. See Reviews for details.
context.secrets
Project secrets available to this node’s TypeScript blocks. Declare secret blocks, wire secrets: { … } on nodes, and read context.secrets.<blockName>.<VAR>. Full reference: Connections.
Inferred vendor keys name which credential a node needs (OPENROUTER_API_KEY for ai, RESEND_API_KEY for email, etc.), but the key must be declared in a secret block (optionally type: managed). They are not on context.secrets for user code unless you also list them on the node secrets: map. ARCHIL_API_KEY for disk is platform-only and never declared.
The LSP types context.secrets from the secrets map on each node.
context.db
context.db.<name> is a generated, fully typed Prisma client for a declared database block. No SQL, no params: block, no JSON Schema to keep in sync.
context.db is only available inside a type: database node's run: block. The client there is narrowed to the node's declared operation (query, insert, update, delete, or transaction): a call outside that operation is rejected at runtime. See Node types.
node active_admins {
type: database
database: my_db
operation: query
run: @ts {
return await context.db.my_db.user.findMany({
where: { role: "ADMIN" },
include: { posts: true },
})
}
}Accessing context.db from a code node, or any other node type, throws at runtime: "context.db is only available inside a database node. Move this query into a database node and reference its output." Move the query into a type: database node and read its output from downstream nodes with context.nodes.<name>.output.
$transaction only works from a type: database node with operation: transaction. That operation exposes the full client, opened inside one atomic $transaction, for a multi-step change no single narrowed operation can express.
In a top-level migration block's operation: body, context.db is the same full client available to a transaction operation.
return await ... works directly in any @ts body: a returned promise resolves before it becomes the node's output. Values round-trip with real types across the boundary: DateTime fields come back as Date objects, BigInt and byte columns round-trip as BigInt and bytes, and Decimal values come back as strings.
See Database for the block, migrations, and the governed node in full.
context.meta
Execution metadata:
context.meta.triggerId: ID of the trigger that started this execution. String ornull.context.meta.triggerType:"form","webhook","schedule", ornull. There is no"agent"trigger type.
How the LSP builds types
The LSP creates a virtual TypeScript document for each ts block:
- Reads JSON Schemas from
inputSchema,outputSchema, and reviewschemafields. - Converts them to TypeScript interfaces.
- Declares
contextwith the correct types. - The TypeScript language service provides completions, hover info, and error checking.
The LSP derives types from your schemas. Write schemas, not type annotations.
Example
This workflow shows how context typing flows through nodes. The root node normalizes input from context.nodes.root.input, the downstream greet node reads context.nodes.root.output with full type safety, and the notify node accesses a secret via context.secrets.
secret api_creds {
label: "API credentials"
vars: [NOTIFY_KEY]
}
workflow typed_example {
label: "Type Safety Demo"
root {
type: code
label: "Entry"
inputSchema: @json {
{
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
},
"additionalProperties": false
}
}
code: @ts {
// context.nodes.root.input is typed from inputSchema
const { name, email } = context.nodes.root.input
return { name: name.trim(), email: email.toLowerCase() }
}
}
node greet {
type: code
label: "Generate greeting"
schema: @json {
{
"type": "object",
"required": ["greeting"],
"properties": { "greeting": { "type": "string" } },
"additionalProperties": false
}
}
code: @ts {
// context.nodes.root.output is typed from the root outputSchema
const name = context.nodes.root.output.name
return { greeting: `Hello, ${name}!` }
}
}
node notify {
type: code
label: "Send notification"
secrets: {
api_creds: [NOTIFY_KEY]
}
code: @ts {
// context.secrets is typed from the secrets map above
const key = context.secrets.api_creds.NOTIFY_KEY
return { sent: Boolean(key) }
}
}
flow {
root -> greet
greet -> notify
}
}Further reading
- Workflows: workflow structure and node definitions
- Reviews: accessing review data via
context.reviews - Connections:
secretblocks,authcredential profiles, andcontext.secrets - Database:
databaseblocks, migrations, and thedatabasenode that carriescontext.db