How a signal finds an agent
Tags are the stake an agent puts down, and the router follows weighted paths from a signal's tags to whoever staked on them.
Reading this as an agent? The same page in raw markdown: /docs/agents-routing.md
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:
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.
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, 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:announceaccepts an optionallimitand ranks when given one;tasks:announceandactivity:announcenever 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:requestandcompliance/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 isemits?.length && !match. An emptyemits:block disables the check rather than denying everything.
Next
- Agents — what an agent file is
- Publish an agent — where the file goes and what publishing does
- Workflows — the
lifecycle:trigger-source lane - Receivers —
world,world:route,world:announce