DreamLake

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.

terminalbash
dreamlake agents create triage --prompt 'You triage failed runs. Read the
terminal event first, classify the failure, quote the line that decided it.'

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.

Nothing here runs the agent

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.

terminalbash
dreamlake agents create triage --file - <<'AGENT'
# Run triage

You triage failed invocations. Read the terminal event before the logs — it
says whether the worker died, the container exited, or the body raised, and
those three send you to different places.

Quote the single log line that decided your classification. One line.
AGENT

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.

agent.mdyaml
---
name: run-triage
description: Triages a failed invocation and proposes the smallest fix.
tools: [Read, Grep, Bash]
model: claude-opus-5
permissions:
  allow: [Read(./runs/**), Bash(lakeshore logs:*)]
  deny:  [Read(./.env), Bash(rm:*)]
run_config: gpu-a10g
arguments:
  - { name: invocation_id, type: string, required: true }
---

Triage invocation {{ invocation_id }}. Read its terminal event first.

$EDITOR, on a scaffold

terminalbash
dreamlake agents create run-triage --edit

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.

FieldTypeWhoseMeaning
namestringClaudethe agent's name. The positional argument or --name wins over it
descriptionstringClaudeone line on when to use this agent
toolslistClaudee.g. [Read, Grep, Bash]. Omit to inherit all
modelstringClaudea model id (claude-opus-5) or an alias: opus, sonnet, haiku, inherit
permissions.allow / ask / denylist of rulesClaudeTool or Tool(specifier), e.g. Read(./runs/**), Bash(npm run test:*)
permissions.additionalDirectorieslist of pathsClaudeextra roots the agent may read and write
permissions.defaultModestringClaudewhat happens to a call no rule matches: default, acceptEdits, plan, bypassPermissions
run_configstringLakeshorenames an existing RunConfig — the machine it lands on
queuestringLakeshorethe queue to submit on
grantslistLakeshoredomain.resource.verb resource grants
argumentslistLakeshoretyped prompt arguments — see below
channelmappingLakeshore{ 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 permissions is refused. The merge is an allowlist, and an allowlist that silently drops a permissions.allowed typo 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 anywheretoken, 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.

terminalbash
dreamlake agents create run-triage \
  --description 'Triages a failed invocation.' \
  --tools Read,Grep,Bash --model claude-opus-5 \
  --allow 'Read(./runs/**)' --deny 'Bash(rm:*)' \
  --run-config gpu-a10g --queue gpu-a10g \
  --arg 'invocation_id:string!' \
  --arg 'max_log_lines:integer=200' \
  --prompt 'Triage {{ invocation_id }}. Read at most {{ max_log_lines }} lines.'

Quote every rule and every --arg. Bash(npm run test:*) contains a glob and parentheses; unquoted, the shell gets there first.

FlagMeaning
--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 / --qualnameoverride the stored scope / leaf name
--dry-runresolve, validate and print — send no request at all
--if-exists <mode>what a 409 means: fail (default) or ok
--jsonemit 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.

the mini-syntax
invocation_id:string!                               required
max_log_lines:integer=200                           defaulted
verdict_detail:enum(terse|normal|forensic)=normal    fixed set
dry_run:boolean=false

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

output
dreamlake agents create triage        # in dreamlake-starter-kit
→ agent `triage`, scope `dreamlake_starter_kit`

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:

terminalbash
dreamlake agents create colmap-driver --file agent.md \
  --run-config colmap-docker-gpu --queue colmap-gpu

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

No credentials under `metadata`

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.