SWIRLS_
Language

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: aitype: agent
Use forSingle-shot generation, classification, structured outputMulti-step reasoning, file manipulation, tool calls
Tool loopNoYes. Calls tool workflows and built-in workspace tools.
WorkspaceNonePersistent Linux sandbox
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-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

FieldTypeRequiredDescription
modelstringYesModel identifier, e.g. "claude-3-5-sonnet-20241022", "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 Portal.
system@ts { } blockNoReturns the system prompt string. Must be a @ts block. A plain string is rejected.
temperaturenumberNoSampling temperature.
maxTokensnumberNoMax tokens per turn.
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].
teamidentifier arrayNoOther agent blocks this agent may delegate to as subagents, e.g. [researcher, writer]. See Subagent teams.
sandboxsandbox: { } blockNoResource 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:

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 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> { }.

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.
sandboxsandbox: { } blockNoOverrides 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 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.

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

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, 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

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.

Good to know: 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. 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). 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:

  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.

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_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>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

On this page