GuidesFiles and durable work

Publish and download Artifacts

Publish an Artifact when an Agent-created file must leave its private Sandbox workspace and become an application output. This guide uses a stateful Turn, then verifies the Artifact's filename, media type, and exact downloaded bytes.

Outcome

The Agent will create and publish release.md. Your backend will find it in the producing Session, download it, and assert its complete contract.

Before you begin

You need a file-enabled Agent, its Sandbox, and a backend SDK client. Choose a stable Session and Attribution scope so your application can find the intended output later.

Create and publish a file

Ask for both actions explicitly: writing creates mutable workspace state; only save_artifacts publishes the selected path as an immutable Artifact.

const expected = "# Release\n\nReady to ship.\n";
const turn = await client.chat({
  agentId,
  userId: "end-user-42",
  message: { id: crypto.randomUUID(), role: "user", parts: [{
    type: "text",
    text: `Write release.md with exactly ${JSON.stringify(expected)}, then use save_artifacts to publish release.md.`,
  }] },
});
const sessionId = await turn.sessionId;
for await (const _chunk of turn.toUIMessageStream()) { /* drain */ }

const page = await client.artifacts.list(agentId, {
  sessionId,
  userId: "end-user-42",
});
const artifact = page.data.find((item) => item.filename === "release.md");
if (!artifact) throw new Error("release.md was not published");

const download = await client.artifacts.download(agentId, artifact.artifactId);
const bytes = new Uint8Array(await download.arrayBuffer());
const expectedBytes = new TextEncoder().encode(expected);
if (artifact.filename !== "release.md") throw new Error("filename differs");
if (artifact.mediaType !== "text/markdown") throw new Error("media type differs");
if (download.headers.get("content-type") !== artifact.mediaType) throw new Error("header differs");
if (bytes.length !== expectedBytes.length
  || !bytes.every((byte, index) => byte === expectedBytes[index])) throw new Error("bytes differ");

List the Session Artifacts

client.artifacts.list() is Agent-scoped and accepts sessionId, userId, includeDeleted, and cursor filters. Matching both the producing Session and its Attribution narrows this example to the intended output.

Download from the backend

Download through an authenticated backend

client.artifacts.download() sends a tenant credential. Keep that credential out of browser and mobile code; proxy the bytes through your backend under your application's own access rules.

Download returns a standard Response. Keep the list item's filename and mediaType, and consume the response body once as text, bytes, or a stream.

Verify the download

The assertions verify the stored filename, stored media type, download Content-Type, and exact body. The server also forces attachment behavior and sends X-Content-Type-Options: nosniff.

Delete an Artifact

Delete an output when your retention policy no longer needs it:

await client.artifacts.delete(agentId, artifact.artifactId);
const tombstones = await client.artifacts.list(agentId, { includeDeleted: true });
console.log(tombstones.data.find((item) => item.artifactId === artifact.artifactId)?.deletedAt);

The delete returns after the product row is tombstoned and durable cleanup is recorded. Normal lists omit it, includeDeleted: true exposes deletedAt, and later download or repeated delete returns HTTP 410.

Production notes

  • Every Artifact belongs to one tenant, Agent, and stateful Session and inherits the Session's userId and metadata. Attribution filters data; it is not authorization.
  • Artifact bytes live in private object storage. Tenant credentials can access the tenant's resources, so enforce End-user access in your backend.
  • Artifact content is immutable. Publish a new Artifact to replace a file.
  • Deletion keeps a tombstone, does not rewrite the Session transcript, and reclaims private bytes asynchronously through durable cleanup.
  • One save_artifacts call accepts at most 10 paths, 10 MiB per file, and 25 MiB total. A Session can hold at most 100 active Artifacts. See limits and reliability.

SDK and REST reference

On this page