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

▓ agent.yaml
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: 4096

Before 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

FieldRequiredMeaning
versionyesFormat version. Only "1" exists. Keep the quotes: an unquoted 1 is a number and fails.
agentfor the CLIagent.name: the agent's name, unique in your workspace.
modelyesWhich model provider and model the agent uses.
pluginsnoThe runtime plugins: tools, prompts, memory, guards.
runtimenoLimits, and how sessions follow new versions.
channelsnoChannel 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.

providerAPI it speaksProvider key it uses
anthropicAnthropic Messagesanthropic
openaiOpenAI Chat Completionsopenai
openai-responsesOpenAI Responsesopenai
openai-compatibleOpenAI Chat Completions at config.baseUrlconfig.credentialProvider (default: the provider name)
anthropic-compatibleAnthropic Messages at config.baseUrlconfig.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

KeyMeaning
baseUrlEndpoint base URL. Required for openai-compatible and anthropic-compatible; for the other providers it replaces the default URL. baseURL is accepted too.
credentialProviderName of the workspace provider key to use, for example openrouter or deepseek: lowercase letters, digits and dashes.
apiKeyA 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.
headersExtra HTTP headers sent with every model request: a map of header names to strings. Values can be ${NAME} references.
parameterDialectHow 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).
thinkingModeClaude 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.
requestMetadatatrue 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, documentstrue or false: whether the model accepts images (vision) and PDF files (documents). When they are not set, PAREL guesses from the model id.

Examples

▓ anthropic
model:
  provider: anthropic
  model: claude-sonnet-5
▓ deepseek (openai-compatible)
model:
  provider: openai-compatible
  model: deepseek-v4.1-flash
  config:
    baseUrl: https://api.deepseek.com/v1
    credentialProvider: deepseek

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

▓ deepseek, key kept with this agent
model:
  provider: openai-compatible
  model: deepseek-v4.1-flash
  config:
    baseUrl: https://api.deepseek.com/v1
    apiKey: ${DEEPSEEK_API_KEY}
▓ openrouter (openai-compatible)
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-bot

Connect 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:

▓ agent.yaml
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: 2000

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

▓ agent.yaml
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.

PluginKindNeedsWhat it does
system-staticPrompt-Adds the text in prompt to the system prompt.
memory-rolling-summaryMemory-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-capGuard-Stops model calls once the session's cost reaches max_usd (default 5) or its turns go past max_turns.
security-basicGuard-Blocks destructive shell commands, lets through only allowed programs (mode: allowlist by default; allowed_patterns, exec_tools), and hides secrets in tool output.
steering-immediateControl-Stops the running turn when an interrupt input arrives (POST /sessions/ID/inputs with type interrupt).
subagentTools-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-e2bSandboxSecret apiKey (your E2B API key)A cloud VM from E2B with shell, files, background processes and ports. Optional settings: template, timeout, commandTimeout.
workspaceWorkspacesandbox-e2bClones 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-toolsToolssandbox-e2b and workspaceTools that work in the workspace: read and write files, search, edit and patch, git, shell commands, background processes.
port-toolsToolssandbox-e2bExposes sandbox ports, and lists or revokes them.
approval-toolsTools-request_approval and check_approval tools for asking a person to sign off.
coding-agentProfile-A coding-agent system prompt and tool-use guidance. Pair it with sandbox-e2b, workspace and the tool plugins.
Not in the table: 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

FieldTypeDefaultMeaning
maxStepsinteger ≥ 0200Steps per turn (a step is one model call plus the tool calls it makes). When reached, the turn ends with a turn_limit error.
maxTurnsinteger ≥ 0no limitTurns per session. When reached, the session is marked completed and new messages are refused.
maxParallelToolCallsinteger ≥ 18How many tool calls from one step may run at the same time. Only tools that allow parallel runs run together.
toolResultMaxBytesinteger ≥ 165536A tool result longer than this is cut before the model sees it.
instanceBudgetUsdnumber > 0no limitSpend 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.
deploymentTrackinglive | pinnedlivelive: 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.enabledbooleanfalseAsk the model to reason before it answers, when the provider supports it.
reasoning.budgetTokensinteger ≥ 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.

▓ agent.yaml
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.

▓ shell
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.

A reference is a whole-value match: ${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.

▓ agent.yaml
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.

▓ shell
# 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.

▓ agent.yaml
channels:
  - type: managed_ws
    plugin: "@parel/channel-slack-socket"
    config:
      appToken: ${SLACK_APP_TOKEN}
      botToken: ${SLACK_BOT_TOKEN}
    routing:
      mode: per_subject
FieldMeaning
typeRequired. webhook (the service calls PAREL) or managed_ws (PAREL keeps a WebSocket open to the service).
pluginRequired. The connector package: @parel/channel-slack-socket (managed_ws; appToken, botToken) or @parel/channel-telegram (webhook; botToken, webhookSecret).
versionSemver range for the connector, fixed at deploy.
nameTells two entries with the same type and plugin apart.
configConnector settings. Top-level values can be ${NAME} references to workspace secrets.
startmanaged_ws only: open the connection at deploy. Default true.
routing.modeHow conversations map to sessions: main (the default), per_subject, per_actor or isolated.
routing.injectInFlighttrue: a message for the same conversation that arrives during a turn joins that turn at its next step. Default false.
instanceThe instance the conversations run in. Default main.
observeAgent events to push to the connector: turn, steps, pause. Default none.
childSessionstrue: 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.