Stage files for a turn to work on and get the references to attach.
Sometimes the answer isn't in the warehouse — it's in a CSV the user just dropped into your UI. Uploads is how that file reaches the agent.
It is a two-step flow, and the second step is the one people miss: uploading a file does not hand it to the agent. Upload returns references; you attach those references to the next turn.
graph LR
A["Your UI<br/>user picks a file"] -->|"multipart/form-data"| B["POST /projects/{projectId}/uploads"]
B -->|"returns filePath references"| C["Your backend"]
C -->|"files: [ ... ] on the request"| D["POST /stream/agent_ask"]
D --> E["Agent reads the file<br/>in the thread workspace"]
Endpoints Overview
| Endpoint | What it does |
|---|---|
POST /v2/projects/{projectId}/uploads | Stage one or more files and return their references. |
Basic Usage
Step 1 — stage the bytes
Send multipart/form-data with one or more file parts:
curl --request POST \
--url https://cloud.getwren.ai/api/v2/projects/1/uploads \
--header 'Authorization: Bearer <API_KEY>' \
--form '[email protected]' \
--form '[email protected]'{
"uploadSessionId": "6f1c8a2e-77b3-4c1d-9a55-1e0b7c3d2f10",
"files": [
{
"fileName": "q3-targets.csv",
"filePath": "staging/1/6f1c8a2e-77b3-4c1d-9a55-1e0b7c3d2f10/q3-targets.csv",
"fileSize": 20481,
"fileType": "text/csv"
},
{
"fileName": "notes.md",
"filePath": "staging/1/6f1c8a2e-77b3-4c1d-9a55-1e0b7c3d2f10/notes.md",
"fileSize": 812,
"fileType": "text/markdown"
}
]
}Step 2 — attach them to a turn
Pass the entries back as files, with filePath verbatim:
{
"projectId": 1,
"question": "Compare the Q3 targets in this CSV against actual revenue.",
"files": [
{
"fileName": "q3-targets.csv",
"filePath": "staging/1/6f1c8a2e-77b3-4c1d-9a55-1e0b7c3d2f10/q3-targets.csv",
"fileType": "text/csv"
}
]
}The turn moves the file into the thread's workspace, and the agent can open it:
event: tool_call
data: {"block_id":2,"id":"toolu_01C","name":"Read","input":{"file_path":"q3-targets.csv"}}
Send a turn's attachments in one request
Every file in one upload request shares a single uploadSessionId. A turn moves one upload session into the thread — so a files array spanning two sessions is rejected with 400 rather than silently dropping the extras.
// ✅ Correct — one request, one session, all files reach the agent.
const { files } = await uploadFiles(projectId, [csv, notes]);
await streamAgentAsk({ projectId, question, files });
// ❌ Wrong — two sessions; the second file would be dropped, so this is rejected.
const a = await uploadFiles(projectId, [csv]);
const b = await uploadFiles(projectId, [notes]);
await streamAgentAsk({ projectId, question, files: [...a.files, ...b.files] });Limits
| Extensions | .csv, .doc, .docx, .pdf, .xls, .xlsx, .sql, .yaml, .yml, .md, .json, .txt, .zip |
| Files per request | 10 |
| Size per file | Set by AGENT_FILE_UPLOAD_MAX_SIZE_MB, default 10 MB. Exceeding it returns 413. |
Lifecycle
Uploaded bytes are staging only. The turn moves them into the thread workspace, after which the agent copies what it needs — so they survive a sandbox restart and remain available to later turns in the same thread.
An upload never referenced by a turn stays in staging. How long it is kept before cleanup is set by your deployment's staging lifecycle policy.
Uploads are for giving the agent a file to work on now. To manage the project's durable knowledge and modeling files, use the Files API instead.
