| name | multica-creating-agents |
| description | Use when creating, inspecting, or debugging a Multica agent definition via the `multica agent` CLI or POST /api/agents. Not for assigning issues to agents that already exist, and not for runtime task prompts. |
| user-invocable | false |
| allowed-tools | Bash(multica *) |
Creating Multica agents
This is the contract for Multica's agent-creation path: what the create entry
points accept, what the server validates and rejects, how each field is
persisted, and which fields the daemon actually reads at claim time. It is
not a parameter manual — it states source-traced facts, and every claim is
backed by file:line in references/creating-agents-source-map.md.
Quick start (read-only inspection)
These commands read state and have no side effects:
multica agent get <agent-id> --output json
multica agent skills list <agent-id> --output json
multica agent env get <agent-id> --output json
An agent can also be unbound: runtime_id is NULL (served as "" with
runtime_bound: false) after its runtime was deleted, which unbinds instead of
deleting its agents (MUL-5559). An unbound agent keeps everything it owns and
stays editable, but no trigger path will run it — they all refuse with
agent_runtime_required — until agent update <id> --runtime-id <runtime-id> binds
it again. Unbound is orthogonal to archived.
agent get returns the persisted agent including runtime_id, model,
thinking_level, service_tier, custom_args, has_custom_env,
custom_env_key_count, and skills. It never returns plaintext custom_env.
Core model
An agent is a workspace-scoped row (table agent). Creation is a single
POST /api/agents (multica agent create). At task claim time the daemon
re-reads the agent row and assembles the runtime payload — so the persisted
fields, not the create-time output, are what the agent runs on.
Two distinct text fields, often confused:
description is a catalog summary. It is stored and shown in listings; the
daemon does NOT inject it into the agent's runtime prompt. Treat it as
human-facing metadata only. Capped at 255 Unicode code points.
instructions is the runtime behavior contract. The daemon reads it at
claim time and ships it to the provider as the agent's durable instructions.
Persona, responsibilities, boundaries, output and escalation rules go here,
not in description.
CLI / API entry points
Minimum create call (--name and --runtime-id are both required):
multica agent create --name <name> --runtime-id <runtime-id> \
--description "<short catalog summary>" \
--instructions "<runtime behavior contract>" \
--output json
runAgentCreate builds a JSON body and posts it to /api/agents. It only
adds a key when its flag was provided — description/instructions on a
non-empty value, the rest (runtime-config, custom-args, model,
thinking-level, service-tier, visibility, …) on the flag being Changed
— so omitted flags fall through to server defaults rather than sending empty
strings. --max-concurrent-tasks is validated as 1–50 before the request is
sent.
The HTTP body (CreateAgentRequest) accepts: name, description,
instructions, avatar_url, runtime_id, runtime_config, custom_env,
custom_args, model, thinking_level, service_tier, visibility,
max_concurrent_tasks, mcp_config, skill_ids.
Copying an agent
multica agent copy <source-agent-id> forks an existing agent's portable
configuration into a brand-new agent, leaving the source untouched. It is the
CLI/headless equivalent of the web "Duplicate" action. No dedicated server API
is involved: runAgentCopy reads the source with GET /api/agents/<id>, then
POSTs a CreateAgentRequest — passing the source's skill ids in skill_ids so
the bindings attach in the SAME create transaction (unlike agent create, which
binds nothing). The mutation is therefore a single atomic create.
multica agent copy <source-agent-id> --name "My Agent (copy)"
multica agent copy <source-agent-id> --runtime-id <target> --model <model>
- Copied by default, each overridable with the matching flag:
name (suffixed
" (copy)"), description, instructions, avatar, custom_args,
max_concurrent_tasks, invocation permission (permission_mode +
allow-list), and assigned workspace skills.
- A copied
max_concurrent_tasks is included only when the source value is
within 1–50. Historical out-of-range values are omitted so the new agent
receives the server default (6); an explicit out-of-range
--max-concurrent-tasks override is rejected before any API request.
- Runtime-specific fields (
model, thinking_level, service_tier) are copied
ONLY when the target runtime is unchanged. --runtime-id selecting a
different runtime drops them and REQUIRES --model (pass --model "" to
accept the target runtime default), mirroring the web Duplicate clearing model
on a runtime switch.
- Never copied:
custom_env, mcp_config, runtime_config (secret /
machine-local; redacted or masked on read anyway). Supply fresh values with
the same secret-safe flags as agent create (--custom-env*, --mcp-config*,
--runtime-config), or with agent env set after the copy exists.
--no-skills skips copying the source's skill bindings.
Field contracts
| Field | Persisted as | Validated? | Consumed by |
|---|
name | agent.name | required, 400 if empty | listings, runtime payload |
description | agent.description | 400 if > 255 code points | catalog/listing only — NOT the runtime prompt |
instructions | agent.instructions | none | daemon → provider at claim time |
avatar_url | agent.avatar_url | none; an explicit non-empty value is preserved, while omitted/empty creates a random emoji:<glyph> avatar | catalog/listing UI only — NOT the runtime prompt |
runtime_id | agent.runtime_id (nullable) | required at create (400) + must resolve to a runtime in this workspace | selects runtime/provider; NULL means unbound — see below |
model | agent.model (nullable) | none beyond runtime support | daemon reads; empty = runtime default |
thinking_level | agent.thinking_level (nullable) | provider-level enum/safe-token gate; unknown literal → 400. Pi accepts only `off | minimal |
service_tier | agent.service_tier (nullable) | Codex-only safe token; other providers reject; exact model/tier pair checked by daemon | daemon → Codex app-server; empty = local Codex config |
custom_args | agent.custom_args (JSON array) | JSON shape checked CLI-side; server stores as-is | daemon (extra CLI switches); defaults to [] |
runtime_config | agent.runtime_config (JSON) | JSON shape checked CLI-side; server stores as-is | runtime-specific config; defaults to {} |
custom_env |
Defaults when omitted or explicitly null: max_concurrent_tasks → 6.
Other defaults when omitted: runtime_config → {}, custom_env → {},
custom_args → [], avatar_url → a random emoji:<glyph>, visibility →
private
(all materialized server-side before the insert). custom_args/runtime_config
are typed []string/any and marshaled as-is — the JSON-shape rejection
happens in the CLI, not the create handler.
The 1–50 concurrency range applies consistently to create and update. On
create, an omitted field defaults to 6 while an explicitly supplied 0 is
rejected; on update, omission preserves the current value. The CLI performs the
same range check before sending create or update requests.
thinking_level is validated only at the provider level: fixed-vocabulary
providers reject an unrecognized literal, while dynamic-vocabulary providers
such as Codex/OpenCode accept a syntactically safe token. Pi's provider-level
vocabulary is fixed (off|minimal|low|medium|high|xhigh|max), but its exact
supported subset is model-specific and discovered from the local Pi RPC model
catalog. A value unsupported for the chosen model is NOT rejected here — the
daemon checks its local model catalog at execution time, logs a warning, and
omits the incompatible override.
Set it from the CLI with --thinking-level on agent create and agent update, mirroring --model: the flag is a thin pass-through to the top-level
thinking_level field, and on update an empty string (--thinking-level "")
clears it back to the runtime default. The CLI deliberately does not enumerate
the valid levels — they are runtime/model-specific (Claude currently uses
low|medium|high|xhigh|max; Pi uses
off|minimal|low|medium|high|xhigh|max; Codex values are discovered from the
runtime's model catalog). It forwards the token, the server applies the
provider's fixed-enum or safe-token gate, and the daemon performs the exact
model/level check. A runtime whose provider has no thinking concept rejects any
non-empty value with a 400.
service_tier is the matching first-class Codex speed control. Set it with
--service-tier <catalog-id> on create/update; use --service-tier "" on
update to clear it. The runtime model catalog owns both availability and
display copy (currently priority, shown as Fast). The server accepts safe
future Codex catalog IDs, while the daemon verifies the exact model/tier pair
before execution and omits a stale incompatible override. Agents without an
explicit model fail closed because the effective config.toml model is unknown.
model vs custom_args
model is a first-class persisted column the daemon reads directly.
custom_args are normally raw provider CLI args. The CLI help notes that some
providers (codex app-server, openclaw) reject --model inside custom_args —
but that is documented CLI guidance, not a server-enforced invariant; nothing
in the create handler inspects custom_args for a model flag. Provider
backends may consume protocol selectors before launch:
- Pi filters
--thinking because the first-class thinking_level field owns
that flag and must be its only source.
- ZeroClaw consumes
--agent <alias> / --agent-alias <alias> (including
=value forms) and sends the value as the ACP session/new.agentAlias
parameter. zeroclaw acp has no such CLI flag. Set one of these custom args
when ZeroClaw has multiple agents and no [acp].default_agent; omit it for a
sole-agent config so ZeroClaw can auto-select that agent.
Never put credentials or other secrets in custom_args. Daemon command logs
redact argument values, but values that a backend does not consume still live
in the provider process's argv and may be visible to other local processes
through ps or /proc. Put provider credentials in custom_env instead,
using its stdin or 0600 file input where possible.
Env & secrets
custom_env is secret material. The CLI offers three input channels; two keep
secrets out of shell history and the process list:
multica agent create --name <name> --runtime-id <runtime-id> --custom-env-stdin --output json
multica agent create --name <name> --runtime-id <runtime-id> --custom-env-file <0600-json> --output json
--custom-env-stdin reads the JSON object from stdin; --custom-env-file
reads it from a file (suggested mode 0600). The third channel,
--custom-env <json>, puts the value on the command line where shell history
and ps can see it — avoid it for real secrets.
Read-side facts (these are the wrong assumptions to avoid):
- Agent resources never expose plaintext
custom_env. agent list/get/create/update and WS events return only has_custom_env (bool) and
custom_env_key_count (int).
- Reading plaintext values requires the dedicated
GET /api/agents/{id}/env
endpoint (multica agent env get). It is gated to the agent's own human
owner or a workspace owner/admin, and agent actors are denied
regardless of the backing member's role — a running agent cannot read another
agent's secrets, not even one its own human owns.
- Writing values after creation does NOT go through
agent update. The generic
update handler rejects any custom_env field with a 400 ("use PUT
/api/agents/{id}/env"). Plaintext env writes are handled by
PUT /api/agents/{id}/env (multica agent env set), which carries the same
gate and writes an audit row.
mcp_config
mcp_config is the agent's MCP server configuration (a JSON object such as
{"mcpServers": {…}}). It is also secret material — MCP entries routinely embed
API tokens — and offers the same three input channels as custom_env, on BOTH
agent create and agent update:
multica agent create --name <name> --runtime-id <runtime-id> --mcp-config-file <0600-json> --output json
multica agent update <agent-id> --mcp-config-stdin --output json
multica agent update <agent-id> --mcp-config 'null'
--mcp-config-stdin / --mcp-config-file keep the value out of shell history
and ps; the inline --mcp-config <json> does not. The CLI requires a JSON
object or the literal null; a top-level array or primitive is rejected
client-side, and empty stdin/file input errors rather than silently clearing.
Two ways mcp_config differs from custom_env:
- It IS settable through
agent update. Unlike custom_env, mcp_config
has no dedicated audited endpoint — the generic PUT /api/agents/{id} accepts
it. Tri-state per the raw request body: field omitted → no change; null →
clear; object → replace.
- It is serialized on read, but redacted.
agent get/list return
mcp_config only to callers allowed to view agent secrets; otherwise the
field is null and mcp_config_redacted is true. Agent actors never see
it, and a workspace may force redaction for everyone.
Provider support is not uniform: Qwen Code accepts a managed mcp_config through a daemon-owned 0600 temporary JSON file passed with --mcp-config; it is removed when the run exits. Leave the field unset (null) to inherit Qwen Code native settings.
Workspace MCP servers
A workspace keeps a LIBRARY of MCP servers (workspace Settings → MCP, or
multica workspace mcp list|add|update|remove). Adding one there gives it to
NO agent — same shape as a workspace skill. It reaches an agent only when
someone assigns it:
multica workspace mcp list --output table
multica agent mcp add <agent-id> <server-id>
multica agent mcp disable <agent-id> <server-id>
multica agent mcp remove <agent-id> <server-id>
At claim time the effective set is:
| Layer | Reaches the agent when |
|---|
| runtime-local servers | always (the daemon merges the runtime's own file) |
| workspace servers | assigned to THIS agent and left enabled |
the agent's own mcp_config | always; it WINS on a name collision |
Two consequences worth knowing before writing an agent's config: assigning a
shared server does not require re-listing it in mcp_config (they merge), and
mcp_config is now only about servers private to that agent — a
managed-but-empty {} no longer means anything about the workspace layer,
because nothing is inherited in the first place.
The stored entry is write-only — reads return the server's name and
transport, never urls, commands, headers, or env, for any role.
Skill binding
Creating an agent does NOT bind any workspace skill — binding is a separate
call after the agent exists. Two distinct verbs:
add is additive — it merges the given ids with existing bindings
(POST /api/agents/{id}/skills/add).
set is replace-all — it overwrites the entire binding list with exactly
the given ids (PUT /api/agents/{id}/skills); --skill-ids '' clears all.
multica agent skills add <agent-id> --skill-ids <skill-id> --output json
multica agent skills list <agent-id> --output json
At claim time the daemon assembles the agent's skills as workspace-bound skills
FIRST, then appends the platform built-in skills. LoadAgentSkills loads each
bound skill's content plus its supporting files; built-in skills are embedded
at compile time and loaded from SKILL.md + sibling files. Both reach the
provider as skill content — which is why capability belongs in a bound skill,
not pasted into instructions.
Side effects needing approval
Read-only (safe): agent get, agent skills list, agent env get.
State-changing (require an explicit instruction — do not run speculatively):
multica agent create — inserts a new agent row.
multica agent copy — inserts a new agent row (a fork of an existing agent);
the source is left untouched.
multica agent skills add / set — mutate bindings (set is destructive:
it drops bindings not in the new list).
multica agent env set — overwrites the full custom_env map and writes an
audit row.
Common wrong assumptions
- "
description is the prompt." It is not — only instructions reaches the
runtime. A rich description with empty instructions yields a named shell with
no operating contract.
- "Create binds the agent's skills." It does not; bind explicitly afterward.
- "
agent update can rotate env." It cannot — it 400s on custom_env; use the
env endpoint.
- "
mcp_config behaves like custom_env on update." It does not — mcp_config
IS settable via agent update (--mcp-config), with --mcp-config null to
clear; only custom_env is gated behind the dedicated env endpoint.
- "
agent get shows env values." It shows only has_custom_env and
custom_env_key_count.
- "An invalid
thinking_level/model combo is caught at create." Only an
unknown provider-level literal is — model-specific gaps fail at run time.
- "
set and add are interchangeable for skills." set replaces all
bindings; using it when you meant add silently removes capabilities.
References
references/creating-agents-source-map.md maps every contract above to its
file:line on the current tree, the runtime effect, and a safe read-only
verification command.