File uploads
Get user files into a task's workspace at three points — task creation, task update, and comments.
Three endpoints accept file attachments: task creation, task update, and comments. Each accepts either JSON (no files) or multipart (with files), chosen by content-type; the multipart form field for files is attachments. Uploaded files land under the task workspace at /task/uploads/<id>_<safe-name>, where <id> is a runtime-assigned identifier and <safe-name> is the sanitized original filename. The attachment metadata merges into the board item's or comment's metadata.attachments.
Per-file size cap: 64 MB. For anything larger, mount it instead of uploading — see S3 mounts.
At task creation
Use when the user submits a task with files attached.
import { createProjectTaskBoardItemWithAttachments } from "@wacht/backend";
const task = await createProjectTaskBoardItemWithAttachments(
projectId,
{
title: "Review these contracts",
description: "Flag IP, auto-renewal, and termination clauses.",
},
[
{ filename: "contract-a.pdf", content: bufA, contentType: "application/pdf" },
{ filename: "contract-b.pdf", content: bufB, contentType: "application/pdf" },
],
);use wacht::api::ai::actor_projects::WachtFileUpload;
use wacht::models::CreateProjectTaskBoardItemRequest;
let task = wacht::try_get_client()?
.ai()
.actor_projects()
.create_board_item_with_attachments(
project_id,
CreateProjectTaskBoardItemRequest {
title: "Review these contracts".into(),
description: Some("Flag IP, auto-renewal, and termination clauses.".into()),
..Default::default()
},
vec![
WachtFileUpload {
filename: "contract-a.pdf".into(),
content_type: Some("application/pdf".into()),
bytes: bytes_a,
},
WachtFileUpload {
filename: "contract-b.pdf".into(),
content_type: Some("application/pdf".into()),
bytes: bytes_b,
},
],
)
.send()
.await?;When the agent runs, files appear at /task/uploads/<id>_<safe-name>. The agent reads them with read_file (or processes them in code_runner). The same list is in task.metadata.attachments, so your UI can render the attachments without listing the filesystem.
The agent does not automatically know which uploads matter for the task — tell it in the prompt:
Files attached to the task are under /task/uploads/. Read each one with
read_file, identify flagged clauses, and write findings to
/task/artifacts/review-<filename>.md. Mark the task completed when every
input is reviewed.At task update
Add more files to an existing task without changing other fields.
import { updateProjectTaskBoardItemWithAttachments } from "@wacht/backend";
await updateProjectTaskBoardItemWithAttachments(
projectId,
taskId,
{},
[{ filename: "addendum.pdf", content: buf, contentType: "application/pdf" }],
);use wacht::api::ai::actor_projects::WachtFileUpload;
use wacht::models::UpdateProjectTaskBoardItemRequest;
wacht::try_get_client()?
.ai()
.actor_projects()
.update_board_item_with_attachments(
project_id,
task_id,
UpdateProjectTaskBoardItemRequest::default(),
vec![WachtFileUpload {
filename: "addendum.pdf".into(),
content_type: Some("application/pdf".into()),
bytes,
}],
)
.send()
.await?;The new attachments append to metadata.attachments; existing entries are preserved.
On a comment
Use when the user adds context (file + note) to an in-progress task. Posting a comment also preempts any active assignment, so the agent's next iteration sees the new context.
import { createProjectTaskBoardItemCommentWithAttachments } from "@wacht/backend";
await createProjectTaskBoardItemCommentWithAttachments(
projectId,
taskId,
actorId,
"Use this updated spec instead.",
[{ filename: "spec-v2.md", content: buf, contentType: "text/markdown" }],
);use wacht::api::ai::actor_projects::WachtFileUpload;
wacht::try_get_client()?
.ai()
.actor_projects()
.create_board_item_comment_with_attachments(
project_id,
task_id,
actor_id,
"Use this updated spec instead.",
vec![WachtFileUpload {
filename: "spec-v2.md".into(),
content_type: Some("text/markdown".into()),
bytes,
}],
)
.send()
.await?;The comment is stored with attachments in its metadata.attachments. The agent's prompt on the next iteration includes the comment body and can see the new files at /task/uploads/.
S3 mounts for large inputs
When the file is huge (hours of video, a large dataset) or already lives in your S3, don't upload — mount it. The runtime makes the S3 object visible to the agent as a regular file without copying it into the workspace. mount_path is where the agent sees it, s3_relative_key is the key in your configured bucket, and mode is ro or rw.
import { createProjectTaskBoardItem } from "@wacht/backend";
await createProjectTaskBoardItem(projectId, {
title: "Annotate the demo recording",
description: "Mark up chapters.",
mounts: [
{
mount_path: "/task/source",
s3_relative_key: "demos/2026-q2/full-recording.mp4",
mode: "ro",
},
],
});use serde_json::json;
use wacht::models::{CreateProjectTaskBoardItemRequest, ScheduleMount};
let mount: ScheduleMount = serde_json::from_value(json!({
"mount_path": "/task/source",
"s3_relative_key": "demos/2026-q2/full-recording.mp4",
"mode": "ro",
}))?;
wacht::try_get_client()?
.ai()
.actor_projects()
.create_board_item(
project_id,
CreateProjectTaskBoardItemRequest {
title: "Annotate the demo recording".into(),
description: Some("Mark up chapters.".into()),
mounts: Some(vec![mount]),
..Default::default()
},
)
.send()
.await?;Use this for any input larger than the upload cap, or when the same source is referenced by many tasks.
What the attachment metadata looks like
After upload, each attachment surfaces in metadata.attachments:
{
"attachments": [
{
"path": "/task/uploads/1735781234_contract-a.pdf",
"name": "1735781234_contract-a.pdf",
"original_name": "contract-a.pdf",
"mime_type": "application/pdf",
"size_bytes": 142307
}
]
}Read it from task.metadata.attachments or comment.metadata.attachments in your UI. The path is what the agent's filesystem tool will see.
Where to go next
- Workspace and artifacts — how the agent reads what you uploaded
- Deliverables — how the agent reports back
- Realtime UI — surfacing live state to the user