Agents

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: aitype: agent
Use forSingle-shot generation, classification, structured outputMulti-step reasoning, file manipulation, tool calls
Tool loopNoYes. Calls tool workflows, subagents, skills, and MCP tools.
WorkspaceNoneLinux workspace with a persistent dedicated disk
ConfigurationInline on the nodeTop-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

FieldTypeRequiredDescription
modelstringYesModel identifier, e.g. "claude-sonnet-4-6", "gpt-4o".
secretsidentifierYesName of a top-level secret block. Must declare the provider's API key variable. Not a quoted string.
providerstringNoOne of: openrouter (default), anthropic, openai, google.
labelstringNoDisplay name.
descriptionstringNoDescription shown in the dashboard.
system@ts { } blockNoReturns the system prompt string. Must be a @ts block. A plain string is rejected.
temperaturenumberNoSampling 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.
maxTokensnumberNoCaps the output tokens the model may produce per call. Must be a whole number of 1 or more. Defaults to 8,192 tokens.
maxStepsnumberNoMax tool-call steps per turn. Defaults to 20 when unset.
toolsidentifier arrayNoWorkflows this agent may call as tools, e.g. [search_kb, file_writer].
skillsidentifier arrayNoTop-level skill blocks the agent can read at runtime, e.g. [swirls_lang]. See Skills.
mcpidentifier arrayNoTop-level mcp blocks naming remote MCP server slots the agent draws tools from. See MCP servers.
teamidentifier arrayNoOther agent blocks this agent may delegate to as subagents, e.g. [researcher, writer]. See Subagent teams.
disksidentifier arrayNoTop-level disk blocks mounted into the agent's workspace at /mnt/disks/<name>. See Disks.
sandboxsandbox: { } blockNoOverrides workspace resources and idle lifecycle. See The sandbox block.
walletwallet: { } blockNoPer-agent tool-spend budget that enables the hosted Zero tools. See The wallet block.
contextcontext: { } blockNoOverrides 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:

providerRequired variable
openrouter (default)OPENROUTER_API_KEY
anthropicANTHROPIC_API_KEY
openaiOPENAI_API_KEY
googleGOOGLE_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> { }.

FieldTypeRequiredDescription
descriptionstringNoHuman-readable description of the profile.
system@ts { } blockNoOverrides the agent-level system prompt when this profile is active.
toolsidentifier arrayNoSubset of the agent's tools available in this profile. Must not include workflows not already listed in the agent's tools.
skillsidentifier arrayNoSubset of the agent's skills available in this profile.
mcpidentifier arrayNoSubset of the agent's mcp slots available in this profile.
sandboxsandbox: { } blockNoOverrides 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 agent block 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 -> a is a validation error, and so is any longer loop.
  • Team members resolve their own skills and mcp lists. 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

FieldTypeMinimumDescription
cpusnumber1CPU cores allocated to the sandbox.
memoryMiBnumber128RAM in mebibytes.
diskGiBnumber1Disk space in gibibytes.

Lifecycle fields

FieldTypeDescription
autoStopMinutesnumberStop the sandbox after this many idle minutes. Set to 0 to disable auto-stop.
autoArchiveMinutesnumberArchive the sandbox this many minutes after it stops. Set to -1 to disable.
autoDeleteMinutesnumberDelete the sandbox this many minutes after it stops. Set to -1 to disable.
ephemeralbooleanWhen 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
  }
}
FieldTypeRequiredDescription
budgetnumberYesUSD cap for the cadence window. Must be positive.
cadenceidentifierYesWindow length: daily, weekly, or monthly. Write it bare: cadence: daily.
maxPerCallnumberNoUSD 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 budget for 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
  }
}
FieldTypeRequiredDescription
budgetnumberNoWorking-set token cap, overriding the platform default. Must be a positive whole number of tokens.
compact_atnumberNoFraction of the budget at which compaction runs. Must be greater than 0 and at most 1. Defaults to 0.8.
keep_recent_turnsnumberNoRecent user turns kept verbatim across a compaction. Must be a whole number of 0 or more. Defaults to 3.
prune_tool_results_afternumberNoNumber 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_retentionidentifierNoRequested 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 context than the context object available inside @ts blocks (context.nodes, context.iteration, and so on). The agent's context: { } block is a top-level declaration; the @ts object 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

FieldTypeRequiredDescription
agentidentifierYesName of a top-level agent block.
prompt@ts { } blockYesReturns the user message string. Must be a @ts block.
profilestringNoName of a profile declared in the agent block.
toolsidentifier arrayNoNarrows 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 { } blockNoOverrides the system prompt for this invocation only.
schema@json { } blockNoStructured 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:

  1. Agent system (lowest priority)
  2. Profile system (overrides agent)
  3. 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, not outputSchema. Writing outputSchema: 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:

  1. A non-empty description on the workflow block. The model reads this description to decide when to call the tool.
  2. An inputSchema on the root node with a non-empty properties object. This defines the function signature the model sees.
  3. An output schema on every leaf node (outputSchema on the root, schema on other leaf nodes). Non-object ai leaves 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:

  1. The prompt value becomes the user message.
  2. The effective system prompt (from agent, profile, or node) becomes the system message.
  3. The model generates a response. If it calls a tool, the runtime runs the corresponding workflow.
  4. Tool output returns to the model for the next step.
  5. Steps repeat until the model returns a final response without tool calls, or maxSteps is 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_agent

This 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.

CommandDescription
swirls chat start <agent>Start a new chat session.
swirls chat resume <session_id>Resume a previous session.
swirls chat listList 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 agent node.
  • Secrets and auth: secret blocks 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.

On this page