---
title: "Publish an agent"
description: "Scaffold an agent.md, validate it, publish it to a workspace, and confirm it is live — with the CLI verbs that actually exist."
type: how-to
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/agents-publish
---
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](/docs/agents) and [How a signal finds an agent](/docs/agents-routing).

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

```bash
npx @oneie/cli agent new hello
```

```text
✓ path="hello/agent.md" lines=13
```

That writes `hello/agent.md`, verbatim:

```markdown
---
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:

```bash
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:

```markdown
---
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

```bash
npx @oneie/cli agent validate hello/agent.md
npx @oneie/cli agent lint hello/agent.md
```

```text
✓ 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:`:

```text
✗ ["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

```bash
export ONE_API_KEY=one-xxxxxxxx
npx @oneie/cli agent publish hello/agent.md --slug your-workspace
```

```text
✓ 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](/docs/errors).

| 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](/docs/authority). |

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

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

```bash
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:

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

```bash
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](/docs/mcp) 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_agent` is dead and still shipped.** The MCP tool, the SDK's `client.syncAgent()`, and the `agents:sync` receiver all reach a receiver with no handler. It answers HTTP **200** with `{ outcome: "dissolved", reason: "no_handler" }`, so a caller checking `res.ok` reads a no-op as success. There is also no `agent sync` verb in the CLI, despite `npx oneie agent sync agent.md` still appearing in this repo's `one.ie/ai/agents/README.md`. `agent publish --slug` is the door.
- **`agent validate` is 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-kebab `name:`, 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 only `name:` and `subscribes:`. 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 the `roster:*` keys. A first publish is immediate; an edit may take until the TTL expires.
- **`agent serve` does not start a server.** It prints `status: "listening"` and exits.
- **`agent sign` and `agent verify` are stubs.** `sign` returns `bundle: "pending — run oneie publish to trigger Sigstore"` and signs nothing. `verify` reports `signatures: 0` and 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 write `subscribes:` that way (73 use the form somewhere in their frontmatter, most often under `emits:`). Write bare strings.
- **`--live` only rewrites `lifecycle: draft`.** The `core` scaffold already ships `lifecycle: active`, so the flag does nothing there.

## Next

- [Agents](/docs/agents) — what an agent is and what the frontmatter means
- [How a signal finds an agent](/docs/agents-routing) — what `subscribes:` buys you
- [CLI](/docs/cli) — the rest of the command surface
- [Authority and access](/docs/authority) — who may publish into which workspace