Publish an agent
Scaffold an agent.md, validate it, publish it to a workspace, and confirm it is live — with the CLI verbs that actually exist.
Reading this as an agent? The same page in raw markdown: /docs/agents-publish.md
Six steps from an empty directory to an agent file live in your workspace. For what an agent is and how a signal reaches one, read Agents and How a signal finds an agent.
You need a workspace slug you control and a world key (it starts one- or osk_). npx @oneie/cli auth login writes one to ~/.config/oneie/key; ONE_API_KEY in the environment does the same job.
Use npx @oneie/cli, not npx oneie. Both install a bin named oneie, but they are two npm packages and the unscoped one lags: oneie is 4.0.3, @oneie/cli is 4.0.7.
1. Scaffold
npx @oneie/cli agent new hello
✓ path="hello/agent.md" lines=13
That writes hello/agent.md, verbatim:
---
agentmd: "0.1"
name: hello
title: Template Agent
model: anthropic/claude-haiku-4-5
summary: A helpful agent.
description: Use when you need to accomplish tasks.
sensitivity: public
lifecycle: active
---
You are a helpful assistant. Complete tasks clearly and concisely.
--profile picks the starter. --help advertises core | commerce | asi, but the flag accepts any of the eight names agent templates prints:
npx @oneie/cli agent templates
core, commerce, asi, travel-planner, marketing-strategist, support-tier1, sales-discovery, code-reviewer.
agent fork <template> <local-name> scaffolds the same file and rewrites both name: and title: to the local name — agent fork core hello gives you name: hello and title: hello Agent. It refuses rather than overwrite an existing file.
2. Edit the frontmatter
Two keys carry the weight. name: decides whether the publish lands; subscribes: decides whether anything reaches the agent afterwards.
| Key | Rule |
|---|---|
name: | Required. Kebab-case ([a-z0-9][a-z0-9-]*). It becomes the R2 key <slug>/agents/<name>.md, so it must be unique within the workspace. |
subscribes: | Optional. Bare words only — marketing, lead, mql. A namespaced tag such as lifecycle:marketing matches nothing. |
The scaffolded title: Template Agent is cosmetic; change it. Everything below the closing --- is the system prompt. For the rest of this page the file reads:
---
agentmd: "0.1"
name: hello
title: Hello Agent
model: anthropic/claude-haiku-4-5
summary: A helpful agent.
description: Use when you need to accomplish tasks.
sensitivity: public
lifecycle: active
subscribes: [marketing, lead, mql]
---
You are a helpful assistant. Complete tasks clearly and concisely.
3. Validate and lint
npx @oneie/cli agent validate hello/agent.md
npx @oneie/cli agent lint hello/agent.md
✓ errors=[] checked=1
✓ issues=[] rules=8
validate fails hard; lint reports style issues. Both exit 1 on a finding, so they work in a script. validate on a file with no name::
✗ ["missing required field: name"]
Add --json for one machine-readable line: {"ok":true,"errors":[],"checked":1}. The flag works before or after the subcommand.
agent dev hello/agent.md watches the file and re-checks it on every save. It runs its own error/warn checker, not validate’s — a missing title: is a warning there and invisible to validate.
4. Publish
export ONE_API_KEY=one-xxxxxxxx
npx @oneie/cli agent publish hello/agent.md --slug your-workspace
✓ name="hello" url="your-workspace/agents/hello.md" bytes=307 subscribes=["marketing","lead","mql"]
bytes is the length of the file you sent, so yours differs if your text does.
This POSTs { data: { slug, name, content } } to https://one.ie/api/ask/world%3Apublish-agent. The receiver writes R2 <slug>/agents/<name>.md, archives a v<epoch-ms>.md copy (newest 20 kept), busts the workspace roster cache, and upserts the subscribes: tags into the follows table so the router can reach the agent.
These are the refusals you hit while authoring an agent.md, and this is where name: bites. For the full refusal surface — rate limits, status codes, the rest — read Errors and limits.
| Refusal | Cause |
|---|---|
missing required field: name | No name: in frontmatter. The CLI catches this locally and never sends the request; over raw HTTP or MCP the same condition returns 400 agent.md missing `name:` in frontmatter. |
frontmatter name "x" does not match publish target "y" | The publish target defaults to name:, so this appears when you pass --name. <slug>--<name> is also accepted as a match. |
forbidden | The key does not control that workspace. The CLI appends which case it is: an unrecognised key, or an authenticated identity that does not own the slug. See Authority and access. |
--target agentverse is a different destination entirely, and it uploads two of your secrets to agentverse.ai: the resolved ONE world key (as ONEIE_API_KEY) and, if it is set in your environment, ANTHROPIC_API_KEY. The first is the credential that controls your workspace. Leave the flag off unless sending both to a third-party host is what you want.
5. Verify it is live
npx @oneie/cli agent list --slug your-workspace
Returns { ok, slug, agents: [{ name, bytes, key, versionCount }] }. Your agent should be in the list; versionCount is how many archived versions it has.
npx @oneie/cli agent history hello --slug your-workspace
Returns { ok, slug, name, versions: [{ ts, bytes, key }] }, newest first — one entry per publish, up to the newest 20. The archive write is best-effort; a publish that landed is never failed because its archive did not.
Round-trip the file to prove the bytes match what you sent:
npx @oneie/cli agent pull hello --slug your-workspace --out /tmp/hello.md
diff /tmp/hello.md hello/agent.md
6. Iterate
Edit the file and run step 4 again. Each publish archives a version.
npx @oneie/cli agent history hello --slug your-workspace
npx @oneie/cli agent rollback hello --slug your-workspace --to 1757000000000
--to is the epoch-ms ts from agent history. Add --yes to skip the confirmation prompt. agent unpublish hello --slug your-workspace removes the live file.
agent ai-edit hello/agent.md --prompt "make the tone more formal" rewrites the file with Claude and needs ANTHROPIC_API_KEY. agent diff a.md b.md compares two files.
The same recipe over MCP
The agent-facing tools in @oneie/mcp cover the same six steps, so an agent inside Claude Code or Cursor follows this recipe without a shell. See MCP server for connecting it.
| Step | MCP tool | Note |
|---|---|---|
| Get a key | auth_agent | Calls POST /api/auth/agent, no prior credential. Returns { uid, apiKey, wallet, group }. The key owns its own group group:<uid> — publishing into someone else’s workspace still needs authority over that slug. |
| 1. Scaffold | scaffold_agent | Takes a preset name. list_presets lists them, get_agent returns one preset — it does not fetch a published agent. |
| 2–3. Validate | — | No MCP equivalent. The publish route checks that name: is present and matches, scans the content for XSS patterns (422), bounds its length to 8–65,536 characters, and rate-limits to 10 publishes per workspace per minute (429). |
| 4. Publish | publish_agent | { slug, name, content } → POST /api/agents/publish. Same R2 object and archive as the CLI. You pass name yourself, so it must match the frontmatter name: — this is where the mismatch 400 bites. |
| 5. Verify | list_agents, agent_history, pull_agent | Same three routes as the CLI. |
| 6. Iterate | rollback_agent, unpublish_agent | rollback_agent takes the epoch-ms ts. |
| Run one | agent_run | Registry-derived, not declared in the tool files. |
| — | sync_agent | Dead. Do not call it. See below. |
Known limits
sync_agentis dead and still shipped. The MCP tool, the SDK’sclient.syncAgent(), and theagents:syncreceiver all reach a receiver with no handler. It answers HTTP 200 with{ outcome: "dissolved", reason: "no_handler" }, so a caller checkingres.okreads a no-op as success. There is also noagent syncverb in the CLI, despitenpx oneie agent sync agent.mdstill appearing in this repo’sone.ie/ai/agents/README.md.agent publish --slugis the door.agent validateis not the schema. It is a regex checker, not the Zod schema its docblock points at. Its hard errors are: a path that does not exist, frontmatter that is missing or never closed, a missing or non-kebabname:, and a stage that references a section id that does not exist. Nothing else. The server does parse your file through the Zod schema at publish, but a schema error there does not block the write — the publish path reads onlyname:andsubscribes:. A file can be published and still be wrong.- A republish can be served stale for up to five minutes. The channels runtime caches the agent file under
persona:<slug>:<agentId>with a 300-second TTL, and nothing deletes that key — publishing busts only theroster:*keys. A first publish is immediate; an edit may take until the TTL expires. agent servedoes not start a server. It printsstatus: "listening"and exits.agent signandagent verifyare stubs.signreturnsbundle: "pending — run oneie publish to trigger Sigstore"and signs nothing.verifyreportssignatures: 0and only checks that the file exists.- The nested
- signal:list form splits two ways. Written as a mapping (- signal: some:receiver) rather than a bare string, the entry is dropped by the web parser and unwrapped and registered by the channels parser, so one file produces two different stakes depending on which lane reads it. Of the 138 agent files in this repo, 68 still writesubscribes:that way (73 use the form somewhere in their frontmatter, most often underemits:). Write bare strings. --liveonly rewriteslifecycle: draft. Thecorescaffold already shipslifecycle: active, so the flag does nothing there.
Next
- Agents — what an agent is and what the frontmatter means
- How a signal finds an agent — what
subscribes:buys you - CLI — the rest of the command surface
- Authority and access — who may publish into which workspace