Attachments
Agents work with files, not just text. A run can start with uploaded files, generate new files mid-workflow (an image from a prompt_call, a report from a sub-agent), and deliver files at the end (email an attachment, write to S3, render in the UI). This page is the high-level guide to how files move through an agent and how you wire them between steps. For the exhaustive field-by-field reference, see Attachments references field.
The two channels: binary and text
Every attachment reaches a step in one of two channels:
- Native binary — capable LLMs (Claude, Gemini, Nova, GPT-4o-class) receive the raw bytes as a media content block, so the model truly "sees" the image / hears the audio / reads the PDF natively.
- Converted — a step whose model cannot read the file's type receives it converted: an image to PNG or JPEG, a file to its text (OCR, document extraction or an audio/video transcript), or a PDF with no usable text to images of its pages. A file is converted once and every later step reuses the conversion. The prompt scanner and governance input policies inspect a conversion before the step sees it, and an uploaded file's text before a step reads the file in any other form; a Redact policy that changes that text sends the redacted text in place of the file. Governance also reads an image, or a file with no extractable text, as the file itself when its evaluation model accepts the type.
You don't choose between them — the runtime picks the right channel per step based on the model's capabilities (see multi-modal routing). The thing you control is which files reach which step. That is what attachment references do.
Where attachments come from
There are exactly two origins for an attachment inside a run:
| Origin | How it's produced | How to reference it |
|---|---|---|
| Agent input (uploads) | A user (or the API / MCP) uploads files when triggering a run, or an email arrives with attachments. Requires a dynamic_input or email_received trigger, whose declared attachments say which files it accepts. | {{agent.attachments.<name>}} or {{agent.attachments}} — available from any step |
| Step-generated | A prompt_call emits images/media (image-generation models or the image_generation tool), the dedicated generate_image / generate_audio / generate_video steps always emit one, or a call_agent sub-agent returns files. A for_each loop whose body generates media aggregates every per-iteration asset, so a post-loop step can attach them all via {{step.<for_each_id>.attachments}}. A read_cloud_drive_file step emits one when the file it reads is an image / audio / video / PDF / DOCX / XLSX — the binary rides as an attachment with its extracted or transcribed text alongside. Web Fetch also produces one conditionally — only when its media_types field is set and at least one asset is actually downloaded (otherwise it stays text-only). | {{step.<id>.attachments[…]}} — references that step's output |
The file that lands on a step's input (from whichever upstream step fed it) is also reachable with the bare {{attachments[…]}} form.
Sending text with the files
"A photo and a sentence about it" is one run, not two fields you have to choose between. Send input together with input_upload_id / input_upload_ids — they are not mutually exclusive:
curl -X POST https://api.seclai.com/agents/{agent_id}/runs \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Is this listing OK?", "input_upload_ids": ["<upload-id>"]}'
The prompt text leads and each file follows as its # {filename} heading, so {{agent.input}} reads as one document:
Is this listing OK?
# photo.jpg
Each file contributes only its name, text files included. Before Seclai-Version: 2026-10-16, each file's extracted text is written under its heading.
The binaries are unaffected — they still reach multi-modal steps natively as attachments, addressable with {{agent.attachments.<name>}} or {{agent.attachments}}. In the Run Agent modal, the message box stays visible on the Attach files tab for the same reason.
Send the file with no text, and a retrieval step whose query is left blank will search its knowledge base with the file itself — "which of these pages is in my photo?". A file with no extracted text, such as a raw photo, still contributes its # {filename} heading, so {{agent.input}} on such a run is "# photo.jpg" rather than empty; the retrieval step knows that heading is a label it wrote and does not treat it as your query.
The only pair that is mutually exclusive is input_upload_id and input_upload_ids — two spellings of the same batch. And you no longer need the old workaround of uploading the prompt as a synthetic input.txt text attachment; that added an upload round-trip and a spurious # input.txt heading.
There is no length limit on the combined text — the run is accepted whatever its size, and an input too large for the model surfaces as an error on the first step that calls one, not at submit time. A text-only step receives the text of every file it reads, so with 20 files keep an eye on which ones reach a text-only model; a multi-modal model reads the binaries natively.
What does not produce attachments
- Web Fetch returns text (markdown / HTML / plain text) by default. The page content flows as
{{step.<id>.output}}— feed it to aprompt_callas text. Exception: when itsmedia_typesfield selects images, video, audio, or documents, Web Fetch can produce attachments — but only when at least one asset is actually downloaded. If none are found, or every asset is skipped (over a size cap, failed fetch), the output stays text-only, so don't assume{{step.<id>.attachments}}exists just becausemedia_typeswas set; see Web Fetch. Web Search always returns text, not binary files. text,merge,extract_content,extract_data,regex_replaceand other transforms operate on text and never create files. A gate, wait, retry orregex_replacestep always passes its input's files through. Atextstep does when its template is exactly{{input}}. Amergestep does when its input carries files, it has no content type set, and its template is nothing but whole-output references of which exactly one produced output (the branch an exclusive merge ran): it passes that step's files on. Any other transform outputs text only. Past any other transform, name the producer's files directly with{{step.<id>.attachments}}.
If you need a remote file as a real binary attachment, fetch it inside a prompt_call using the web tools, or upload it as agent input.
Declared attachments
A trigger declares the files it accepts as named attachments. Only dynamic_input and email_received triggers take files; other trigger types declare none. For a walkthrough of the trigger editor, common declarations, and a worked table of which file sets run and which are refused, see Accepting Files in the Triggers chapter. Each attachment has:
| Field | Meaning |
|---|---|
name | How steps reference it. Lowercase letters, digits and underscores, starting with a letter, up to 64 characters; unique per trigger |
label | Optional name shown to people, such as Invoice |
content_types | Types it accepts, such as application/pdf or image/*. Empty accepts any type |
name_pattern | Optional filename glob the file must also match, ignoring case, such as invoice-*.pdf |
min | Fewest files a run must include (default 0); an email trigger set to run anyway lists the shortfall instead |
max | Most files it takes; empty means no limit |
A file-taking trigger created without a declaration, or whose declaration is set to null through the API or MCP, gets one open attachment, files, accepting any number of files of any type.
Steps reference the files assigned to an attachment with {{agent.attachments.<name>}}, and every agent input file with {{agent.attachments}}.
How files are assigned. Before any step runs, each incoming file is assigned to one declared attachment it fits, by type and filename pattern, and each attachment must receive at least its min files and at most its max. If a file fits no attachment with room for it, or an attachment receives fewer than its minimum, the run is refused, and the message lists each problem:
'Invoice' needs at least 1 file (application/pdf); 0 received
'brief.docx' (application/vnd…) is not an attachment this agent accepts
An email trigger can run anyway instead, leaving out the files that don't fit; see Attachments: strict or run anyway.
Where to set it. In the app, the trigger editor's Attachments step. Through the API, the trigger's accepted_attachments field when creating an agent, or PUT /api/agents/{agent_id}/triggers/{trigger_id}/accepted-attachments; through MCP, set_trigger_accepted_attachments. The AI agent builder and agent export carry it too, and so does importing an export as a new agent; importing into an existing agent keeps that agent's declaration, so set it first if the imported steps reference attachments it lacks. A step's Attachments field and the template toolbar's Agent Input menu offer the declared attachments to pick from, and the Run Agent modal lists the attachments the trigger accepts.
The reference grammar
An attachment reference is a {{…}} expression that selects zero or more files from one source. The same grammar works everywhere — inside a text field or in a step's dedicated Attachments list.
| Reference | Selects |
|---|---|
{{attachments}} | All attachments on this step's input |
{{attachments[0]}} | The input attachment at zero-based index 0 (out-of-range ⇒ no match) |
{{attachments[*.pdf]}} | Input attachments whose filename matches the glob (* = any run, ? = one char) |
{{attachments[invoice.pdf]}} | Input attachment by exact filename |
{{agent.attachments}} | Every agent input file — available from any step |
{{agent.attachments.<name>}} | The agent input files assigned to the trigger's declared attachment <name> — available from any step. An index, glob or filename form ({{agent.attachments[0]}}) is saved as every agent input file |
{{step.<id>.attachments[…]}} | Files a specific upstream step produced or passed on — a prompt_call, call_agent, generate_*, web_fetch, read_cloud_drive_file, result, gate / wait / retry step, or aggregated from a for_each loop's body ({{step.<for_each_id>.attachments}}) |
{{step.<for_each_id>.item.attachment}} | Inside a for_each in iterate_attachments mode, the current iteration's file |
Multiple references union their matches in list order and dedupe (first-seen wins). A reference that matches nothing is never an error; a filename or pattern that matches none of the files adds a step warning.
Implicit vs explicit visibility
A step only "sees" an attachment when its template declares the dependency — this keeps uploads from silently leaking into intermediate steps that don't want them.
- Implicit — referencing
{{input}},{{agent.input}}, or{{step.<id>.input|output}}pulls in all attachments carried on that source. Existing templates keep working unchanged. - Explicit — any
{{attachments[…]}}selector narrows the set. As soon as one explicit selector targets a source, it replaces the implicit "all" for that source.
Two ways to use a reference
-
Inline, in a text field — drop the placeholder into a
prompt_callprompt, an email HTML body, an S3 object key, or a webhook payload. The file is routed to that exact spot. This is how LLM-call steps (prompt_call,evaluate_step,extract_data) usually receive files.Summarize the contract in three bullet points. Contract: {{agent.attachments.contract}}{{attachments[0]}}works the same way for the first file on the step's input, and{{step.<id>.attachments}}for another step's files. -
The per-step Attachments field — file-consuming steps (
send_email,write_aws_s3_object,write_cloud_drive_file,webhook_call,call_agent,publish_content,write_content_attachment,for_each,display_result,streaming_result,add_chat_turn) expose a dedicated Attachments list of references that selects which files the step ingests. The editor builds it row-by-row with a live match-count preview.["{{attachments[*.pdf]}}", "{{step.report.attachments[0]}}"]In this list,
{{attachments}}always means the step's input files — none when its input is text only; name the trigger uploads with{{agent.attachments}}. An empty list means no files. A newsend_email,webhook_call,write_aws_s3_object,write_cloud_drive_fileorcall_agentstep starts with["{{attachments}}"](every input file); a result step (display_result/streaming_result) with no list shows every input file;publish_contentandwrite_content_attachmentrequire a reference when their input carries files.prompt_call,extract_dataandevaluate_stepaccept the list too: with one, the step receives exactly those files instead of the ones its templates reference.
What the model sees
A model that receives several files with nothing to tell them apart can only refer to them by position — "the second image" — so your prompt has to describe the ordering, and stays correct only for as long as that ordering does. To avoid that, each file an LLM-call step sends is announced by a short label immediately before it, naming the kind and the filename:
Which of these pages matches the photo?
[image: photo.jpg]
<the photo>
[image: page_7_image_0.png]
<the page image>
[image: page_9_image_0.png]
<the page image>
This is what lets you ask for a citation and get a reliable answer: "tell me which image you used" works because the model can name it.
The name comes from wherever the file came from: the original filename for an upload, the extracted asset's filename for a retrieval match, the generated filename for step-produced media.
Retrieval labels also carry the step's name and the match's position, because neither a document title nor an extracted image's filename is unique on its own — the title is shared by every match from that document, and an extracted image is usually named after its page (page_7_image_0.png) or its alt text, so two different documents can produce the same one:
[image: Annual Report.pdf (Research, match 1)]
[image: Annual Report.pdf (Research, match 2)]
[image: page_7_image_0.png (Research, page 7, match 3)]
[image: Budget.pdf (Finance, match 1)]
The step name matters because two retrieval steps feeding the same LLM call each number their own matches from 1. Search Memory steps label their results the same way. These are exactly the names the step writes into its own results, so a citation the model gives you can be looked up in the step output.
Three things to know about what appears in the label. A file with no name is sent unlabeled rather than labelled with a placeholder. A very long name is shortened to 120 characters in the label only, with the middle replaced by … so the end survives — that is where a filename carries its extension. A retrieval match's (step, page, match) suffix is never shortened away: the name in front of it is trimmed to make room instead, since the suffix is the part that tells two matches apart. The file itself keeps its full name, so reference patterns like {{attachments[*.pdf]}} and the filename on an emailed attachment are unaffected. And because filenames can come from outside your account (an uploaded file, a file dropped in a shared cloud-drive folder, a page title), line breaks and square brackets in a name are neutralised so a filename cannot forge a label for a file that was never sent. That applies everywhere a name is shown in brackets: the label, the citation a retrieval step writes into its results, the [attachment: …] marker a {{attachments[…]}} reference expands to, the text a file is replaced by when the model cannot read its type, and — in that last case — the file's type itself.
Labels are added where Seclai builds the message for you — an attachment reference on a Prompt Call, chat history replay, and the media a Prompt Call hands back to the model for review when generation tools run with iterate on. A Prompt Call using use_step_input_as_messages receives a message array you built yourself and is sent through unchanged, so if you hand-build content blocks there, label them yourself if you want citations by name.
Worked examples
- Summarize an uploaded PDF
A dynamic_input trigger accepts exactly one PDF; a prompt_call reads it inline.
Trigger: dynamic_input
accepted_attachments: [{ name: "pdf", content_types: ["application/pdf"], min: 1, max: 1 }]
→ Prompt Call:
prompt_template: "Summarize {{agent.attachments.pdf}} in 200 words."
→ Streaming Result
The trigger refuses a run without exactly one PDF, so the step never sees anything else. Run it with the file:
curl -X POST https://api.seclai.com/agents/{agent_id}/upload-input \
-H "X-API-Key: YOUR_API_KEY" -F "file=@report.pdf"
# → {"id": "<upload-id>", …}
curl -X POST https://api.seclai.com/agents/{agent_id}/runs \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"input_upload_id": "<upload-id>"}'
To take the same PDF by email, give the agent an email_received trigger with the same declaration; the steps are unchanged.
- Analyze each uploaded image
Fan out over every uploaded file, one iteration each, with a for_each in iterate_attachments mode. This is the right shape for "do X to each attached image / file" — and it works as the first step, drawing directly from the trigger uploads.
Trigger: dynamic_input
→ For Each (id: image-loop, iterate_attachments: true) # omit input_template; omit the attachments filter to take ALL uploads
body:
→ Prompt Call (id: describe):
prompt_template: "Describe {{step.image-loop.item.attachment}} and list any text you can read."
→ Item Value (id: description) # this iteration's value
child_steps: # run ONCE, after every iteration finishes
→ Prompt Call (id: digest):
prompt_template: |
Combine these per-image descriptions into one summary, grouping similar images:
{{step.image-loop.output}}
→ Display Result
Each iteration's item carries the current file's metadata (storage_key, mime, name); {{step.<for_each_id>.item.attachment}} hands the real file to the body prompt. Inside the body, the bare {{attachments[0]}} resolves to the same current file. To loop over only some uploads, declare an attachment for them on the trigger and set the attachments filter to it (e.g. ["{{agent.attachments.pdfs}}"]).
Collecting the results. When every iteration has finished, the loop's own output is the aggregated array of each iteration's Item Value, addressable as {{step.<for_each_id>.output}} — here a JSON array of descriptions, one per image. Put the steps that consume it in the for_each's child_steps (the post-loop continuation that runs once): here a digest Prompt Call folds the per-image descriptions into one summary, which a Display Result then surfaces. Swap in Send Email or Write to S3 to deliver the digest instead. See the For Each step reference.
- Generate an image and deliver it
A prompt_call on an image-generation model produces a file; downstream steps reference it via {{step.<id>.attachments[…]}}.
Prompt Call (id: gen, image-generation model):
prompt_template: "A watercolor fox in a snowy forest."
→ Send Email:
html_body: '<p>Here is your image:</p><img src="{{step.gen.attachments[0]}}" />'
In an email HTML body the placeholder is CID-embedded so it renders inline (see inline images). To store it instead, point a Write to S3 step at the same reference:
→ Write to S3:
object_key: "fox-{{index}}-{{name}}"
attachments: ["{{step.gen.attachments}}"]
Because image generation can emit several files, include a per-attachment placeholder ({{name}} / {{index}} / {{ext}}) in object_key so each file writes to a distinct object. To surface the generated image in the UI, give a Display Result step the reference ["{{step.gen.attachments}}"] (a result step with no references surfaces only its direct input's files).
- Forward uploads to a sub-agent
call_agent is a file consumer — its Attachments field is the only way files reach the sub-agent, which assigns them to the attachments its own trigger declares, as it would an upload.
Trigger: dynamic_input
accepted_attachments: [{name: "pdfs", label: "PDFs", content_types: ["application/pdf"], min: 1}]
→ Call Agent (target: "invoice-parser"):
attachments: ["{{agent.attachments.pdfs}}"]
Only the files assigned to the pdfs attachment reach the sub-agent, and the step fails if they don't fit the attachments the sub-agent's trigger declares. Files go only with the input: with pass_input off, the sub-agent receives no files whatever the list says. For a sub-agent that takes no files, leave the list empty: if the sub-agent has no Dynamic Input or Email Received trigger, the default list is saved empty and any other list that picks files is refused; if its trigger declares no attachments, it refuses the files when the step runs, and the step fails. See the Call Agent step.
- Route one specific file
References compose, so you can pick exactly one file out of a mixed batch and send it somewhere specific — e.g. email just the contract PDF while archiving everything to S3:
Prompt Call → (text and files flow downstream)
→ Send Email:
attachments: ["{{attachments[contract.pdf]}}"]
→ Write to S3:
object_key: "archive/{{index}}-{{name}}"
attachments: ["{{attachments}}"]
- Chatbot that iterates on a generated image
A conversational agent that generates an image and lets the user refine it turn by turn ("make the moon bigger", "warmer palette"). Each turn is one run; conversation memory carries the running thread, and the Add Chat Turn step returns it as a messages array that a use_step_input_as_messages Prompt Call feeds straight to the model. The key is the attachments field on the assistant turn — it persists the generated image with that turn so it replays into the next turn as a native media block the model can actually see.
Trigger: chat (conversational)
→ Add Chat Turn (conversation, key: "{{metadata.user_id}}", speaker: "user", content: "{{agent.input}}")
→ Prompt Call (id: gen, image-generation model, use_step_input_as_messages: true)
→ Display Result (attachments: ["{{step.gen.attachments}}"])
→ Add Chat Turn (conversation, key: "{{metadata.user_id}}", speaker: "assistant",
content: "Here's the image.", attachments: ["{{step.gen.attachments}}"])
On turn 2, the loaded history is replayed into the Prompt Call as a messages array in which the assistant turn names the image and the user turn after it carries the image as a media block. The model sees the picture it drew, so "make the moon bigger" now refers to something concrete — it can iterate instead of regenerating blind.
How memory carries attachments. When an Add Chat Turn step has an
attachmentsreference, each matched file is copied into memory-owned storage (so it outlives the run) and recorded with the turn. On replay — aprompt_callwithuse_step_input_as_messages: truereading that history — a user turn's files are rendered in its own message as native media, and an assistant turn's files are named in its text and sent as native media in the user turn that follows. Each file's label carries the turn it came from ([image: hero.1.png — turn 2]), since the same step produces the same file name every turn. The 20 most recent files are replayed; older ones are named with(not attached). Notes: only conversation memory (Add Chat Turn) persists attachments — not general memory (Add Memory); a turn keeps up to 10 attachments of 20 MB each (oversized files are skipped); the model must support image input natively, and remembered images add to each turn's token cost, so persist deliberately. Leaveattachmentsempty for a text-only turn.
- Two attachments, two jobs
Declare one attachment per kind of file, and each step reads only the kind it needs. An expense agent declares a required invoice PDF and up to ten optional receipts photos:
Trigger: dynamic_input (or email_received)
accepted_attachments: [
{ name: "invoice", label: "Invoice", content_types: ["application/pdf"], min: 1, max: 1 },
{ name: "receipts", label: "Receipt photos", content_types: ["image/*"], min: 0, max: 10 }
]
→ Extract Data:
prompt_template: "Extract the vendor and total.\nInvoice: {{agent.attachments.invoice}}"
→ For Each (id: receipt_loop, iterate_attachments: true, attachments: ["{{agent.attachments.receipts}}"])
body: → Prompt Call: "What was bought?\nReceipt: {{step.receipt_loop.item.attachment}}"
The files are assigned by type, so a caller doesn't say which file is which. The Triggers chapter has the full version of this agent and a table of which file sets run.
- Save email attachments to a cloud drive
An inbox that files the attachments it receives. The trigger takes any files but requires at least one, and a Write Cloud Drive File step writes each file to its own path:
Trigger: email_received
accepted_attachments: [{ name: "files", content_types: [], min: 1, max: null }]
→ Write Cloud Drive File:
destination_path: "/inbox/{{date UTC}}/{{index}}-{{name}}"
attachments: ["{{agent.attachments.files}}"]
{{name}}, {{index}} and {{ext}} are filled in per file; put {{index}} in the path whenever more than one file may be written, since two files can share a name. With no files the step would write the email body instead, which is why the declaration requires one. Leave content empty: a step with content set writes that text and no files.
- Reply to the sender with a generated file
An email agent turns a request into a chart and sends it back. The reply's Attachments list names the generated file:
Trigger: email_received
accepted_attachments: []
→ Generate Image (id: chart):
prompt_template: "A bar chart of: {{agent.input}}"
→ Send Email:
reply_to_sender: true
subject: "Re: {{metadata.email_subject}}"
text_body: "Here is your chart."
attachments: ["{{step.chart.attachments}}"]
Use the same reference in a Webhook Call's or Write to S3 step's list to deliver the file elsewhere.
- Send files fetched from a page to a webhook
A Web Fetch step with media_types set downloads the page's files; a Webhook Call passes them on:
Web Fetch (id: fetch, url: "{{agent.input}}", media_types: ["documents"])
→ Webhook Call:
url: "https://example.com/ingest"
payload_shape: json_with_urls
attachments: ["{{step.fetch.attachments}}"]
With json_with_urls the receiver gets a download URL for each file; attachment_url_mode chooses between short-lived signed URLs and URLs that need a Seclai API key. When the page had no files to download, the Web Fetch step has none, and the webhook sends no files.
Per-step cheat sheet
| Step | Role | How it handles attachments |
|---|---|---|
| Prompt Call | Both | Consumes via inline references in the prompt/system template, or exactly the files its Attachments list names; produces files on image-gen models / the image_generation tool |
| Extract Data / Evaluate Step | Consumes | Inline references in the prompt, or an Attachments list, same as Prompt Call |
| For Each | Consumes | iterate_attachments: true loops once per file; body uses {{step.<id>.item.attachment}} |
| Send Email | Consumes | Attachments field → RFC-5322 file attachments; inline <img src> placeholders → CID-embedded images |
| Write to S3 | Consumes | Attachments field → one S3 object per match (use {{name}}/{{index}}/{{ext}} in object_key) |
| Webhook Call | Consumes | Attachments field → encoded into the request body per payload_shape |
| Call Agent | Both | Attachments field forwards files to the sub-agent, only with pass_input on; sub-agent outputs may carry files back |
| Write Cloud Drive File | Consumes | Attachments field → one file per match at destination_path (use {{name}}/{{index}}/{{ext}}); with content set it writes that text and no files |
| Read Cloud Drive File | Produces | An image, audio, video, PDF, DOCX or XLSX file it reads becomes its attachment, with the file's text as its output |
| Publish Content / Write Content Attachment | Consumes | Attachments field — single-file consumers (reject zero or >1 match; wrap in a for_each to fan out) |
| Display Result / Streaming Result | Consumes | Attachments field — default is the files the step's input carries. On Display Result, references replace that default; Streaming Result's saved output uses its references only while streaming, and not when its parent prompt_call outputs files |
| Web Fetch | Produces | Text by default; produces files only when media_types is set and ≥1 asset downloads — else text-only (no attachments). No cap on asset count; a page that needs too many separate downloads stops partway rather than dropping the lot |
| Web Search | Neither | Returns text, not attachments — consume via {{step.<id>.output}} |
| Add Chat Turn | Consumes | Attachments field persists files with the turn; they replay as native media when the history is fed to a use_step_input_as_messages prompt |
| Add / Load / Search Memory (general) | Neither | Text only — general memory stores text; for files use Add Chat Turn (conversation) or store the binary in S3 and keep its URL as text |
Validation, uploads, and limits
Three gates check attachments:
- Editor save rejects a reference on a step type that can't route binaries, a
{{step.<id>.attachments}}against a step that doesn't produce files, and an{{agent.attachments.<name>}}naming an attachment the trigger does not declare (undeclared_attachment). - Upload time rejects a file (
POST /agents/{agent_id}/upload-input, 400) that no declared attachment accepts. - Run time assigns every file to a declared attachment and checks each attachment's
minandmaxbefore any step runs; see Declared attachments. An email-triggered agent can instead run anyway and be told what didn't fit; see Attachments: strict or run anyway.
Uploaded files are also prompt-scanned: a text file with the run's input, where an unsafe one fails the input scan closed, and any other file through its text before a step reads it, where an unsafe one (e.g. a jailbreak embedded in a PDF) fails that step. Per-file limits: 200 MB per uploaded file; 25 MB total per email (SES); 5 MB for base64 webhook payloads. See File Attachments on the Agents page for the full upload / API / MCP flow and the endpoints that return an agent's declared attachments.
Input file retention
An uploaded input binary is part of the run's trace, so it is kept only as long as the run trace itself — i.e. your account's agent-trace retention (the free default is 7 days; set the retention on the system-managed agent-traces source connection, or keep traces forever). After that window the binary is swept, along with the text made from it. Practically: replaying a run (the "run again with the same files" affordance) re-attaches the original uploads while they're still present — and each replay refreshes the retention clock on the files it reuses, so a file stays available as long as it keeps getting replayed (a blob is only swept once no run has referenced it within the retention window). The replayed files are re-resolved server-side from the original run, so this is automatic and safe — there's nothing to re-upload. Replay is available from the UI, the API, and MCP.
Further reading
- Attachments references field — the complete reference table and per-consumer behaviour.
- File Attachments (Agent Steps) — selector syntax and multi-modal routing internals.
- File Attachments (Agents) — uploading files to a run, discovering an agent's declared attachments, and downloading run outputs.
- For Each step — iterating over attachments.