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.
| Tool | Direction | What it does |
|---|---|---|
docx_write | produce | Generate a Word document from a title and markdown body. |
xlsx_write | produce | Generate an Excel workbook from one or more typed sheet specifications. |
pdf_write | produce | Render a PDF from HTML or markdown. |
docx_read | consume | Extract text and headings from a previously emitted .docx. |
xlsx_read | consume | Read headers and rows from each sheet of a previously emitted .xlsx. |
pdf_read | consume | Extract 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:
- Renders the document bytes.
- Uploads them to the platform's artifacts bucket under the object key
jobs/{job_id}/artifacts/{artifact_id}/{filename}. - Deduplicates by sha256 — byte-identical content emitted twice in the same job yields a single object, a single id.
- Mints a freshly-signed download URL with a short TTL.
- Emits an
artifact.createdevent into the live stream. - 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:
| Extension | MIME |
|---|---|
.docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
.xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
.pdf | application/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 node | Injected metadata |
|---|---|
docx_write | (none beyond what the agent passed through) |
xlsx_write | sheet_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": "..." } }.
| Status | Code | When |
|---|---|---|
| 302 | — | Success — follow the redirect. |
| 401 | unauthorized | No or invalid auth token. |
| 403 | unauthorized | Authenticated, but not authorised to read this job. |
| 404 | artifact_not_found | The response exists, but no artifact with this id is attached. |
| 404 | response_not_found | The response id does not belong to this agent. |
| 410 | response_not_found | The 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:
- Key on
item.id(which equalsartifact.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. - 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. - Show
source_nodeas a human-readable hint when useful:docx_write→ "Word document",xlsx_write→ "Spreadsheet",pdf_write→ "PDF". - Ignore the matching
response.output_item.donefor artifacts — it carries the same payload and is only useful as a "fully settled" marker if your state machine cares. - Wire every download click through the platform endpoint — never reuse the
downloadUrlfrom the live stream past itsexpiresAt.
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
| Limit | Value |
|---|---|
| Download URL TTL | 5 minutes |
| Filename max length (post-sanitise) | 200 characters |
| XLSX rows per sheet (reader cap) | 10 000 |
| Per-job artifact dedup | sha256-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
- Wiring an Artifact-Capable Workflow — declaring the artifact tools on a workflow so the agent can call them.
- Sending Documents to the Agent — uploading docx/xlsx/pdf inputs the agent can read via
s3_key. - Responses API — the streaming surface artifacts ride on.
- Streaming overview — choosing between Chat Completions, Responses, and the NodeStreamEvent envelope.