Contents
agent.yaml Reference
agent.yaml describes one agent: its model, its plugins, its limits and, if you want, its channels. Deploy it with parel deploy or from the console. The server checks the file on every deploy and answers 400 with the reason when something doesn't parse.
# Full example
version: "1"
agent:
name: research-bot
model:
provider: anthropic
model: claude-sonnet-5
plugins:
- system-static:
prompt: "You are a research assistant for ${var:CUSTOMER:-our team}."
- sandbox-e2b:
apiKey: ${E2B_API_KEY}
- memory-rolling-summary
- budget-cap:
max_usd: 25
runtime:
maxSteps: 100
maxTurns: 200
maxParallelToolCalls: 4
reasoning:
enabled: true
budgetTokens: 4096Before you deploy it, store an Anthropic key with parel provider-keys set anthropic --from-env ANTHROPIC_API_KEY (or under Settings → Model providers), and have E2B_API_KEY in your shell or stored with parel secrets set E2B_API_KEY. Because the file has a sandbox plugin but no security plugin, security-basic is added for you.
# Top-level fields
| Field | Required | Meaning |
|---|---|---|
| version | yes | Format version. Only "1" exists. Keep the quotes: an unquoted 1 is a number and fails. |
| agent | for the CLI | agent.name: the agent's name, unique in your workspace. |
| model | yes | Which model provider and model the agent uses. |
| plugins | no | The runtime plugins: tools, prompts, memory, guards. |
| runtime | no | Limits, and how sessions follow new versions. |
| channels | no | Channel connections set up when the version goes live. |
Unknown keys at the top level and under runtime are dropped without an error, so check the spelling. Inside agent, model and each channel entry, unknown keys are rejected.
# agent
name is how the CLI finds the agent: parel deploy and parel run deploy to the agent with that name and create it the first time. Deploys through the API (POST /agents/NAME/versions) take the name from the URL instead. parel agents rename changes the name and keeps the agent's id, versions and sessions.
# model
provider and model are required; model is the model id the provider expects. config is optional.
| provider | API it speaks | Provider key it uses |
|---|---|---|
| anthropic | Anthropic Messages | anthropic |
| openai | OpenAI Chat Completions | openai |
| openai-responses | OpenAI Responses | openai |
| openai-compatible | OpenAI Chat Completions at config.baseUrl | config.credentialProvider (default: the provider name) |
| anthropic-compatible | Anthropic Messages at config.baseUrl | config.credentialProvider (default: the provider name) |
PAREL doesn't supply model keys. A model call uses model.config.apiKey when it is set, otherwise the workspace's provider key for the credential provider in the table, which you add under Settings → Model providers or with parel provider-keys set. With neither, the turn fails with a missing-key error.
model.config
| Key | Meaning |
|---|---|
| baseUrl | Endpoint base URL. Required for openai-compatible and anthropic-compatible; for the other providers it replaces the default URL. baseURL is accepted too. |
| credentialProvider | Name of the workspace provider key to use, for example openrouter or deepseek: lowercase letters, digits and dashes. |
| apiKey | A key for this agent only, written as a ${NAME} secret reference. It wins over the workspace provider key. Never write the key itself here: it would be stored in plain text. |
| headers | Extra HTTP headers sent with every model request: a map of header names to strings. Values can be ${NAME} references. |
| parameterDialect | How max tokens and reasoning are sent on Chat Completions APIs: openai (max_completion_tokens and reasoning_effort; the default for openai), openrouter (max_tokens and reasoning.max_tokens; the default for openai-compatible) or compatible (max_tokens only; turning on reasoning is then an error). |
| thinkingMode | Claude thinking: adaptive, enabled or disabled. By default, Claude Opus and Sonnet 4.6 and later use adaptive, and other models use enabled with the reasoning budget. |
| requestMetadata | true by default: each model request carries X-Parel-Call-Id, X-Parel-Attempt, X-Parel-Session-Id, X-Parel-Turn-Id and X-Parel-Step headers. Set it to false to leave them out. |
| vision, documents | true or false: whether the model accepts images (vision) and PDF files (documents). When they are not set, PAREL guesses from the model id. |
Examples
model: provider: anthropic model: claude-sonnet-5
model:
provider: openai-compatible
model: deepseek-v4.1-flash
config:
baseUrl: https://api.deepseek.com/v1
credentialProvider: deepseekStore the DeepSeek key once for the workspace with parel provider-keys set deepseek --from-env DEEPSEEK_API_KEY. Or keep the key with this one agent: make apiKey a secret reference, and parel deploy uploads the value from your shell.
model:
provider: openai-compatible
model: deepseek-v4.1-flash
config:
baseUrl: https://api.deepseek.com/v1
apiKey: ${DEEPSEEK_API_KEY}model:
provider: openai-compatible
model: anthropic/claude-sonnet-5
config:
baseUrl: https://openrouter.ai/api/v1
credentialProvider: openrouter
parameterDialect: openrouter
headers:
HTTP-Referer: https://example.com
X-Title: research-botConnect OpenRouter under Settings → Model providers, or run parel provider-keys set openrouter --from-env OPENROUTER_API_KEY. The headers are optional; OpenRouter uses them to show your app's name.
# plugins
A list of plugins. Each entry takes one of three forms:
plugins:
# 1. Name only, no config
- memory-rolling-summary
# 2. Name with config
- budget-cap:
max_usd: 25
max_turns: 50
# 3. Full form: any public npm package, optional semver range
- plugin: "@parel/subagent"
version: "^0.4.0"
config:
mode: sync
maxTokens: 2000A short name means a first-party package: sandbox-e2b is @parel/sandbox-e2b. A name that starts with @ is used as written, so any public npm package works, including your own.
version (full form only) is a semver range. It is resolved to one exact version when you deploy and stays fixed; deploy again to pick up newer releases. Without it you get the latest release at deploy time.
An entry without config must be the bare name. A name followed by a colon and nothing else (- memory-rolling-summary:) fails validation.
If the list has a sandbox plugin but no security plugin, security-basic is added for you. Model providers can't be listed here; use model.
Local plugins
parel deploy and parel run can also load a plugin from a directory. The path starts with ./, ../ or / and is relative to the agent.yaml file. The directory needs a package.json with a name and version, and a parel.plugin.json. The CLI packs it with pnpm pack (or npm pack), uploads it, and stores the entry in the third form below. Deploys through the HTTP API can't use local paths. Writing Plugins has a complete example.
plugins:
# Any one of these three forms:
- ./plugins/hello
- plugin: ./plugins/hello
config:
greeting: Hi
- plugin: "@acme/hello"
source:
type: path
path: ./plugins/hello# Available plugins
First-party plugins that run on hosted PAREL. All of them work with their short names.
| Plugin | Kind | Needs | What it does |
|---|---|---|---|
| system-static | Prompt | - | Adds the text in prompt to the system prompt. |
| memory-rolling-summary | Memory | - | Folds older messages into a running summary as the context fills up (compact_at, keep_recent_messages, max_context_tokens). Adds the /compact command. The stored conversation is never changed. |
| budget-cap | Guard | - | Stops model calls once the session's cost reaches max_usd (default 5) or its turns go past max_turns. |
| security-basic | Guard | - | Blocks destructive shell commands, lets through only allowed programs (mode: allowlist by default; allowed_patterns, exec_tools), and hides secrets in tool output. |
| steering-immediate | Control | - | Stops the running turn when an interrupt input arrives (POST /sessions/ID/inputs with type interrupt). |
| subagent | Tools | - | Adds a subagent tool. sync (the default) runs one inline model call. async starts a child session: a fork of this conversation, or a fresh session of the agent named in agent (by name or id). Settings: mode, agent, instructions, maxTokens. |
| sandbox-e2b | Sandbox | Secret apiKey (your E2B API key) | A cloud VM from E2B with shell, files, background processes and ports. Optional settings: template, timeout, commandTimeout. |
| workspace | Workspace | sandbox-e2b | Clones a public git repository into the sandbox (identity, baseDir) and gives the tool plugins a workspace to work in. |
| filesystem-tools, search-tools, edit-tools, git-tools, shell-tools, process-tools | Tools | sandbox-e2b and workspace | Tools that work in the workspace: read and write files, search, edit and patch, git, shell commands, background processes. |
| port-tools | Tools | sandbox-e2b | Exposes sandbox ports, and lists or revokes them. |
| approval-tools | Tools | - | request_approval and check_approval tools for asking a person to sign off. |
| coding-agent | Profile | - | A coding-agent system prompt and tool-use guidance. Pair it with sandbox-e2b, workspace and the tool plugins. |
sandbox-cloudflare can't run on PAREL today (parel-oss issue #38). sandbox-daytona, sandbox-modal and sandbox-vercel are published, but their SDKs rely on Node.js libraries and they haven't been verified on the hosted runtime, so don't depend on them yet. channel-slack-socket and channel-telegram are channel connectors: use them under channels, not plugins.Every plugin's source and README is in parel-oss/js/plugins.
# runtime
| Field | Type | Default | Meaning |
|---|---|---|---|
| maxSteps | integer ≥ 0 | 200 | Steps per turn (a step is one model call plus the tool calls it makes). When reached, the turn ends with a turn_limit error. |
| maxTurns | integer ≥ 0 | no limit | Turns per session. When reached, the session is marked completed and new messages are refused. |
| maxParallelToolCalls | integer ≥ 1 | 8 | How many tool calls from one step may run at the same time. Only tools that allow parallel runs run together. |
| toolResultMaxBytes | integer ≥ 1 | 65536 | A tool result longer than this is cut before the model sees it. |
| instanceBudgetUsd | number > 0 | no limit | Spend ceiling in US dollars across all sessions of one instance. Once reached, new turns are refused until the ceiling is raised. It is checked when a turn starts, so one turn can go over. |
| deploymentTracking | live | pinned | live | live: sessions switch to a newly live version at their next turn. pinned: a session keeps the version it started with. Forks, branches and replays are always pinned. |
| reasoning.enabled | boolean | false | Ask the model to reason before it answers, when the provider supports it. |
| reasoning.budgetTokens | integer ≥ 1 | - | Reasoning budget in tokens. Claude with thinkingMode enabled uses it as the thinking budget (at least 1024, default 4096). Adaptive Claude and parameterDialect openai turn it into an effort level (up to 4096 low, up to 8192 medium, more high). parameterDialect openrouter sends it as reasoning.max_tokens. |
The number and boolean fields above (not deploymentTracking) can also hold a whole-value instance var, such as maxSteps: ${var:MAX_STEPS:-100}; the value is filled in and checked per instance at each turn. Other keys under runtime are ignored.
# Secrets
Never put secret values in agent.yaml. Write a reference instead: ${NAME} as the whole value of a field, where NAME starts with an uppercase letter followed by uppercase letters, digits or underscores. References work anywhere inside model.config and inside a plugin's config (nested values too), and as top-level values of a channel's config. The file, the stored config and API responses only ever contain the reference; the value is filled in when a session starts.
model:
provider: openai-compatible
model: deepseek-v4.1-flash
config:
baseUrl: https://api.deepseek.com/v1
apiKey: ${DEEPSEEK_API_KEY}
plugins:
- sandbox-e2b:
apiKey: ${E2B_API_KEY}To provide the values, export them and deploy: parel deploy uploads every referenced name it finds in your shell, encrypted and stored for that agent. Or store a value for the whole workspace with parel secrets set or under Settings → Secrets. An agent's own value wins over the workspace value.
export E2B_API_KEY=e2b_your_key export DEEPSEEK_API_KEY=sk-your_key parel deploy agent.yaml # uploads both, stored for this agent # or store them for the whole workspace once: parel secrets set E2B_API_KEY
The deploy checks every reference. If a name has no value in the upload, in the agent's secrets or in the workspace's secrets, it fails with 400 and lists what to set. Plugins mark their secret fields in their manifest (for example apiKey of sandbox-e2b). Such a field must be a reference, so a plain value there, or a required field left out, also fails the deploy.
${NAME} must be the field's entire value. Text around it is not filled in, so https://${HOST}/x stays exactly as written. A secret field may instead hold an instance var, such as ${var:KEY_REF} whose value on each instance is a reference like ${E2B_KEY_ACME}, but not one with a :-default.# Instance vars
One config can serve many instances that differ in a few values. Write ${var:NAME}, or ${var:NAME:-default} with a fallback, anywhere in model (the model id too), in plugin configs, and as the whole value of a runtime number or boolean. Inside longer text, a reference is filled in where it stands. NAME starts with a letter or underscore, followed by letters, digits or underscores.
model:
provider: anthropic
model: ${var:MODEL:-claude-sonnet-5}
plugins:
- system-static:
prompt: "You support ${var:CUSTOMER:-our customers}. Answer in ${var:LANGUAGE:-English}."
runtime:
maxSteps: ${var:MAX_STEPS:-100}
reasoning:
enabled: ${var:REASONING:-false}A value comes from the first place that has one: the instance's own vars, then the agent's default vars (shared by all instances, and the only vars that parel try sees), then the :-default in the reference. Values are text; when a reference is a field's whole value, "true" becomes a boolean and "16384" a number. If no value is found, a plugin config keeps the reference text as it is, while in model or runtime the turn fails before the model is called.
# Per instance (replaces that instance's whole set)
parel instances vars research-bot customer-a --set CUSTOMER=Acme --set LANGUAGE=German
# Agent-level defaults for every instance (replaces the whole set; no CLI command yet)
curl -X PATCH https://api.parel.sh/agents/research-bot \
-H "Authorization: Bearer $PAREL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vars": {"LANGUAGE": "English"}}'Changes reach live sessions at their next turn; pinned sessions keep the values they started with. To see which instances would be missing a value before you make a version live, call POST /agents/NAME/deployments with the version and "dryRun": true.
Vars are for plain values, not secrets: they are stored and shown as written. A default can't contain braces or another reference; the deploy rejects one that does.
# channels
Connects the agent to outside services, such as a Slack app or a Telegram bot, when the version goes live (on deploy or promote). Each entry creates or updates one connection and binds it to this agent; deploying again updates the same connection instead of making a new one.
channels:
- type: managed_ws
plugin: "@parel/channel-slack-socket"
config:
appToken: ${SLACK_APP_TOKEN}
botToken: ${SLACK_BOT_TOKEN}
routing:
mode: per_subject| Field | Meaning |
|---|---|
| type | Required. webhook (the service calls PAREL) or managed_ws (PAREL keeps a WebSocket open to the service). |
| plugin | Required. The connector package: @parel/channel-slack-socket (managed_ws; appToken, botToken) or @parel/channel-telegram (webhook; botToken, webhookSecret). |
| version | Semver range for the connector, fixed at deploy. |
| name | Tells two entries with the same type and plugin apart. |
| config | Connector settings. Top-level values can be ${NAME} references to workspace secrets. |
| start | managed_ws only: open the connection at deploy. Default true. |
| routing.mode | How conversations map to sessions: main (the default), per_subject, per_actor or isolated. |
| routing.injectInFlight | true: a message for the same conversation that arrives during a turn joins that turn at its next step. Default false. |
| instance | The instance the conversations run in. Default main. |
| observe | Agent events to push to the connector: turn, steps, pause. Default none. |
| childSessions | true: the connector may start child sessions that fork the main conversation. Needs routing.mode main. Default false. |
Channel secrets must already be stored for the workspace: run parel secrets set SLACK_APP_TOKEN (and the others) before deploying, because parel deploy doesn't upload values for channels. A webhook connection also needs config.webhookSecret set to a reference to a workspace secret; it is used to check incoming requests.
If a channel can't be set up, the deploy still succeeds; the result for each channel is in the deploy answer (parel deploy --json). Every deploy applies the declared settings again, replacing changes made to the binding through the API in the meantime.