File Upload
Ask the user for files that meet stated rules (type, size, aspect ratio), checked before they're sent.
≈ 731 tokens per use (tool 430 · guidance 204 · typical payload 97)How token costs are estimated
Example: File Upload
Launch the autumn blend ad on Instagram Stories and Feed.
Autumn blend creatives
When to use
- The agent needs files from the user to continue: ad creatives, a receipt, a profile photo, a document to review.
- The files have rules the agent can state up front: type, size or image shape.
When not to use
- The file is already in a connected system. Fetch it instead.
- The agent only needs a link or a short piece of text. Ask in text.
How the agent should use it
- Use one slot per distinct file, with the platform's real requirements, e.g. Story 9:16 and Feed 1:1.
- State
accept,maxSizeMBandaspectRatioinstead of describing them in prose. - Never ask for identity documents, payment cards or other sensitive files.
- The agent receives metadata only. The host passes the files to its tools; the agent shouldn't claim to have seen the contents.
Interaction
Each slot is a drop zone and a button: drag files onto it, or press it (Enter or Space) to browse. Every file is checked on arrival:
| Rule | Check |
|---|---|
accept | MIME type (image/png), wildcard (image/*) or extension (.pdf; .jpg also matches .jpeg) |
maxSizeMB | File size |
aspectRatio | The image's real dimensions, within ±2% |
Files that fail stay in the list with the reason, e.g. "This image is 1080×1080. It needs to be 9:16, e.g. 1080×1920", until removed. Images get a preview (object URLs, revoked on remove and unmount). A single-file slot replaces its file when a new one arrives. On submit, a missing required file or a failing file moves focus to that slot. Additions, rejections and removals are announced to screen readers.
The checks are plain functions exported from the schema file (validateUpload, matchesAccept, matchesAspectRatio, parseAspectRatio), so the server can run the same rules on the uploaded files.
Installation
npx shadcn@latest add https://your-site.com/r/file-upload.json
Usage
import { tool } from "ai"
import { fileUploadTool } from "@/components/genui/file-upload.schema"
const tools = {
// No execute(): the call is completed by the user's upload in the UI.
[fileUploadTool.name]: tool({
description: fileUploadTool.description,
inputSchema: fileUploadTool.inputSchema,
}),
}File objects can't be sent to the agent, so the component hands them to the host with onFiles (once per slot, before onResult). Upload them there and give your tools a reference:
import { FileUpload } from "@/components/genui/file-upload"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-request_files") return null
return (
<FileUpload
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onFiles={(slotId, files) => uploadFiles(part.toolCallId, slotId, files)}
onResult={(result) =>
addToolResult({
tool: "request_files",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{
action: "submit",
files: [
{ slotId: "story", name: "autumn-story.png", type: "image/png", size: 1843200, width: 1080, height: 1920 },
{ slotId: "feed", name: "autumn-feed.jpg", type: "image/jpeg", size: 902144, width: 1080, height: 1080 },
],
}
{ action: "cancel" }With LangGraph
Push it from a graph node with ui.push({ name: "request_files", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer. Upload the files from onFiles to storage your graph can read.
Restoring from history
Pass the stored result back as result. The component lists the submitted file names and sizes, without previews (the files are no longer in the browser), and never calls onResult again.
<FileUpload
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string | What's being asked for: "Upload ad creatives" |
description | string? | One short line under the title. Skip it when it restates the title |
slots | UploadSlot[] | 1–6 |
submitLabel | string? | Verb for the submit button. Default "Submit" |
UploadSlot
| Field | Type | Notes |
|---|---|---|
id | string | Returned with each file as slotId |
label | string | "Story image" |
description | string? | Guidance on what to upload. Read to screen readers with the drop zone, not shown; the accepted types, ratio and size limit are always shown |
accept | string[] | MIME types or extensions |
maxSizeMB | number? | |
aspectRatio | string? | width:height, e.g. 9:16. ±2% is accepted |
required | boolean | Default true |
multiple | boolean | Default false |
Host props: status (streaming, ready, submitting, submitted, cancelled), onFiles, onResult, result and className.