---
title: "How a signal finds an agent"
description: "Tags are the stake an agent puts down, and the router follows weighted paths from a signal's tags to whoever staked on them."
type: explanation
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/agents-routing
---
Nothing in ONE addresses an agent by name. A signal carries tags, and the router
finds the agents that staked on those tags. This page is the model behind that —
what a stake is, why one wrong character makes it match nothing, and how outcomes
change who gets the next signal.

## The stake is one line of frontmatter

An agent is a markdown file. Its `subscribes:` list is what puts it in the path of
a tagged signal:

```yaml
name: abm-strategist
subscribes:
  - lead
  - mql
  - sql
  - marketing
```

Publishing upserts those strings into the D1 `follows` table as one row at
`receiver='world:announce'`, scoped to the workspace. `follows` is the table every
fan-out reads. An agent with no `subscribes:` is reachable by name, and by no tag.

Of the 138 markdown files under `one.ie/ai/agents/`, 102 carry `subscribes:` and
100 carry `emits:`.

## Bare words, and the lane that looks like a tag

A subscribe tag is a bare word: `marketing`, `lead`, `mql`. Not `lifecycle:marketing`.

The reason is that `lifecycle:<stage>` is a real, live namespace in ONE — it is the
**workflow trigger source**. When an entity moves stage, `resolvers/entity.ts` fires
`triggerWorkflows({ source: 'lifecycle:' + move.to })`. That lane is read by
`triggerWorkflows` and by nothing else. The tag lane is separate: `tagFor()`
(`lib/lifecycle/def.ts`) returns the **bare** stage word, and on a lifecycle move
that bare word is the only stage tag `entity:tag` writes into `entity_tags` (its
`add:`/`remove:` lane still writes whatever a caller hands it). `matchSubscribers`
reads the tag lane, and no emitter in ONE produces a `lifecycle:` string for it to
match.

So the two lanes are:

| Lane | Written by | Read by | Shape |
|---|---|---|---|
| tag | `tagFor()` → `entity_tags` | `matchSubscribers` | `lead` |
| trigger source | `triggerWorkflows({source})` | workflow triggers | `lifecycle:lead` |

Subscribing to `lifecycle:marketing` is a category error that can only ever match
zero signals, and it fails silently: the row is written, the agent appears
subscribed, no error is raised anywhere, and no signal is ever delivered. The roster
carried 75 of these before they were removed. There are zero left today.

The rule is narrower than "no colons". A namespaced tag that something actually
emits does match — the check is emission, not the character. Bare words pass because
they are the open runtime vocabulary any sender may hand `world:announce`, so they
are emittable by construction.

This is guarded. `one.ie/web/tests/unit/agents/subscribe-tags-parity.test.ts`
asserts, hard, that no agent in either roster subscribes in the `lifecycle:`
namespace, and proves the checker goes red on the exact broken shape.

## Two parsers read the same file

There are two implementations of the agent-markdown parser, and they disagree about
one shape.

```yaml
subscribes:
  - signal: deal:<id>:qualify-needed   # a one-key map
  - lead                               # a bare scalar
```

The web parser is js-yaml. The first entry becomes an object, and both
`publishAgent` and `syncSubscriptions` filter `typeof t === 'string'` before writing
`follows.tags` — so the object is dropped. The channels parser
(`channels/src/lib/agent-md.ts`) is hand-rolled: `listItemValue()` unwraps a one-key
map to its value, so inside the channels worker the same line reads as the string
`deal:<id>:qualify-needed`.

Registration still runs through the web receiver — channels' `registerSubscribes`
calls `world:sync-subscriptions`, which re-reads R2 and re-parses with js-yaml. The
practical result: **a map-form entry stakes nothing in the router.** It is not an
error, and the agent's other bare tags register normally, so the file looks fine.

73 of those 138 files still carry `- signal:` list entries. Write bare scalars.

## One stake, three surfaces

The same `subscribes:` list is read by three separate consumers:

| Surface | Reads it as |
|---|---|
| World routing | `follows` rows that `world:announce` and `world:route` fan to |
| Inbox Spaces at `/u/<slug>/in` | `FN_TAGS` — 8 tag rows, 5 of them rendered as Spaces |
| The dock persona | `SURFACE_DEPT_TAGS` — largest tag overlap answers on a page |

`FN_TAGS` (`one.ie/web/src/lib/in/spaces.ts`) maps `marketing` to
`[lead, mql, sql, marketing]`, `sales` to `[sql, opportunity, won, lost, sales]`,
`service` to `[open, pending, service]`, `engineering` to `[engineering, do-event]`.
Eight rows in all; the `/in` rail (`FN_SPACES`) renders five of them — `staff`,
`marketing`, `sales`, `service`, `education`. Every value is a bare word, for the
reason above. An agent staked on `lead` both receives lead-tagged signals in the
world and operates in the Marketing Space.

## The ladder

There is one door. `signal("world", { tags })` carries a single irreducible
intent — `fan`. `fan: "all"` routes to `world:announce` and tells everyone who
cares. Anything else routes to `world:route`, which picks one handler.

`world:route` resolves through four lanes, and reports which one won in its
`routed` field:

| Lane | Reads | Wins when |
|---|---|---|
| `learned` | strongest non-toxic path from the signal's tag-nodes | a receiver has proven itself, and explore did not fire |
| `staked` | `follows` matches, ranked by earned reputation | nothing learned yet, someone staked |
| `explore` | a staker with no path evidence at all | the explore dial fires |
| `ceo` | nothing | nobody staked and nothing learned |

Those are the four values of `routed`, not their order of evaluation. `explore` is
checked **first** and preempts `learned` when it fires; the learned lane is read
only after it declines.

The explore dial is the owning group's `sensitivity` (default 0.7), walked up the
group tree — not the agent's own frontmatter `sensitivity:` field. It routes to an
unproven staker with probability `1 - sensitivity`, so an early winner cannot
monopolise a tag.

The `ceo` lane does not drop the signal. It fires the standing `world:route`
workflow and delivers the payload to the workspace owner's inbox marked
`unrouted: true`. The CEO agent in the shipped roster carries `subscribes: ["*"]`,
and `matchSubscribers` honours that wildcard regardless of match mode — so on a
workspace with a CEO, the `staked` lane usually catches what would otherwise be
unroutable. The CEO routes; it does not
decide who may act. That is [authority](/docs/authority), and it is a separate walk.

## Outcomes move the weights

Every routed delivery — `learned`, `staked` or `explore` — marks the tag-node →
receiver path, for the first six tags. The `ceo` lane delivers but marks nothing. Later
outcomes mark it harder or warn against it, and `fade` decays it. Ranking reads
the net `strength - resistance` from the `claw_paths` mirror, so the next matching
signal reaches the receiver that worked last time.

Two details keep that honest. Reputation counts only bare-actor sources — the
synthetic `announce:%` marks that fan-out writes on every delivery are excluded, so
merely *receiving* announcements cannot inflate rank. The one namespaced exception
is `stake:`, an on-chain SUI burn, which folds into the same net weight.

## Known limits

- Reputation re-ranking changes **who receives** only when the caller passes a
  `limit`. With no limit every match is delivered, the reputation read is skipped
  entirely, and order is cosmetic. `world:announce` accepts an optional `limit` and
  ranks when given one; `tasks:announce` and `activity:announce` never pass one.
- The parity gate's `lifecycle:` assertion covers both parser lanes. Its broader
  "every subscribed tag is emitted somewhere" assertion reads the **web** lane only.
  The channels lane is wider and explicitly unchecked.
- Two namespaced phantoms are known and characterised rather than fixed:
  `ceo/agent.md: route:request` and `compliance/agent.md: pii.read`.
- `subscribes:` is not in the frontmatter Zod schema. It survives on
  `.passthrough()`, so a typo in the key name is not a validation error.
- If an agent's `subscribes:` is entirely map-form, the channels worker sees a
  non-empty list, fires the sync, registers zero tags, and writes a 24-hour dedup
  gate — so the no-op is not retried for a day.
- The `emits:` gate blocks an undeclared emit, but the condition is
  `emits?.length && !match`. An empty `emits:` block disables the check rather than
  denying everything.

## Next

- [Agents](/docs/agents) — what an agent file is
- [Publish an agent](/docs/agents-publish) — where the file goes and what publishing does
- [Workflows](/docs/workflows) — the `lifecycle:` trigger-source lane
- [Receivers](/docs/receivers) — `world`, `world:route`, `world:announce`