Agents
Declare stateful LLM agents with tools, profiles, and sandboxes, then invoke them from any workflow.
What it is. An actor that can reason. You give it a fuzzy set of instructions and it decides what to do, unlike a workflow, which follows exact steps.
Use it when you want to chat with your system, offload tasks that resist exact procedures, or add a judgment step inside a workflow. Agents can carry tools, restricted profiles, and a team of subagents.
Works with channels (chat surfaces), skills (knowledge packages), MCP servers (remote tools), workflows (as a node, or with workflows as the agent's tools), disks (a workspace), and access roles (who may invoke it).
An agent block defines a persistent LLM worker. It has a model, a secret, and optional tool workflows, skills, MCP server slots, and profiles. Invoke the agent from a workflow with a type: agent node, chat with it in Swirls Cloud or from the CLI, or expose it on a channel.
When to use agent vs ai
type: ai | type: agent | |
|---|---|---|
| Use for | Single-shot generation, classification, structured output | Multi-step reasoning, file manipulation, tool calls |
| Tool loop | No | Yes. Calls tool workflows, subagents, skills, and MCP tools. |
| Workspace | None | Optional persistent Linux sandbox |
| Configuration | Inline on the node | Top-level agent block, referenced by name |
Use type: ai for a single model call that returns a value. Use type: agent when the model needs to reason, call tools, or work on files over multiple steps.
The agent block
The agent block is a top-level declaration. A workflow node references it by name.
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent support_agent {
label: "Support Agent"
description: "Triages support tickets and drafts responses."
provider: anthropic
model: "claude-3-5-sonnet-20241022"
secrets: llm_creds
maxSteps: 15
system: @ts {
return "You are a support agent. Use the available tools to investigate and resolve tickets."
}
tools: [lookup_ticket, post_reply]
profile escalation {
description: "Handles escalated tickets with expanded tool access"
system: @ts {
return "You are a senior support agent. Be thorough. Document every action you take."
}
tools: [lookup_ticket, post_reply]
}
}Field reference
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier, e.g. "claude-3-5-sonnet-20241022", "gpt-4o". |
secrets | identifier | Yes | Name of a top-level secret block. Must declare the provider's API key variable. Not a quoted string. |
provider | string | No | One of: openrouter (default), anthropic, openai, google. |
label | string | No | Display name. |
description | string | No | Description shown in the Portal. |
system | @ts { } block | No | Returns the system prompt string. Must be a @ts block. A plain string is rejected. |
temperature | number | No | Accepted and validated. Has no effect on agent turns today. |
maxTokens | number | No | Accepted and validated. Has no effect on agent turns today. |
maxSteps | number | No | Max tool-call steps per turn. Defaults to 20 when unset. |
tools | identifier array | No | Workflows this agent may call as tools, e.g. [search_kb, file_writer]. |
skills | identifier array | No | Top-level skill blocks the agent can read at runtime, e.g. [swirls_lang]. See Skills. |
mcp | identifier array | No | Top-level mcp blocks naming remote MCP server slots the agent draws tools from. See MCP servers. |
team | identifier array | No | Other agent blocks this agent may delegate to as subagents, e.g. [researcher, writer]. See Subagent teams. |
disks | identifier array | No | Top-level disk blocks mounted into the agent's workspace at /mnt/disks/<name>. See Disks. |
sandbox | sandbox: { } block | No | Declares the agent's workspace: resources and idle lifecycle. Enables the built-in workspace tools. See The sandbox block. |
wallet | wallet: { } block | No | Per-agent tool-spend budget that enables the hosted Zero tools. See The wallet block. |
Provider and secret key
Each provider expects a specific variable in the secret block:
provider | Required variable |
|---|---|
openrouter (default) | OPENROUTER_API_KEY |
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
google | GOOGLE_GENERATIVE_AI_API_KEY |
The key must appear in the vars list of the referenced secret block. Set its value with swirls env set or the project vault. See Secrets and auth.
Profiles
A profile scopes the agent to a specific persona. Declare it inside the agent block with profile <name> { }.
| Field | Type | Required | Description |
|---|---|---|---|
description | string | No | Human-readable description of the profile. |
system | @ts { } block | No | Overrides the agent-level system prompt when this profile is active. |
tools | identifier array | No | Subset of the agent's tools available in this profile. Must not include workflows not already listed in the agent's tools. |
skills | identifier array | No | Subset of the agent's skills available in this profile. |
mcp | identifier array | No | Subset of the agent's mcp slots available in this profile. |
sandbox | sandbox: { } block | No | Overrides the agent-level sandbox for this profile. |
The agent node selects a profile with profile: <name>. When a profile declares tools, skills, or mcp, those become the effective sets. When it declares system, that overrides the agent's system prompt.
A profile cannot override model, provider, secrets, team, wallet, or disks. Profiles apply on the workflow-node path; chat sessions use the agent's default system prompt.
A profile controls what the agent may do when it runs. To control who may invoke the agent at all, use the top-level role and policy blocks. See Access control.
Subagent teams
An agent can delegate to other agents. List them in the team field. Each team member becomes a callable tool that the model invokes with a task description. The member runs as its own agent, with its own model, tools, and sandbox, and returns its result to the caller.
Use teams to compose specialists. One orchestrator routes work to focused agents instead of holding every tool and instruction itself.
secret vendor_keys {
vars: [OPENROUTER_API_KEY]
}
agent researcher {
label: "Researcher"
secrets: vendor_keys
model: "openai/gpt-4o-mini"
tools: [search_kb]
system: @ts {
return "Research the question and return concise findings with sources."
}
}
agent writer {
label: "Writer"
secrets: vendor_keys
model: "openai/gpt-4o-mini"
system: @ts {
return "Turn findings into clear, well-structured prose."
}
}
agent orchestrator {
label: "Orchestrator"
secrets: vendor_keys
model: "google/gemini-3.1-flash-lite"
maxSteps: 16
team: [researcher, writer]
system: @ts {
return [
"You coordinate specialists.",
"Call a team tool with a clear task describing what to do.",
"Relay the specialist's answer plainly.",
].join("\n")
}
}Rules
- Team members are referenced by bare identifier, not a quoted string.
- Each member must be a defined
agentblock in the workspace. - An agent cannot list itself in its own
team. - A team member name cannot collide with a workflow tool name in the same agent's
tools. - Teams cannot form a cycle.
a -> b -> ais a validation error, and so is any longer loop. - Team members resolve their own
skillsandmcplists. They do not inherit the caller's.
Skills and MCP servers
Two more ways to extend an agent, each with its own page:
- Skills package reference material from your repo. Declare a top-level
skillblock, list it inskills:, and the agent reads the files on demand at runtime. - MCP servers connect the agent to remote tools. Declare a top-level
mcpblock as a named slot, list it inmcp:, and bind the slot to a server URL and credential in Swirls Cloud.
The sandbox block
An agent with a sandbox: { } block gets a persistent Linux workspace where it can run code and manipulate files. The block controls the workspace's resources and idle lifecycle, and its presence enables the built-in workspace tools. Mounting disks: enables them too.
agent data_agent {
model: "gpt-4o"
secrets: oai_creds
sandbox: {
cpus: 2
memoryMiB: 2048
diskGiB: 10
autoStopMinutes: 30
autoArchiveMinutes: 1440
autoDeleteMinutes: -1
ephemeral: false
}
}Resource fields
| Field | Type | Minimum | Description |
|---|---|---|---|
cpus | number | 1 | CPU cores allocated to the sandbox. |
memoryMiB | number | 128 | RAM in mebibytes. |
diskGiB | number | 1 | Disk space in gibibytes. |
Lifecycle fields
| Field | Type | Description |
|---|---|---|
autoStopMinutes | number | Stop the sandbox after this many idle minutes. Set to 0 to disable auto-stop. |
autoArchiveMinutes | number | Archive the sandbox this many minutes after it stops. Set to -1 to disable. |
autoDeleteMinutes | number | Delete the sandbox this many minutes after it stops. Set to -1 to disable. |
ephemeral | boolean | When true, workspace state is discarded on stop. Files do not persist across turns. |
The sandbox provisions lazily. A turn that does not call any workspace tool never starts one. Workspace files persist across turns for the same agent unless ephemeral: true. Scratch files live at /srv/workspace; shared disks listed in disks: mount at /mnt/disks/<name>.
The wallet block
The wallet: { } block gives the agent a tool-spend budget and enables the hosted Zero tools: zero_search (free catalog search), zero_get (inspect a capability's schema), zero_fetch (paid capability calls), and zero_wallet_status (remaining budget). The budget is a per-agent cap enforced per UTC calendar window against your organization's prepaid tool-spend balance.
agent researcher {
secrets: vendor_keys
model: "openai/gpt-4o-mini"
wallet: {
budget: 50
cadence: daily
maxPerCall: 2
}
}| Field | Type | Required | Description |
|---|---|---|---|
budget | number | Yes | USD cap for the cadence window. Must be positive. |
cadence | identifier | Yes | Window length: daily, weekly, or monthly. Write it bare: cadence: daily. |
maxPerCall | number | No | USD ceiling for a single paid call. Must not exceed budget. Defaults to a platform cap. |
There is no profile-level wallet override.
The agent node
A type: agent node invokes a top-level agent block from within a workflow. The workflow execution waits for the agent turn to complete, then continues with the agent's output.
node triage {
type: agent
label: "Triage ticket"
agent: support_agent
profile: escalation
prompt: @ts {
return "Investigate and resolve this ticket: " + context.nodes.root.output.description
}
schema: @json {
{
"type": "object",
"required": ["resolution", "category"],
"properties": {
"resolution": { "type": "string" },
"category": { "type": "string" }
},
"additionalProperties": false
}
}
}Field reference
| Field | Type | Required | Description |
|---|---|---|---|
agent | identifier | Yes | Name of a top-level agent block. |
prompt | @ts { } block | Yes | Returns the user message string. Must be a @ts block. |
profile | string | No | Name of a profile declared in the agent block. |
tools | identifier array | No | Narrows the effective tool set for this invocation. Must be a subset of the profile's tools (if a profile is active and declares tools) or the agent's tools. |
system | @ts { } block | No | Overrides the system prompt for this invocation only. |
schema | @json { } block | No | Structured output schema. The agent must return a validated object matching this shape. |
System prompt precedence
Three layers can contribute a system prompt. Later layers win:
- Agent
system(lowest priority) - Profile
system(overrides agent) - Node
system(overrides profile and agent)
Structured output
When a node declares schema, the runtime instructs the model to return a JSON object matching that schema. The validated object becomes the node's output, accessible as context.nodes.<nodeName>.output in downstream nodes.
Good to know: The field is
schema, notoutputSchema. WritingoutputSchema:on an agent node is a parse error.
Tools as workflows
Tools are workflows. There is no separate tool syntax in the DSL. Any workflow can become a tool if it meets three requirements:
- A non-empty
descriptionon the workflow block. The model reads this description to decide when to call the tool. - An
inputSchemaon the root node. This defines the function signature the model sees. - An output schema on every leaf node (
outputSchemaon the root,schemaon other leaf nodes). This types the return value.
workflow lookup_ticket {
label: "Look up ticket"
description: "Retrieve a support ticket by its ID. Returns the ticket title, description, and current status."
root {
type: code
label: "Entry"
inputSchema: @json {
{
"type": "object",
"required": ["ticket_id"],
"properties": { "ticket_id": { "type": "string" } },
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["ticket_id"],
"properties": { "ticket_id": { "type": "string" } },
"additionalProperties": false
}
}
code: @ts {
return { ticket_id: context.nodes.root.input.ticket_id }
}
}
node fetch {
type: postgres
label: "Fetch from DB"
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 agent lists this workflow in its tools array. When the model decides to look up a ticket, the runtime runs lookup_ticket as a checkpointed workflow execution and returns the output to the model.
How a turn runs
When an agent node executes, the runtime starts a turn:
- The
promptvalue becomes the user message. - The effective system prompt (from agent, profile, or node) becomes the system message.
- The model generates a response. If it calls a tool, the runtime runs the corresponding workflow.
- Tool output returns to the model for the next step.
- Steps repeat until the model returns a final response without tool calls, or
maxStepsis reached.
The default maxSteps is 20. Set it lower to limit cost, or higher for tasks that require many steps.
Every turn assembles one tool set from the agent's declarations: tool workflows, team members, skill tools, MCP tools, and Zero tools when a wallet is declared.
Built-in workspace tools are available when the agent declares a sandbox block or mounts disks: read, write, edit, bash, grep, find, ls. These operate on files inside the agent's sandbox. An agent that declares neither runs with its declared tools only. Tool workflows and built-in tools both count toward maxSteps.
Each turn is durably checkpointed. If the worker restarts mid-turn, the execution resumes from the last completed checkpoint. See How execution works.
Persistent chat
When agent chat is enabled for a hosted project, agents can run as multi-turn chat sessions. The platform stores the full conversation transcript.
In the Portal, the Agents page lists every deployed agent. Open any agent to chat in-app. No platform: web channel required. Sessions are keyed by agent name and stream to /chat/agent/:projectId/:agentName.
For a standalone chatbox link or SDK embed, declare a platform: web channel. Sessions there are keyed by channel name and use /chat/web/:projectId/:channelName.
From the CLI:
swirls chat start support_agentThis starts an interactive session with support_agent. Each message you send becomes the prompt for a new turn. The agent retains its sandbox state and conversation history across messages.
| Command | Description |
|---|---|
swirls chat start <agent> | Start a new chat session. |
swirls chat resume <session_id> | Resume a previous session. |
swirls chat list | List all sessions for the project. |
swirls chat show <session_id> | Print the transcript of a session. |
swirls chat send <session_id> <message> | Send a message to an existing session. |
Complete example
This example wires a form trigger to an agent that triages a ticket and posts a Slack notification.
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent triage_agent {
label: "Triage Agent"
description: "Classifies and routes support tickets."
provider: anthropic
model: "claude-3-5-sonnet-20241022"
secrets: llm_creds
maxSteps: 10
system: @ts {
return "You are a support triage agent. Classify the ticket and determine routing."
}
tools: [fetch_kb]
profile critical {
description: "For tickets marked urgent"
system: @ts {
return "You are handling a critical ticket. Escalate quickly. Be concise."
}
tools: [fetch_kb]
}
}
workflow fetch_kb {
label: "Search knowledge base"
description: "Search the knowledge base for relevant articles. Pass a keyword query."
root {
type: code
label: "Entry"
inputSchema: @json {
{
"type": "object",
"required": ["query"],
"properties": { "query": { "type": "string" } },
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["articles"],
"properties": {
"articles": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": false
}
}
code: @ts {
return { articles: ["Article A", "Article B"] }
}
}
}
workflow triage_ticket {
label: "Triage Ticket"
root {
type: code
label: "Entry"
inputSchema: @json {
{
"type": "object",
"required": ["subject", "body", "priority"],
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" },
"priority": { "type": "string" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["subject", "body", "priority"],
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" },
"priority": { "type": "string" }
},
"additionalProperties": false
}
}
code: @ts {
return {
subject: context.nodes.root.input.subject,
body: context.nodes.root.input.body,
priority: context.nodes.root.input.priority
}
}
}
node run_agent {
type: agent
label: "Triage with agent"
agent: triage_agent
profile: critical
prompt: @ts {
const { subject, body } = context.nodes.root.output
return `Triage this ticket.\nSubject: ${subject}\nBody: ${body}`
}
schema: @json {
{
"type": "object",
"required": ["category", "suggested_response"],
"properties": {
"category": { "type": "string" },
"suggested_response": { "type": "string" }
},
"additionalProperties": false
}
}
}
flow {
root -> run_agent
}
}Common mistakes
outputSchema on an agent node
The field is schema, not outputSchema. Using outputSchema on a type: agent node is a parse error.
// Incorrect
node triage {
type: agent
agent: triage_agent
prompt: @ts { return "Triage this ticket" }
outputSchema: @json { { "type": "object" } }
}// Correct
node triage {
type: agent
agent: triage_agent
prompt: @ts { return "Triage this ticket" }
schema: @json { { "type": "object" } }
}secrets: as a quoted string
The secrets field on an agent block takes a bare identifier, not a quoted string. A quoted value silently fails to resolve.
// Incorrect
agent bad_agent {
model: "gpt-4o"
secrets: "llm_creds"
}// Correct
agent good_agent {
model: "gpt-4o"
secrets: llm_creds
}Provider and secret key mismatch
Each provider resolves a specific key name. If the variable in your secret block does not match the expected name, the agent fails at runtime.
// Incorrect: provider is anthropic but secret declares the wrong key
secret bad_creds {
vars: [OPENROUTER_API_KEY]
}
agent bad_agent {
provider: anthropic
model: "claude-3-5-sonnet-20241022"
secrets: bad_creds
}// Correct
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent good_agent {
provider: anthropic
model: "claude-3-5-sonnet-20241022"
secrets: llm_creds
}system as a plain string
The system field on an agent block or agent node must be a @ts { } block. A plain string is rejected.
// Incorrect
agent bad_agent {
model: "gpt-4o"
secrets: llm_creds
system: "You are a helpful assistant."
}// Correct
agent good_agent {
model: "gpt-4o"
secrets: llm_creds
system: @ts {
return "You are a helpful assistant."
}
}Profile tools not a subset of agent tools
A profile's tools must be a subset of the agent's tools. Listing a workflow in a profile that is not in the agent's tools is a validation error.
// Incorrect: post_reply is not in the agent's tools list
agent bad_agent {
model: "gpt-4o"
secrets: llm_creds
tools: [lookup_ticket]
profile escalation {
tools: [lookup_ticket, post_reply]
}
}// Correct
agent good_agent {
model: "gpt-4o"
secrets: llm_creds
tools: [lookup_ticket, post_reply]
profile escalation {
tools: [lookup_ticket, post_reply]
}
}Tool workflow missing description, inputSchema, or leaf schema
A workflow used as a tool must declare a non-empty description, an inputSchema on its root node, and an output schema on every leaf node. Missing any of these causes the tool to be silently unusable.
See Tools as workflows for the full contract.
Expecting workspace tools without a sandbox
The built-in workspace tools (read, write, edit, bash, grep, find, ls) register only when the agent declares a sandbox block or mounts disks. An agent with neither gets its declared tools only, and file or shell instructions in the prompt go nowhere.
// Workspace tools available: sandbox declared
agent builder {
model: "gpt-4o"
secrets: llm_creds
sandbox: {
cpus: 1
}
}Further reading
- Skills: Package knowledge from your repo and let the agent read it on demand.
- MCP servers: Give the agent tools from remote MCP servers, bound per project in Swirls Cloud.
- Channels: Expose an agent as a chat assistant on Slack, Linear, Discord, or the web.
- Workflows: DAG structure, edges, and subgraphs.
- Node types: All 20 node types including the
agentnode. - Secrets and auth:
secretblocks and provider key setup. - How execution works: Checkpointing, durable execution, and the agent turn loop.
- Common mistakes: Parser errors and silent drops.