Agents
Declare stateful LLM agents with tools, roles, and sandboxes, then invoke them from any workflow.
An agent block defines a persistent LLM worker. It has a model, a secret, an optional set of tool workflows, and optional roles. Hosted projects can invoke the agent from a workflow using a type: agent node, or expose it as an interactive chat session when agent chat is enabled for the project.
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 and built-in workspace tools. |
| Workspace | None | 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
temperature: 0.3
maxTokens: 2048
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 | Sampling temperature. |
maxTokens | number | No | Max tokens per turn. |
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]. |
team | identifier array | No | Other agent blocks this agent may delegate to as subagents, e.g. [researcher, writer]. See Subagent teams. |
sandbox | sandbox: { } block | No | Resource and idle-lifecycle overrides for the agent's workspace. See The sandbox 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. |
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, those become the effective tool set. When it declares system, that overrides the agent's system prompt.
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.
The sandbox block
Every agent can run code and manipulate files in a persistent Linux workspace. The sandbox: { } block controls its resources and idle lifecycle.
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.
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.
Built-in workspace tools are available in every turn: read, write, edit, bash, grep, find, ls. These operate on files inside the agent's sandbox. 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.
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.
Further reading
- Channels: Expose an agent as a chat assistant on Slack, Linear, Discord, or the web.
- Workflows: DAG structure, edges, and subgraphs.
- Node types: All 16 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.