Skip to content

Artifacts

After this page you can find everything a run produced, fetch and verify its bytes, and write code that keeps working after the bytes are gone — because the record outlives the content by design.

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…",
"state": "available",
"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.
state available or pruned.
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.

available ──▶ pruned

available means the bytes are fetchable. pruned means they have been reclaimed and are gone for good — there is no un-prune, and no cold-storage retrieval behind it.

The manifest survives pruning. That asymmetry is the contract:

You call available pruned
GET …/artifacts listed listed, with state: "pruned"
GET …/artifacts/{artifactId} 200 200, with state: "pruned"
GET …/artifacts/{artifactId}/content 200 + bytes 410 cloud_artifact_pruned

So a 410 on the content endpoint is not a bug in your code and not a missing resource. It is the documented answer to “fetch something whose bytes were reclaimed”, and any long-lived integration will meet it.

The retention window is not documented. There is a pruning policy, but no duration is published, and you should not infer one from observation. The correct engineering response is to copy the bytes you care about into your own storage at the moment you learn the artifact exists, and to treat your copy as authoritative afterwards. Keep the sha256 alongside it so you can prove the two are the same object.

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 the bytes are guaranteed present, which makes it the right place to trigger a copy. 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.

Retention is bounded per file and per run, and the caps are enforced when the artifact is registered, not when you read it:

Cap Value
One presented file 32 MiB
All presented files from one run 64 MiB

An output that would exceed either 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.

Terminal window
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 sha256

The response is raw bytes — not base64, not wrapped in JSON — with these headers:

Header Value
Content-Type The artifact’s own type.
Content-Length Matches the manifest’s size.
ETag "<sha256>" — the same digest as the manifest.
Cache-Control private, no-store
Content-Security-Policy sandbox
X-Content-Type-Options nosniff
Content-Disposition attachment with the file name, when metadata.fileName is present.

Two things follow from those headers. First, the digest is free: compare ETag or the manifest’s sha256 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.

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. 410 cloud_artifact_pruned, at any age. 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.