Agents
Declare stateful LLM agents with tools, profiles, and sandboxes, then invoke them from any workflow.
An agent block defines a persistent LLM worker with a model, credentials, and optional tools, skills, MCP servers, profiles, and subagents. Use agents for conversation, tasks that need tool calls, or judgment within a workflow.
Invoke an agent from a workflow with a type: agent node, chat with it in Swirls Cloud or from the CLI, or expose it through a channel. Agents can use workflows as tools, read skills, call MCP servers, and work with files on disks. Access roles control who may invoke them.
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 | Linux workspace with a persistent dedicated disk |
| 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-sonnet-4-6"
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-sonnet-4-6", "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 dashboard. |
system | @ts { } block | No | Returns the system prompt string. Must be a @ts block. A plain string is rejected. |
temperature | number | No | Sampling temperature applied to every model call in the turn, on models that support the parameter. Must be between 0 and 1 for provider: anthropic, and between 0 and 2 for the others. Defaults to the provider's own value when unset. Newer Anthropic and OpenAI models have removed sampling parameters from their APIs, and Swirls omits the value there so the turn still runs. |
maxTokens | number | No | Caps the output tokens the model may produce per call. Must be a whole number of 1 or more. Defaults to 8,192 tokens. |
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 | Overrides workspace resources and idle lifecycle. See The sandbox block. |
wallet | wallet: { } block | No | Per-agent tool-spend budget that enables the hosted Zero tools. See The wallet block. |
context | context: { } block | No | Overrides the platform's managed context budget, compaction, pruning, and cache settings for this agent. See Managed context. |
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 secret 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. List them in
skills:; the agent reads the files on demand at runtime. - MCP servers connect the agent to remote tools. List slots in
mcp:; Bind or Connect credentials per project in Swirls Cloud.
To expose your own capabilities to an external assistant, use an MCP channel. It can offer a conversation with this agent, direct calls to selected workflows, or both.
The sandbox block
Cloud provisions a dedicated persistent disk for each deployed agent. That mount enables the built-in workspace tools. The optional sandbox: { } block controls the Linux workspace's compute resources and idle lifecycle; you do not need it to enable file tools.
agent data_agent {
provider: openai
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, scratch workspace state is discarded on stop. Mounted persistent disks retain their files. |
The sandbox starts lazily on the first workspace tool call. Scratch files at /srv/workspace last for the sandbox's lifetime. Store files that must survive sandbox deletion on the dedicated disk at /mnt/agent, or on shared disks 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.
Managed context
Swirls manages session context in Cloud chat, channels, the CLI, and embedded chat. It limits the prompt sent to the model using token budgets, pruning, and compaction. Defaults apply unless you override them in a context block.
Context management uses four operations:
- Budgeting. Each turn uses a platform token budget with space reserved for responses and tool results. The default budget is 120,000 tokens, derived from an assumed 200,000-token window. Swirls does not look up each model's actual window; declare a smaller
budgetfor a model with less context. Hosting configuration can change the default. - Pruning. Once a session's context fills past a threshold, results from older tool calls are replaced with a short reference the agent can re-fetch if it turns out to matter again. Tool-call records remain in the context after their older outputs are pruned.
- Compaction. If a session keeps growing past that, the platform runs a dedicated summarization pass that folds the older part of the conversation into a structured summary: goals, decisions, what has been tried and its results, and what is still pending. The summary replaces the raw history behind it, the system prompt and the most recent turns stay verbatim, and the compaction appears in the session's timeline. If a request still overflows the model's window despite all of this, the session compacts and retries once before surfacing an error.
- Caching. The platform formats prompts for the provider's cache so later turns can reuse cached input. This applies to OpenRouter and direct Anthropic or OpenAI connections.
An agent can also pin a short fact so it survives every compaction verbatim, for the life of the conversation. This is useful for a detail a later turn must not lose, like which account to bill or a decision a person made earlier in the thread. The agent pins facts itself as it talks with you (up to twelve at a time, each up to 500 characters); there is no DSL for it.
Swirls Cloud shows a context meter for every session and a manual compact control, in case you want to free up room before the platform would do it on its own. Embedded chat gets the same controls through @swirls/sdk/context; see Embed agent chat.
The context block
Override the default context settings with a context: { } block:
agent support_agent {
provider: anthropic
model: "claude-sonnet-4-6"
secrets: llm_creds
context: {
budget: 120000
compact_at: 0.8
keep_recent_turns: 3
prune_tool_results_after: 5
cache_retention: long
}
}| Field | Type | Required | Description |
|---|---|---|---|
budget | number | No | Working-set token cap, overriding the platform default. Must be a positive whole number of tokens. |
compact_at | number | No | Fraction of the budget at which compaction runs. Must be greater than 0 and at most 1. Defaults to 0.8. |
keep_recent_turns | number | No | Recent user turns kept verbatim across a compaction. Must be a whole number of 0 or more. Defaults to 3. |
prune_tool_results_after | number | No | Number of most-recent tool uses whose results stay in full; older ones are replaced with a reference. Must be a whole number of 1 or more. Defaults to 5. |
cache_retention | identifier | No | Requested prompt-cache lifetime: none, short, or long. Write it bare, e.g. cache_retention: long. Defaults to short. |
Both context: { } and context { } (no colon) parse. An empty context: { } block is equivalent to omitting the field entirely: all settings use their defaults.
This is a different
contextthan thecontextobject available inside@tsblocks (context.nodes,context.iteration, and so on). The agent'scontext: { }block is a top-level declaration; the@tsobject is a runtime value.
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.
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 with a non-emptypropertiesobject. This defines the function signature the model sees. - An output schema on every leaf node (
outputSchemaon the root,schemaon other leaf nodes). Non-objectaileaves use their inferred output type instead.
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.
The built-in workspace tools (read, write, edit, bash, grep, find, ls) operate inside the agent's sandbox. Cloud agents receive them through their dedicated disk mount. maxSteps limits model steps, each of which may include multiple tool calls.
The surrounding execution is durable. An interrupted turn may be retried, including its model and tool calls; do not assume each individual model step has a durable 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 dashboard, 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 "<message>" | Send a message to an existing session. |
Complete example
This example defines a ticket-triage workflow and an agent with a stub knowledge-base tool. Invoke triage_ticket with a subject, body, and priority. The workflow always selects the critical profile; it does not branch on priority.
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent triage_agent {
label: "Triage Agent"
description: "Classifies and routes support tickets."
provider: anthropic
model: "claude-sonnet-4-6"
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: "openai/gpt-4o"
secrets: "llm_creds"
}// Correct
agent good_agent {
model: "openai/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-sonnet-4-6"
secrets: bad_creds
}// Correct
secret llm_creds {
vars: [ANTHROPIC_API_KEY]
}
agent good_agent {
provider: anthropic
model: "claude-sonnet-4-6"
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: "openai/gpt-4o"
secrets: llm_creds
system: "You are a helpful assistant."
}// Correct
agent good_agent {
model: "openai/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: "openai/gpt-4o"
secrets: llm_creds
tools: [lookup_ticket]
profile escalation {
tools: [lookup_ticket, post_reply]
}
}// Correct
agent good_agent {
model: "openai/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. The validator rejects workflows that do not satisfy the tool contract.
See Tools as workflows for the full contract.
Treating scratch files as durable storage
Files at /srv/workspace belong to the sandbox and can disappear when it is deleted. Use /mnt/agent or a declared shared disk for files that must survive the sandbox lifecycle.
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; Bind or Connect credentials 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 19 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.
Choose a model for a workflow invocation
A type: agent workflow node can override its declaration with model: "id", inline model: @ts { ... }, or a file-backed @ts returning a nonempty string. Selection is checkpointed before inference and applies to context preparation and the full tool loop for that invocation. It does not change profiles, subagents, tools, secret binding, or chat/channel defaults. Decisions with Jev shows how typed probabilities can drive that mapping. Workflow-backed agent tools can also invoke a decision; its output schema is inferred.