Artifacts
After this page you can find everything a run produced, fetch and verify its bytes, and know which outputs a run keeps rather than discards — because the manifest and the content are two separate things you fetch separately.
What an artifact is
Section titled “What an artifact is”An artifact is a durable record of something a run produced: a written summary, a file the agent chose to show you, the pull request it opened. Each artifact has two halves you fetch separately:
- The manifest — small JSON, listed and read from the session. It says what the thing is, how big it is, and what its digest is.
- The content — the raw bytes, from a separate endpoint that returns them as-is with no JSON wrapper.
That split is the reason artifacts are useful in a durable integration: the manifest is cheap, stays readable, and is enough to reconcile against your own records without transferring anything.
{ "artifact": { "id": "b0d5f5a2-8c3e-4d21-9a44-2f1c7b8e9012", "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f", "runId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "kind": "result.summary", "role": "output", "contentType": "text/markdown; charset=utf-8", "size": 1284, "sha256": "9f2b…", "metadata": { "truncated": false }, "createdAt": "2026-08-11T09:04:12.882Z" }}| Field | Notes |
|---|---|
runId |
Present when the artifact belongs to a run. Absent for session-level records. |
publicationId |
Present on an artifact produced by publishing — the github.pull_request record. |
kind |
An open string. Match the ones you handle; ignore the rest. |
role |
An open string; output is what the public API produces today. |
contentType |
The MIME type the content endpoint will serve. |
size |
Bytes, exact. |
sha256 |
The digest of the content. Also served as the ETag. |
metadata |
At most truncated, fileName, label, description. |
The kinds you will see on the public API today:
kind |
What it is |
|---|---|
result.summary |
The run’s written summary of what it did. Markdown. |
presented.file |
A file the agent deliberately surfaced. metadata.fileName carries its name. |
github.pull_request |
The record of a pull request a publication opened. Carries publicationId. |
kind and role are strings in the contract, not enumerations. New kinds can
appear without a version change, so treat an unrecognized kind as data you skip
rather than as an error.
metadata is allow-listed on the way out: only those four keys can ever appear,
and anything a producer attached beyond them is dropped before the response.
Do not expect to smuggle context through it.
The summary artifact and the last message are not duplicates
Section titled “The summary artifact and the last message are not duplicates”A run’s final wamp.message.created event and its result.summary artifact
usually carry the same words, and that is the one place an integrator records the
agent’s answer twice. They are two carriers with two jobs:
wamp.message.created |
the result.summary artifact |
|
|---|---|---|
| How many per run | one per assistant message, so many | exactly one, or none |
| What it holds | the conversation as it happened, in order | the final handoff only |
| Cap | 16384 characters per event, under a budget of 10000 captured facts per session | 100000 bytes, content-addressed by its SHA-256 |
| When it is absent | never, once a message exists | when the run ended with no prose at all |
| After the fact | immutable — events are append-only | immutable — a second write with different bytes is rejected as a producer error |
Render the events when you are showing work happening: progress, a live timeline, a chat transcript. Messages and progress arrive during the run, and every event is ordered.
Store the artifact when you are keeping an outcome: a ticket comment, a digest, a record your own product owns. It is one stable, addressable document per run rather than a stream you reassemble, and its SHA-256 is what makes storing it idempotent.
Recording both means holding the same prose twice with no rule for which wins when the two caps clip it differently. Pick by what the surface is for.
Nothing reclaims artifact bytes
Section titled “Nothing reclaims artifact bytes”A manifest you can list is a manifest whose bytes you can fetch: the platform runs no reclamation, so an artifact does not expire and the content endpoint has no “gone” answer. The decisions worth making about artifact bytes are the ones on this page that are enforced: what a run keeps at all, and what it discards for size.
Finding artifacts
Section titled “Finding artifacts”Two paths, and you want both.
From the event log, as soon as it happens:
{ "type": "wamp.artifact.created", "subject": { "sessionId": "…", "runId": "…" }, "data": { "artifactId": "b0d5f5a2-8c3e-4d21-9a44-2f1c7b8e9012", "kind": "result.summary", "role": "output", "contentType": "text/markdown; charset=utf-8", "size": 1284, "sha256": "9f2b…", "state": "available", "truncated": false }}This is the earliest moment you can act on an output, which makes it the right place to trigger a fetch. See Follow a run live.
From the list, when you need to reconcile:
GET /v1/sessions/{sessionId}/artifacts?after=<artifactId>&limit=<1..100>Oldest first. after is an artifact id, not a number and not a timestamp —
it acts as an exclusive cursor, and the response’s nextAfter is the id to send
next. hasMore tells you whether the page was capped. There is no filter
parameter: no filtering by runId, by kind, or by date. Page and filter on your
side; unknown query parameters are rejected with 400 invalid_request rather than
ignored.
Internal artifacts — the checkpoints that make a session restorable — are never listed and never fetchable. What you see is the output surface, not the storage layer.
When a run produced more than is kept
Section titled “When a run produced more than is kept”What a run keeps is bounded per file, per run and by count, and the caps are enforced when the artifact is registered, not when you read it. The numbers are in Limits; none of them is advertised through the API, so read them there rather than probing for them.
An output that would exceed a cap is not retained, and the run says so rather than under-reporting:
{ "type": "wamp.artifact.omitted", "subject": { "sessionId": "…", "runId": "…" }, "data": { "omittedArtifacts": 2, "reason": "not_retained" }}Treat that event as information for the human in the loop: the agent produced more than the session keeps, and if those outputs matter the task should be scoped to produce fewer or smaller ones — or the agent should be asked to write them into the repository, where publishing carries them out.
Separately, a retained artifact may be metadata.truncated: true, meaning the
content itself was cut to fit. The size and sha256 always describe the bytes
you will actually receive, truncated or not.
Fetching and verifying content
Section titled “Fetching and verifying content”curl -sS -D headers.txt -o summary.md \ "$WAMP_API/v1/sessions/$SESSION_ID/artifacts/$ARTIFACT_ID/content" \ -H "Authorization: Bearer $WAMP_TOKEN"
shasum -a 256 summary.md # compare against the manifest's sha256The response is raw bytes — not base64, not wrapped in JSON. Its full header set is in API conventions; two things follow from it that change how you write the caller.
First, the digest is free: the ETag is the manifest’s sha256, so compare it
against what you stored and you never have to guess whether a re-fetch changed.
Second, the bytes are agent output and are not trusted content. They are
served with sniffing disabled and a sandboxing policy for a reason. If you
render an artifact in your own product, sandbox it on your side too; do not
inline agent-produced markup into a trusted page.
Whole-object reads only: there is no range request, no partial fetch, and no resume. A 32 MiB file is one call.
Handing a file to another session
Section titled “Handing a file to another session”A presented.file can be the input of a turn in another session without
passing through your backend. Name it in the turn’s attachments:
PUT /v1/sessions/{targetSessionId}/turns/{turnId}Content-Type: application/json
{ "message": "Flash this firmware onto the test board.", "attachments": [{ "sessionId": "<source session>", "artifactId": "<artifact id>" }]}- Any session your credential can read may be the source. Only
presented.fileartifacts can be handed over; anything else is400 invalid_requestnamingattachments[<index>]. - At most 24 files and 64 MiB per turn.
- The bytes are copied into the target session when the turn is admitted, so the source session can be archived afterwards. A replayed turn compares the copied files’ digests and creates nothing new.
- The agent finds each file at
.wamp/inputs/<turnId>/<name>in its workspace, and its prompt names the paths. Images, PDFs and small text files are also shown to it inline. The directory is kept out of git. - A new sandbox for the same session gets earlier turns’ files back, newest first, up to 256 MiB; the agent is told which files are not there.
An agent hands files to the worker sessions it started the same way, limited to its own session and its workers.
What you may assume, and what you must handle
Section titled “What you may assume, and what you must handle”Assume. A manifest, once created, stays readable for the life of the session —
archiving does not remove it. size and sha256 describe exactly the bytes the
content endpoint returns. Artifacts are immutable: the same id never serves
different content. The list is stably ordered oldest-first.
Handle. 404 cloud_artifact_not_found for an id that never existed in this
session. Kinds you do not recognize.
wamp.artifact.omitted, meaning outputs existed that you will never see.
metadata.truncated. And the possibility that a run you consider successful
produced no artifacts at all — nothing guarantees a run writes one.
Related
Section titled “Related”- Sessions, turns, runs — what a
runIdon an artifact refers to. - Publications — the artifact carrying a pull request.
- Follow a run live — catching
wamp.artifact.createdas it happens. - Artifacts operations — the generated endpoint reference, and the API reference for the manifest field by field.
- Events and Limits.