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 travels through the agent in two parallel 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.
- Extracted text — every file is also OCR'd or transcribed. That text is what a text-only model sees, and what the prompt scanner and governance evaluators inspect.
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. Requires a dynamic_input trigger. | {{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.
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 (a multi-asset manifest) — 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_replace, gates, and other transforms operate on text and pass attachments through untouched if their input carried any; they never create new ones.
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.
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[…]}} | The original trigger uploads — available from any step, same index / glob forms |
{{step.<id>.attachments[…]}} | Files emitted by a specific upstream prompt_call / call_agent / generate_* 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 a no-op (logged at INFO), never an error.
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) receive files — there is no separate field; the placeholder is the trigger.Summarize {{attachments[0]}} in three bullet points. -
The per-step Attachments field — manifest-consuming steps (
send_email,write_aws_s3_object,webhook_call,call_agent,publish_content,write_content_attachment,for_each,display_result,streaming_result) 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]}}"]Leaving the list empty defaults to "every attachment on the step's input" — except the result steps (
display_result/streaming_result), where the default is text only (attach nothing unless a reference is set).
Worked examples
- Summarize an uploaded PDF
A dynamic_input trigger accepts one PDF; a prompt_call reads it inline.
Trigger: dynamic_input
→ Prompt Call:
prompt_template: "Summarize {{attachments[0]}} in 200 words."
→ Streaming Result
The bare {{attachments[0]}} resolves to the first file on the prompt step's input — the uploaded PDF. No Attachments field needed; the inline placeholder routes the binary.
- 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."
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. Add an attachments filter (e.g. ["{{agent.attachments[*.pdf]}}"]) only when the user explicitly restricts the set — never narrow "image" to a single extension.
Collecting the results. When every iteration has finished, the loop's own output is the aggregated array of its body outputs, addressable as {{step.<for_each_id>.output}} — for a single-step body it's a JSON array of strings (one per image); for a multi-step body it's an array of objects keyed by body-step id. 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}}"] (result steps attach nothing by default).
- Forward uploads to a sub-agent
call_agent is a manifest consumer — its Attachments field chooses which files to forward to the sub-agent, which receives them as if they were its own dynamic_input uploads.
Trigger: dynamic_input
→ Call Agent (target: "invoice-parser"):
attachments: ["{{agent.attachments[*.pdf]}}"]
Only the PDFs among the uploads are propagated to the sub-agent's first prompt step. 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 → (manifest flows 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 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 — turns that carried files are rendered as multi-block messages (text + native media), exactly as if the file had been uploaded on that turn. 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 (typically just the latest generated image). Leaveattachmentsempty for a text-only turn.
Per-step cheat sheet
| Step | Role | How it handles attachments |
|---|---|---|
| Prompt Call | Both | Consumes via inline {{attachments[…]}} in the prompt/system template; produces files on image-gen models / the image_generation tool |
| Extract Data / Evaluate Step | Consumes | Inline {{attachments[…]}} placeholders in the prompt, 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; sub-agent outputs may carry files back |
| 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 text only; attach files only when a reference is set |
| Web Fetch | Produces | Text by default; produces a manifest only when media_types is set and ≥1 asset downloads — else text-only (no attachments) |
| 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
Saved agents carry a static attachment-reference contract derived from every step's templates, which drives three safety gates:
- Editor save rejects a reference on a step type that can't route binaries, or a
{{step.<id>.attachments}}against a step that doesn't produce a manifest. - Upload time rejects files when the agent declares no attachment references at all (nothing would consume them).
- Run time requires the uploaded batch to satisfy the references: every exact filename must be present, the highest declared index must be in range, and every glob must match at least one file.
Uploaded files are also prompt-scanned in parallel — an unsafe attachment (e.g. a jailbreak embedded in a PDF) fails the run's input scan closed. 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 contract-introspection endpoints.
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 and only its extracted text remains. 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). Replay a run whose files have already been swept and any lost image falls back to its extracted text (documents stay readable as text; raw images are dropped). 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, the attachment contract, and downloading run outputs.
- For Each step — iterating over attachments.