Attach Files to an Agent
This tutorial walks through the full flow for letting an agent read a document you provide at runtime:
- Upload the file once with
POST /v1/files. - Reference its
file_idfrom a Responses API request via aninput_filecontent part. - The agent automatically calls the right read tool (
docx_read,xlsx_read, orpdf_read) on its own.
The wire shape is the same as OpenAI's Files API — if you already point an OpenAI SDK at our backend, file uploads Just Work.
artifact_id or s3_key?- You want the agent to read a file you have on hand → use
file_id(this tutorial). It's the only flow that gives you org-scoped, audited storage. - The file was emitted by the agent earlier in the same run → use
artifact_id. Zero config, the agent's read tools already know about it. - You have a raw S3 key from the legacy
templates/flow → still works but deprecated for one release cycle. Migrate tofile_id.
Prerequisites
- A platform API token with
file:writeandfile:readabilities (anyapi-secret-scope token has both). - An agent on the platform whose tool list includes
docx_read_tool/xlsx_read_tool/pdf_read_tool. See Wiring an Artifact-Capable Workflow if you need to add them.
End-to-end flow
The signed URL never travels through SQS and is never written to disk on the worker — it is minted just-in-time and consumed once. SQS redelivery on retry gets a fresh URL automatically.
1. Upload the file
POST /v1/files is a standard multipart upload. Two form fields are required: file (the bytes) and purpose (always user_data in v1).
Python
import httpx
with open("quarterly-report.docx", "rb") as f:
response = httpx.post(
"https://api.alpha.alien.club/v1/files",
headers={"Authorization": "Bearer oat_YOUR_TOKEN"},
files={"file": (
"quarterly-report.docx",
f.read(),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)},
data={"purpose": "user_data"},
)
response.raise_for_status()
file_id = response.json()["id"]
print(file_id) # file_8pauo0KQA-g_Bi7LHx5RA
TypeScript
const formData = new FormData()
formData.append("file", await fileFromPath("./quarterly-report.docx"))
formData.append("purpose", "user_data")
const res = await fetch("https://api.alpha.alien.club/v1/files", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PLATFORM_TOKEN}` },
body: formData,
})
const { id: fileId } = await res.json()
cURL
curl -X POST https://api.alpha.alien.club/v1/files \
-H "Authorization: Bearer oat_YOUR_TOKEN" \
-F "file=@quarterly-report.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
-F "purpose=user_data"
Response shape
{
"id": "file_8pauo0KQA-g_Bi7LHx5RA",
"object": "file",
"bytes": 36682,
"created_at": 1781702181,
"filename": "quarterly-report.docx",
"purpose": "user_data",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
id is what you pass to the Responses API. created_at is unix seconds (OpenAI convention).
Limits and allowlist
| Constraint | Value |
|---|---|
| Max file size | 50 MB |
| Allowed MIME types | application/pdf, …wordprocessingml.document (.docx), …spreadsheetml.sheet (.xlsx), text/markdown, text/plain |
| Allowed extensions | pdf, docx, xlsx, md, txt |
| MIME detection | From bytes (magic number), not the Content-Type header |
The backend sniffs the file's magic number to determine the real MIME type — relabelling a binary as application/pdf does not bypass the allowlist. Uploads whose detected MIME falls outside the list are rejected with 422.
2. Reference the file in a Responses API request
Use OpenAI's standard input_file content part. Mix it with input_text parts in the same content array:
POST /agent/{agent_id}/responses
Authorization: Bearer oat_YOUR_TOKEN
Content-Type: application/json
{
"model": "claude-sonnet-4-6",
"input": [{
"role": "user",
"content": [
{ "type": "input_file", "file_id": "file_8pauo0KQA-g_Bi7LHx5RA" },
{ "type": "input_text", "text": "Summarise the attached quarterly report in one paragraph." }
]
}],
"stream": true
}
That's it. The agent's prompt is automatically prefixed with a structured preamble listing each attached file (file_id, filename, mime_type) so the model can call the matching read tool directly.
What the agent sees
Behind the scenes the model receives something like:
The following files have been attached to this request and can be read via the artifact read tools
(docx_read / xlsx_read / pdf_read) by passing the matching `file_id` argument:
- file_id=file_8pauo0KQA-g_Bi7LHx5RA (filename=quarterly-report.docx, mime_type=application/vnd.openxmlformats-officedocument.wordprocessingml.document)
User request:
Summarise the attached quarterly report in one paragraph.
The agent issues docx_read_tool({"file_id": "file_8pauo0KQA-g_Bi7LHx5RA"}) on its own — you do not have to instruct it explicitly.
Multiple files in a single turn
Pass as many input_file parts as you need:
"content": [
{ "type": "input_file", "file_id": "file_aaa..." },
{ "type": "input_file", "file_id": "file_bbb..." },
{ "type": "input_file", "file_id": "file_ccc..." },
{ "type": "input_text", "text": "Compare the three reports and find the common KPIs." }
]
The agent typically issues the read tool calls in parallel.
Unsupported reference modes
OpenAI's full surface also accepts file_url (remote URL) and file_data (inline base64). Both are rejected by the backend today with a 400 and the OpenAI-shaped error code:
{
"error": {
"message": "input_file 'file_url' mode is not supported, use file_id with a /v1/files upload",
"type": "invalid_request_error",
"code": "unsupported_file_reference_mode"
}
}
The wire shape is kept spec-compatible so SDKs that emit those modes get a clean, parseable error.
3. Stream the response
The Responses API streams Server-Sent Events. The interesting frames for this flow:
| Event | When |
|---|---|
response.created | Stream opened, the backend committed Job.inputFileIds |
response.output_item.added (type: function_call) | Agent decided to call a read tool |
response.function_call_arguments.done | Arguments finalised, e.g. {"file_id": "file_…"} |
response.tool_call.output | Read tool returned (text + headings for docx, sheets for xlsx, markdown for pdf) |
response.output_text.delta | Model's response tokens |
response.completed | Terminal — full response in payload.response |
Minimal Python consumer:
import httpx
import json
with httpx.stream(
"POST",
"https://api.alpha.alien.club/agent/231/responses",
headers={"Authorization": "Bearer oat_YOUR_TOKEN", "Content-Type": "application/json"},
json={
"model": "claude-sonnet-4-6",
"input": [{
"role": "user",
"content": [
{"type": "input_file", "file_id": file_id},
{"type": "input_text", "text": "Summarise this."},
],
}],
"stream": True,
},
timeout=httpx.Timeout(connect=10.0, read=240.0),
) as response:
for line in response.iter_lines():
if line.startswith("data:"):
payload = json.loads(line[5:].strip())
if payload.get("type") == "response.completed":
final = payload["response"]
print(final["output"][-1]["content"][0]["text"])
break
Managing uploaded files
The Files API endpoints mirror OpenAI's full surface:
| Endpoint | Description |
|---|---|
POST /v1/files | Upload a new file |
GET /v1/files | List the calling org's files (filter with ?purpose=user_data) |
GET /v1/files/{file_id} | Get a single file's metadata |
GET /v1/files/{file_id}/content | 302-redirect to a freshly-signed S3 URL for downloading the bytes |
DELETE /v1/files/{file_id} | Hard-delete (S3 object first, then DB row) |
# List
files = httpx.get(
"https://api.alpha.alien.club/v1/files",
headers={"Authorization": "Bearer oat_YOUR_TOKEN"},
).json()["data"]
# Download
download = httpx.get(
f"https://api.alpha.alien.club/v1/files/{file_id}/content",
headers={"Authorization": "Bearer oat_YOUR_TOKEN"},
follow_redirects=True,
)
# Bytes available on download.content
# Delete
httpx.delete(
f"https://api.alpha.alien.club/v1/files/{file_id}",
headers={"Authorization": "Bearer oat_YOUR_TOKEN"},
)
Lifecycle
- Files persist until you call
DELETEor until the owning organization is deleted (cascade). - There is no automatic TTL. If you only need a file for a single run, delete it after the response completes.
- Re-uploading the same bytes returns a new
file_id— there is no automatic dedup. If you need stable references, cache thefile_idon your side.
Org scoping
Files are scoped to the uploader's current organization. A file uploaded while you have org A active is invisible to org B, even for the same user. Cross-org access returns 404 (not 403) to prevent existence enumeration.
Security model
The properties you can rely on:
- You never hand the agent platform-level credentials. The backend mints a single-use, short-lived presigned URL for the worker; the model only ever sees the opaque
file_id. - MIME is detected from bytes, not the
Content-Typeheader — a malicious client cannot smuggle a non-allowlisted format past the upload boundary. - Filename sanitisation strips path separators before constructing the S3 key. The key is always
files/{org_id}/{file_id}/{filename}; a client cannot influence the{org_id}segment. - Two authorisation checks per file, one at
/v1/responsescreation and one at worker dispatch — a file revoked or deleted between the two returns the worker a clean error rather than a dangling URL. - Read tools are extraction-only: macros in
.docx, formulas in.xlsx, and JavaScript in.pdfare not evaluated.
Troubleshooting
The agent says it doesn't have docx_read available.
The target agent's workflow needs to include the artifact read tools. See Wiring an Artifact-Capable Workflow.
POST /v1/files returns 422.
Either the file exceeded 50 MB, the extension is not on the allowlist, or the magic-number sniff detected something outside the allowed MIME types. The error body lists which validator failed.
POST /agent/:id/responses returns 404 with code file_not_found.
The file_id does not exist for your current organization. Switch organizations or re-upload.
POST /agent/:id/responses returns 400 with code unsupported_file_reference_mode.
You sent an input_file part with file_url or file_data. Switch to file_id (the only supported mode in v1).
The worker raises Backend failed to resolve N input file(s).
The file was deleted (or org membership revoked) between job creation and SQS pickup. The job fails fast rather than running with a partial input set — re-upload and retry.
See also
- Sending Documents to the Agent — full reference for both
file_idand the legacys3_keyflow - Agent Artifacts — the read/write tools and the
artifact_idflow for files the agent itself produces - Wiring an Artifact-Capable Workflow — how to enable the read tools on a workflow
- Responses API — the streaming request/response shape