---
title: Handoff artifacts
description: How stations pass long Markdown documents to each other by id with save_artifact and read_artifact, and what keeps a model-supplied id inside its reserved Blob prefix.
type: guide
summary: Long documents travel between stations as an id, never as text through the orchestrator's context.
related:
  - /docs/stations
  - /docs/pipeline
  - /docs/tools
---

# Handoff artifacts



Stations pass long documents to each other by id rather than by text. The Researcher's full memo and the Analyst's supporting detail are saved as handoff artifacts in Vercel Blob, so a document of up to 200,000 characters reaches the station that needs it without travelling through the orchestrator's context.

## Why documents travel by id

Stations inherit nothing, so every delegation message has to be self-contained. That works for a plan and fails for a long memo, because inlining one would push the same text through the orchestrator twice, once on the way back from the station that wrote it and once on the way out to the station that needs it.

Artifacts replace the text with a pointer.

```text
Analyst returns:       artifact_id: "analysis-dedupe-reset-emails-k3f9qz"
Orchestrator relays:   that id, inside the Implementer's delegation message
Implementer opens it:  read_artifact({ id: "analysis-dedupe-reset-emails-k3f9qz" })
```

The document itself never enters the orchestrator's context.

## The two kinds

`ARTIFACT_KINDS` is a closed set of two, and the kind travels with the id so a reader knows what it is holding.

| Kind             | Saved by   | Holds                                                                | Read by               |
| ---------------- | ---------- | -------------------------------------------------------------------- | --------------------- |
| `research-notes` | Researcher | The full memo behind the cited findings                              | Analyst               |
| `analysis`       | Analyst    | File-level notes, code excerpts, and alternatives explored in detail | Implementer, Reviewer |

Adding a third kind means editing `ARTIFACT_KINDS` in `agent/lib/artifacts/config.ts`. Callers cannot pass a kind as free text, so a new handoff shape is a code change rather than a model decision.

## Who saves and who reads

| Agent        | Artifact tools                   |
| ------------ | -------------------------------- |
| Orchestrator | `read_artifact`                  |
| Researcher   | `save_artifact`                  |
| Analyst      | `save_artifact`, `read_artifact` |
| Implementer  | `read_artifact`                  |
| Reviewer     | `read_artifact`                  |

Holding the reader without the saver is what keeps a relayed document out of the orchestrator's conversation. Its instructions allow one use, reading an artifact when a person asks what is in it, and then answering the question rather than pasting the document. Artifact contents never go into a station message, a pull request body, or a thread.

## The structured output stays the contract

`artifact_id` is a required field on both the Researcher's and the Analyst's `outputSchema`, typed `string | null`. Null is the ordinary answer, since most work items produce nothing worth saving beyond the structured fields.

Artifacts carry depth, never the primary output. Cited findings stay in the Researcher's `findings`, and the plan and acceptance criteria stay in the Analyst's structured fields, so a station that never opens the artifact still has everything it needs to work from. `read_artifact` returns `createdAt` alongside the document, and readers treat an old artifact as possibly stale.

## How a model-supplied id stays contained

Every other Blob key in the factory derives from something the model cannot touch, such as `FACTORY_REPO` or the resolved principal. An artifact id is the exception, so `artifactKey` checks it against a pattern before turning it into a key.

```ts title="agent/lib/artifacts/config.ts"
export const ARTIFACT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
```

Lowercase alphanumerics and hyphens, anchored, with no dots or slashes. Without that check, a station could pass `../factory-brain/<hash>.md` and read a managed document through a tool that it wasn't supposed to.

Ids that fail the pattern and ids that were never saved both come back as `found: false`, so a probe learns nothing from the difference.

Ids are readable rather than opaque, because they appear in delegation messages and in logs. `analysis-dedupe-reset-emails-k3f9qz` is the kind, then a slug of up to 48 characters derived from the title, then a six-character random suffix that keeps two analyses of the same work item apart.

## Bounds and the reserved prefix

| Property  | Value                           |
| --------- | ------------------------------- |
| Blob key  | `artifacts/<validated-id>.md`   |
| Body cap  | 200,000 characters              |
| Title cap | 200 characters                  |
| Overwrite | Never, `allowOverwrite: false`  |
| Auth      | The project's Vercel OIDC token |

Both tools go through the shared document helpers in `agent/lib/blob.ts`, so reads take Blob's authenticated `get` path rather than fetching the object's URL, and one call resolves both existence and content.

The `artifacts/` prefix is reserved for these two tools, the same way `factory-brain/` and `user-preferences/` are reserved for theirs. All three are declared in the reserved-namespace registry in `agent/lib/blob.ts`, so a general-purpose Blob capability added later checks one place with `reservedNamespaceForPath` and learns which tool owns the path it tried to touch. [Factory memory](/docs/memory) covers that registry.

## Why neither tool needs a gate

Every station declares an `outputSchema`, which puts it in task mode, where nothing can stop for approval or input. So a station's tools have to be inert by construction rather than gated, and both artifact tools are.

* Saves mint their own id and never overwrite, so no existing document can be replaced.
* The body and the title are both size-bounded.
* Reads reach only ids that pass the pattern, under one reserved prefix.

That is the same argument that lets the Implementer hold `push_branch`, which the [stations reference](/docs/stations) covers.

## Artifacts are not memory

Artifacts sit in Blob and outlive the run that wrote them, since nothing deletes them. Nothing reads them after that run either. Each id exists to carry one document between two stations of a single pipeline pass, so the storage is durable and the useful lifetime is one run.

The [factory brain](/docs/memory) is the opposite. One document per repository, rewritten in place, read by every future run.

## Next steps

<Cards>
  <Card href="/docs/stations" title="Stations reference" description="Which station saves, which reads, and the full output contract for each." />

  <Card href="/docs/pipeline" title="The pipeline" description="Where the ids are minted and relayed on a real work item." />

  <Card href="/docs/memory" title="Factory memory" description="The two durable stores, and the other two reserved Blob prefixes." />
</Cards>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)