Every tool is a workflow
Swirls now has agents. An agent is a block in a `.swirls` file, and its tools are workflows — so every tool call runs as a checkpointed execution under the runtime, not as a function call around it.
In most agent frameworks, a tool is a back door. You hand the model a function and a JSON schema, and when it calls that function the code runs outside whatever governs the rest of your system — no durable record, no scoped identity, nothing to review. The model's reach is exactly as wide as the loosest tool you gave it.
Swirls now has agents. An agent is a block in a .swirls file, and its tools are workflows — the same durable, schema-bounded primitive you declare in your .swirls files.
#Why this matters
The tool call is where agent governance leaks. Most tool-calling setups treat the tool as a bare function: it executes off to the side, outside your durable execution, outside the stream, outside the identity model the rest of the work runs under. The agent is governed; the moment it calls a tool, the governance stops.
In Swirls a tool is a workflow. The runtime runs each tool call as a checkpointed execution — input and output bounded by schemas, persisted in streams, advanced by the same runtime that runs everything else. Nothing the model reaches for escapes it.
#How it works
An agent is a top-level block: a model, a secret, a system prompt, and the workflows it can call.
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent support_agent {
description: "Triages support tickets and drafts responses."
provider: anthropic
model: "claude-sonnet-4-6"
secrets: llm_creds
system: @ts {
return "You are a support agent. Use the available tools to investigate and resolve tickets."
}
tools: [lookup_ticket, post_reply]
}
Each name in tools is a workflow. lookup_ticket is not a special tool object — it is an ordinary workflow with a description and schemas, the same kind you write for anything else:
workflow lookup_ticket {
description: "Retrieve a support ticket by its ID. Returns title, description, and status."
root {
type: code
inputSchema: @json {
{ "type": "object", "required": ["ticket_id"],
"properties": { "ticket_id": { "type": "string" } } }
}
code: @ts { return { ticket_id: context.nodes.root.input.ticket_id } }
}
node fetch {
type: postgres
postgres: main_db
select: @sql {
SELECT title, description, status FROM tickets WHERE id = {{ticket_id}}
}
params: @ts { return { ticket_id: context.nodes.root.output.ticket_id } }
schema: @json {
{ "type": "array", "items": { "type": "object", "properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"status": { "type": "string" } } } }
}
}
flow { root -> fetch }
}
The description becomes the tool's help text, the root inputSchema is its call signature, and the leaf schema types what comes back — the same schemas the runtime already enforces on any workflow.
From there the runtime drives the turn loop: the model generates, a tool call runs the named workflow, its output returns to the model, and the loop continues until a final response or maxSteps (default 20). Built-in workspace tools — read, write, edit, bash, grep, find, ls — count against that limit too. None of it is fire-and-forget. Every turn is checkpointed, so a restart resumes where it stopped rather than starting the conversation over.
#Reaching the agent
Closing the back door on tools is one thing; an agent still needs a front door. An agent block is a worker, not a way in by itself. Three things can drive it: a type: agent node inside a workflow, an interactive swirls chat start <agent> session, or a channel that binds it to a chat platform.
channel support_web {
label: "Support (Web)"
platform: web
integration: web
agent: support_agent
mode: dm
enabled: true
}
Point platform at slack, linear, discord, or web, name the agent, and the runtime answers messages there: each inbound message starts a turn, and the reply posts back to the conversation. The same agent can back a workflow node, a swirls chat session, and several channels at once — the same .swirls file that defines the agent and its tools defines who gets to talk to it. (Channel reference.)
#Getting started
Three steps:
- Write an
agentblock with a model and asecret - List the workflows it can call in
tools— or hand work to a team of subagents, each on its own model - Reach it from a
type: agentnode, aswirls chat start <agent>session, or achannel
Run it locally first — the same .swirls file runs unchanged in swirls cloud. The full agent reference — roles, teams, sandboxes, channels, structured output, and the turn loop — lives at swirls.ai/docs/language/agents.