Agents
An agent is a prompt with a name. Nothing else is required, and an agent with nothing else is the normal case, not a degraded one.
An agent is a Runnable of kind agent, so everything on
Declaration collections applies to it — versions, --if-exists,
the 409 semantics. This group exists because authoring a prompt through
declare and a JSON file is miserable.
There is no agent API in the SDK and no agent route on the control plane.
DreamLake stores declarations. dreamlake agents create writes a row; some
other system reads it and does the running.
Three ways to write the prompt
A heredoc
Quote the delimiter — <<'AGENT', not <<AGENT — or the shell expands
$VARIABLES and backticks inside your prose before the CLI ever sees it.
That file has no frontmatter and does not need any.
A Claude agent file
Add frontmatter when you want to constrain the agent or give it a machine.
--file <path> reads one; --file - reads stdin.
$EDITOR, on a scaffold
Opens a filled-in scaffold and declares whatever you save.
The frontmatter fields
The file format is Claude's agent file — the same bytes work in
.claude/agents/ — plus fields this platform needs. Every field is optional.
| Field | Type | Whose | Meaning |
|---|---|---|---|
name | string | Claude | the agent's name. The positional argument or --name wins over it |
description | string | Claude | one line on when to use this agent |
tools | list | Claude | e.g. [Read, Grep, Bash]. Omit to inherit all |
model | string | Claude | a model id (claude-opus-5) or an alias: opus, sonnet, haiku, inherit |
permissions.allow / ask / deny | list of rules | Claude | Tool or Tool(specifier), e.g. Read(./runs/**), Bash(npm run test:*) |
permissions.additionalDirectories | list of paths | Claude | extra roots the agent may read and write |
permissions.defaultMode | string | Claude | what happens to a call no rule matches: default, acceptEdits, plan, bypassPermissions |
run_config | string | Lakeshore | names an existing RunConfig — the machine it lands on |
queue | string | Lakeshore | the queue to submit on |
grants | list | Lakeshore | domain.resource.verb resource grants |
arguments | list | Lakeshore | typed prompt arguments — see below |
channel | mapping | Lakeshore | { kind, target? }, e.g. { kind: http-sse, target: /v1/agents/triage/stream } |
An arguments entry is
{ name, type, required?, default?, values?, description? }, where type is
one of string | integer | number | boolean | enum and values lists an
enum's fixed set. required: true and a default on the same argument are
rejected — they contradict each other.
That is the whole vocabulary. There is no thinking-level or effort field —
model is the only model-shaped knob, matching Claude's format — and no
placement field beyond run_config, because placement is derived from the
RunConfig's host key.
Three rules about keys the table does not list:
- An unknown key under
permissionsis refused. The merge is an allowlist, and an allowlist that silently drops apermissions.allowedtypo declares nothing and says nothing. - An unknown key at the top level is ignored, so a Claude agent file carrying fields this platform does not read still declares cleanly.
- A credential-shaped key anywhere —
token,secret,api_key,authorization, … — is refused before any merge, ignored or not. See the callout at the bottom for why.
When a field appears in both the file and a flag, the flag wins for model,
run_config, queue, defaultMode — and for tools, where --tools
replaces the file's list wholesale. Rule lists (allow, ask, deny,
grants, additionalDirectories) concatenate, file first then flags; a
--arg replaces a file argument of the same name.
Or stay on one line
Every property has a flag.
Quote every rule and every --arg. Bash(npm run test:*) contains a glob and
parentheses; unquoted, the shell gets there first.
| Flag | Meaning |
|---|---|
--name <kebab> | the agent's name, if not in the file or the positional argument |
--description <text> | one line on when to use this agent |
--tools <csv> | e.g. Read,Grep,Glob,Bash. Omit to inherit all |
--model <id> | e.g. claude-opus-5, or an alias: opus, sonnet, haiku, inherit |
--allow / --ask / --deny <rule> | permission rules, repeatable |
--permission-mode <mode> | what happens to a call no rule matches: default, acceptEdits, plan, bypassPermissions |
--add-dir <path> | extra root the agent may read and write, repeatable |
--run-config <name> | attach a RunConfig — the machine it lands on |
--queue <name> | the queue to submit on |
--grant <string> | a domain.resource.verb resource grant, repeatable |
--arg <spec> | a typed prompt argument, repeatable |
--channel <kind[:target]> | e.g. http-sse:/v1/agents/triage/stream |
--scope / --qualname | override the stored scope / leaf name |
--dry-run | resolve, validate and print — send no request at all |
--if-exists <mode> | what a 409 means: fail (default) or ok |
--json | emit JSON instead of prose |
Typed arguments
A {{ placeholder }} in the prompt must have a matching --arg or the
create is refused. A typo'd {{ invocaton_id }} must never reach a model as
literal braces.
! and = are mutually exclusive: required plus a default are two statements
that contradict each other.
Naming
An agent has a name, not a module path — it is not a UDF and nothing here asks
you to think in packages. The name is scoped to the repository you are
standing in, so two repos can each hold a triage without colliding.
With no name at all one is generated — from the description if there is one,
otherwise agent-<8 hex>. The generated form is meant to look generated, so
it invites being renamed rather than being left forever. Scope and name are
both printed before anything is sent.
Attaching a machine
Most agents need none. Attach one when the agent has to run somewhere in particular — a GPU, a specific image, a prepared host:
--run-config names an existing RunConfig by name; it is a reference,
not a copy, so the machine can be re-tuned without touching the agent.
Placement is then derived from that RunConfig's host key — there is no second
field where you say "and this needs a GPU".
What actually gets stored
markdown and channel are real columns. tools, model, permissions,
run_config, queue, grants and arguments have none and ride in
metadata, which round-trips unchanged.
The server walks channel for credentials at full depth and walks nothing
else — so a token under metadata would be stored in plaintext and returned
by GET. This command refuses one locally, because the server will not.