Skip to main content

Agent Artifacts

The Alien deep-agent can produce and consume downloadable documents — Word (.docx), Excel (.xlsx), and PDF (.pdf) — as a native part of any run. Artifacts are uploaded to object storage the moment they're generated, streamed live to your client through the Responses API, and persisted on the final job result for later retrieval.

The artifact bytes never enter the LLM context. Only a typed reference (filename, mime type, size, sha256, source) does. This keeps token usage bounded regardless of file size and prevents the model from accidentally regurgitating document contents.

The six tools

The agent has six tools at its disposal — three writers and three readers — and chooses to invoke them autonomously based on the user's prompt.

ToolDirectionWhat it does
docx_writeproduceGenerate a Word document from a title and markdown body.
xlsx_writeproduceGenerate an Excel workbook from one or more typed sheet specifications.
pdf_writeproduceRender a PDF from HTML or markdown.
docx_readconsumeExtract text and headings from a previously emitted .docx.
xlsx_readconsumeRead headers and rows from each sheet of a previously emitted .xlsx.
pdf_readconsumeExtract markdown text and page count from a previously emitted .pdf.

Read tools can target either an artifact_id produced earlier in the same run, or a raw s3_key for cross-job reads (long-running agents that span multiple jobs).

Lifecycle of a produced artifact

When the agent calls a write tool, the worker:

  1. Renders the document bytes.
  2. Uploads them to the platform's artifacts bucket under the object key jobs/{job_id}/artifacts/{artifact_id}/{filename}.
  3. Deduplicates by sha256 — byte-identical content emitted twice in the same job yields a single object, a single id.
  4. Mints a freshly-signed download URL with a short TTL.
  5. Emits an artifact.created event into the live stream.
  6. Appends the artifact reference to the job's persisted result.

By the time your client sees the event, the file is already in storage and downloadable. Artifacts are atomic — there are no partial bytes, no progress chunks, no incremental delivery.

Receiving artifacts over the Responses API

Artifacts are surfaced as a custom output item on the Responses API stream. The item type is "artifact" — a non-standard extension to OpenAI's output item taxonomy. Strict OpenAI SDK clients ignore unknown item types, so your standard text-streaming code path is unaffected; you only react to artifacts in code paths that opt in.

For each artifact, the stream emits exactly two events in immediate succession:

event: response.output_item.added
data: {
"type":"response.output_item.added",
"sequence_number":<n>,
"output_index":<k>,
"item": {
"id":"art_01HXK3...",
"type":"artifact",
"artifact": { ...ArtifactRef... },
"downloadUrl":"https://...signed-s3-url...",
"expiresAt":"2026-06-11T12:05:00.000Z",
"status":"completed"
}
}

event: response.output_item.done
data: { ...identical item, same id, same output_index... }

The ArtifactRef shape

interface ArtifactRef {
id: string // "art_" + ULID, stable per job + per sha256
job_id: number // owning Job.id
filename: string // sanitised, ≤ 200 chars, no path separators
mime_type: string // "application/vnd...docx" | "...xlsx" | "application/pdf"
s3_key: string // jobs/{job_id}/artifacts/{id}/{filename}
size_bytes: number
sha256: string // hex, 64 chars
created_at: string // ISO 8601 UTC
source_node: string // "docx_write" | "xlsx_write" | "pdf_write"
metadata: Record<string, unknown> // node-specific; see below
}

MIME types you'll see on the wire:

ExtensionMIME
.docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.pdfapplication/pdf

Per-node metadata keys

The metadata field is a free-form map, but the writer nodes inject stable keys you can rely on:

Source nodeInjected metadata
docx_write(none beyond what the agent passed through)
xlsx_writesheet_names: string[] — workbook sheets in declaration order.
pdf_write(none beyond what the agent passed through)

Treat any other keys as opaque.

Downloading an artifact

Each artifact.created event carries a downloadUrl that is hot for five minutes. For UIs displaying long-lived chat history, this URL will expire long before the user clicks it — so you should never store or reuse it.

Instead, route every download click through the platform's artifact download endpoint:

GET /agent/:agent_id/responses/:response_id/artifacts/:artifact_id

The endpoint returns an HTTP 302 redirect to a freshly-signed S3 URL with Content-Disposition: attachment forced (browsers always download, never render — neutralising any HTML/SVG rendering concerns). Each hit mints a new presigned URL with the same 5-minute TTL.

The redirect carries Cache-Control: no-store — do not cache the target.

cURL

curl -L \
-H "Authorization: Bearer <your-access-token>" \
https://api.alien.club/agent/<agent_id>/responses/<response_id>/artifacts/<artifact_id> \
-o report.docx

TypeScript (browser)

function downloadArtifact(agentId: string, responseId: string, artifactId: string) {
window.location.href =
`/api/agent/${agentId}/responses/${responseId}/artifacts/${artifactId}`
// → 302 → presigned S3 → browser downloads with original filename
}

Errors

The endpoint returns OpenAI-shaped error envelopes: { "error": { "message": "...", "type": "invalid_request_error", "code": "..." } }.

StatusCodeWhen
302Success — follow the redirect.
401unauthorizedNo or invalid auth token.
403unauthorizedAuthenticated, but not authorised to read this job.
404artifact_not_foundThe response exists, but no artifact with this id is attached.
404response_not_foundThe response id does not belong to this agent.
410response_not_foundThe response expired from server storage, or the job was deleted.

If the job crashed after uploading an artifact but before persisting its final result, the download endpoint transparently replays the live event stream to recover the artifact metadata — you do not need to handle this case in your client.

Retrieving artifacts from the persisted job

For batch workflows, dashboards, or any consumer that doesn't need a live stream, every produced artifact is mirrored onto the job's final result:

{
"result": {
"artifacts": [
{
"id": "art_01HXK3...",
"job_id": 12345,
"filename": "Q3-report.docx",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"s3_key": "jobs/12345/artifacts/art_01HXK3.../Q3-report.docx",
"size_bytes": 28714,
"sha256": "...",
"created_at": "2026-06-11T12:00:00.000Z",
"source_node": "docx_write",
"metadata": { }
}
]
}
}

The list reflects emission order and is per-job deduplicated by sha256. Use the download endpoint (above) to fetch any of them.

Rendering artifacts in your UI

Artifacts are best surfaced as download chips alongside the streamed text — atomic objects, not inline previews. A few patterns worth applying:

  1. Key on item.id (which equals artifact.id) for React lists and deduplication. The server already deduplicates by sha256 within a job; your UI just needs to be idempotent across event replays.
  2. Branch your icon on mime_type, not on the filename extension. Filenames are user-facing (sanitised but otherwise arbitrary); the MIME type is the authoritative format signal.
  3. Show source_node as a human-readable hint when useful: docx_write → "Word document", xlsx_write → "Spreadsheet", pdf_write → "PDF".
  4. Ignore the matching response.output_item.done for artifacts — it carries the same payload and is only useful as a "fully settled" marker if your state machine cares.
  5. Wire every download click through the platform endpoint — never reuse the downloadUrl from the live stream past its expiresAt.

Minimal TypeScript handler

for await (const event of stream) {
if (
event.type === "response.output_item.added" &&
event.item?.type === "artifact"
) {
addArtifactChip({
id: event.item.id,
filename: event.item.artifact.filename,
mimeType: event.item.artifact.mime_type,
sizeBytes: event.item.artifact.size_bytes,
sourceNode: event.item.artifact.source_node,
})
}
}

Limits and guarantees

LimitValue
Download URL TTL5 minutes
Filename max length (post-sanitise)200 characters
XLSX rows per sheet (reader cap)10 000
Per-job artifact dedupsha256-based
Filename character set (post-sanitise)A-Za-z0-9._- only; collapses to "artifact" if empty

When xlsx_read hits the per-sheet row cap, it returns the truncated row set and sets truncated: true on that sheet's result — your client should surface this to the user.

See also