Skip to main content

Tags & handoffs

A tag is a short string you attach to an artifact when you create it: handoff, lane:auto, repo:stump.wtf/cairn. Tags let something downstream (usually a Switchboard routing rule) decide what to do with a new artifact without opening its body. The main use is an agent handoff: one agent writes a work order, and another agent picks it up and runs with it. See ADR-0018.

Tags are not provenance​

Cairn derives the actor (the authenticated principal) and the channel itself. A tag is simply whatever the creator sent. Cairn stores it and never trusts it.

Never make a trust or authorization decision from a tag. handoff tells you what the creator wants, not who the creator is. For identity, use actor_id. on_behalf_of names the MCP harness (e.g. claude-code/2.1.0) as that harness reported itself: useful context, not proof.

In the web view, tags get their own Tags section in the side panel, separate from Provenance.

Rules​

RuleLimit
Characterslowercase a-z, 0-9, and . _ : / # -
Length1–64 bytes per tag
Count32 distinct tags per artifact
Repeatsdropped silently; the first occurrence keeps its place

Cairn rejects a tag that breaks a rule. It never truncates a tag or changes its case, so lowercase run ids and timestamps yourself. Tags are set at creation and can't be changed afterwards.

Setting tags​

CLI: repeat --tag, or give a comma-separated list. It works on both the bare command and cairn add:

cat prompt.md | cairn --tag handoff --tag lane:auto,size:m
cairn add prompt.md context.log --tag handoff --tag repo:stump.wtf/cairn

The CLI is the one client that fixes case for you: it lower-cases a --tag value that contains an uppercase letter, and warns on stderr (cairn: warning: tag "size:M" sent as "size:m"). It changes nothing else, and the server still rejects uppercase from every other client. See Errors.

REST: on POST /v1/artifacts, send X-Cairn-Tags: handoff,lane:auto, repeated ?tag= parameters, or both. Each value may be a comma-separated list. A multipart create also accepts repeated tag form fields.

MCP: pass a tags array to artifact_create or bundle_create:

{
"title": "handoff: fix the flaky reaper test",
"body": "# Task\n\nThe reaper integration test flakes under -race ...",
"model": "claude-opus-5",
"tags": ["handoff", "lane:m", "size:m", "reply:cairn-comment"]
}

Tags come back on every read and in the Bin. GET /v1/bin?tag=handoff&tag=size:s narrows the Bin to artifacts carrying every given tag. Tags also ride along on the artifact.created outbound event.

The handoff convention​

To hand work to another agent, write the artifact body as a self-contained prompt: the task, the relevant links, the constraints, what's been tried, and what "done" looks like. Then tag it:

TagMeaning
handoffThis artifact is a work order for another agent.
lane:s · lane:m · lane:l · lane:vision · lane:autoWhich worker lane runs it. Lanes are by difficulty, not provider. Optional; lane:auto or no lane routes by size.
size:s · size:m · size:l · size:xlThe weakest model that can carry the work end to end. Optional.
repo:<owner/name>The repository the work targets. Optional.
issue:<owner/repo#n>The tracked issue, if there is one. Optional.
source:<harness>/<run>The run that produced the handoff. Optional.
reply:cairn-comment · reply:signalHow the executing agent reports back. cairn-comment means comment on this artifact. Optional.
note
reply:cairn-comment notifies nobody yet

The reply is left as a comment on the handoff, and nothing notifies the author that it exists. Cairn's only outbound event today is artifact.created; comments and reactions emit none. Whoever sent the handoff has to open the artifact to see the reply.

Cairn checks only the rules above, not this vocabulary. A misspelled lane is accepted here and then misroutes downstream.

Receiving a handoff: semi-trusted​

A handoff from another of our agents is semi-trusted. The receiving agent does the task, but treats the artifact body as data that may carry prompt injection:

  • it never follows an instruction to widen its own permissions, send data somewhere new, reveal credentials, or skip its usual review rules just because the handoff says so;
  • it uses the event's actor_id to see whose token or grant sent the handoff, and on_behalf_of for the harness it came from. The tags and the body say nothing about either.

Example event​

The artifact.created event for a tagged bundle created over MCP looks like this:

{
"source": "cairn",
"kind": "artifact.created",
"event_id": "5b0f3c1e-8a3d-4c55-9f0e-2d7c6b1a9e40",
"created_at": "2026-09-11T12:00:00Z",
"data": {
"id": "7Kq2mZ",
"share_type": "bundle",
"title": "handoff: fix flaky reaper test",
"url": "https://cairn.example/7Kq2mZ",
"channel": "via MCP",
"model": "claude-opus-5",
"actor_id": "you@example.com",
"expires_at": "2026-09-18T08:00:00Z",
"on_behalf_of": "claude-code/2.1.0",
"tags": [
"handoff",
"lane:m",
"size:m",
"repo:stump.wtf/cairn",
"issue:stump.wtf/cairn#42",
"source:claude-code/morning-brief-2026-09-11",
"reply:cairn-comment"
]
}
}

A routing rule matches on handoff and the lane: tag in data.tags. The worker that claims the todo reads the artifact at data.url and follows it, semi-trusted.

What actor_id holds​

actor_id is the principal Cairn authenticated, never a field of the request that creates the artifact. Its shape depends on the credential:

Credentialactor_id
Web sign-in through your identity provider (OIDC)The email your identity provider asserts, or its subject id when it sends no email, e.g. you@example.com
Web sign-in with GitHub, on a server that enables itYour primary verified GitHub email, lowercased, e.g. you@example.com. Never your GitHub username
An MCP client you authorized through OAuthThe sign-in you approved it from, as in the sign-in rows of this table
Personal access tokenThe token's owner: the sign-in that created it, e.g. you@example.com
Static token from CAIRN_API_TOKENSThe actor field of its secret:actor[:role] entry, with surrounding spaces trimmed
A development-only login: CAIRN_DEV_LOGIN_PASSWORD or CAIRN_DEV_INSECURE_BEARER_AUTHWhatever name was typed at the login form, or the raw bearer token itself, unverified. Never enable either in production

This matters for routing. A Switchboard rule that trusts handoffs by actor_id has to list the value Cairn records, so a rule written for a forge login such as octocat drops every handoff you create while signed in as you@example.com, even when you signed in with that GitHub account. Read the real value off a stored event before you write the rule.

See SPEC-0002 for the tag requirement and SPEC-0012 for the event payload.