Documentation

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:

  1. 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.
  2. 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:

OriginHow it's producedHow 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-generatedA 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 a prompt_call as text. Exception: when its media_types field 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 because media_types was 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.

ReferenceSelects
{{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

  1. Inline, in a text field — drop the placeholder into a prompt_call prompt, 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.
    
  2. 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

  1. Summarize an uploaded PDF

A dynamic_input trigger accepts one PDF; a prompt_call reads it inline.

Triggerdynamic input · PDFPrompt Call{{attachments[0]}}Streaming Result
Figure 1.An uploaded PDF arrives on the trigger; the Prompt Call's inline {{attachments[0]}} routes the file's bytes into the model, and the summary streams out.
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.

  1. 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.

Triggerdynamic input · imagesFor Eachiterate_attachmentsPrompt Calldescribe file 1Prompt Callfile 2Prompt Callfile 3 … NPrompt Callcombine digestDisplay Result
Figure 2.A For Each in iterate_attachments mode runs its body once per uploaded file (each body Prompt Call gets that iteration's real file via {{step.<loop>.item.attachment}}); a post-loop step then reads the aggregated array {{step.<loop>.output}} and combines the per-image results into one digest.
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.

  1. 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[…]}}.

TriggerPrompt Callgenerate imageSend Emailinline imageWrite to S3archive fileDisplay Result
Figure 3.One image-generating Prompt Call is the producer; each downstream consumer references {{step.gen.attachments}} to deliver the same file — inline in an email, archived to S3, and rendered in the UI.
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).

  1. 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.

Triggerdynamic inputCall Agent{{agent.attachments[*.pdf]}}
Figure 4.Call Agent's Attachments field selects which uploads to forward; the {{agent.attachments[*.pdf]}} reference sends only the PDFs to the sub-agent's first step.
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.

  1. 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:

TriggerPrompt CallSend Emailcontract.pdf onlyWrite to S3archive all
Figure 5.The same manifest flows to both consumers; Send Email narrows to {{attachments[contract.pdf]}} while Write to S3 takes {{attachments}} (every file).
Prompt Call → (manifest flows downstream)
  → Send Email:
      attachments: ["{{attachments[contract.pdf]}}"]
  → Write to S3:
      object_key: "archive/{{index}}-{{name}}"
      attachments: ["{{attachments}}"]

  1. 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.

TriggerchatAdd Chat Turnspeaker: userPrompt Callimage model · messagesDisplay ResultAdd Chat Turnassistant + image
Figure 6.Each turn records the user message, generates an image from the running thread, and persists that image onto the assistant turn (attachments) so the next turn replays it as a native media block the model can 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 attachments reference, each matched file is copied into memory-owned storage (so it outlives the run) and recorded with the turn. On replay — a prompt_call with use_step_input_as_messages: true reading 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). Leave attachments empty for a text-only turn.

Per-step cheat sheet

StepRoleHow it handles attachments
Prompt CallBothConsumes via inline {{attachments[…]}} in the prompt/system template; produces files on image-gen models / the image_generation tool
Extract Data / Evaluate StepConsumesInline {{attachments[…]}} placeholders in the prompt, same as Prompt Call
For EachConsumesiterate_attachments: true loops once per file; body uses {{step.<id>.item.attachment}}
Send EmailConsumesAttachments field → RFC-5322 file attachments; inline <img src> placeholders → CID-embedded images
Write to S3ConsumesAttachments field → one S3 object per match (use {{name}}/{{index}}/{{ext}} in object_key)
Webhook CallConsumesAttachments field → encoded into the request body per payload_shape
Call AgentBothAttachments field forwards files to the sub-agent; sub-agent outputs may carry files back
Publish Content / Write Content AttachmentConsumesAttachments field — single-file consumers (reject zero or >1 match; wrap in a for_each to fan out)
Display Result / Streaming ResultConsumesAttachments field — default is text only; attach files only when a reference is set
Web FetchProducesText by default; produces a manifest only when media_types is set and ≥1 asset downloads — else text-only (no attachments)
Web SearchNeitherReturns text, not attachments — consume via {{step.<id>.output}}
Add Chat TurnConsumesAttachments 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)NeitherText 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