# 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](https://seclai.com/docs/agent-steps/integration#attachment-references).

## 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](https://seclai.com/docs/prompt-scanner) and [governance](https://seclai.com/docs/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](https://seclai.com/docs/agent-steps#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](https://seclai.com/docs/agent-triggers).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | <code>{`{{agent.attachments[…]}}`}</code> — available from any step           |
| **Step-generated**        | A [`prompt_call`](https://seclai.com/docs/agent-steps/ai-generation#prompt-call-step) 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`](https://seclai.com/docs/agent-steps/integration#call-agent-step) sub-agent returns files. A [`for_each`](https://seclai.com/docs/agent-steps/control#for-each-step) loop whose body generates media **aggregates** every per-iteration asset, so a post-loop step can attach them all via <code>{`{{step.<for_each_id>.attachments}}`}</code>. A [`read_cloud_drive_file`](https://seclai.com/docs/agent-steps/integration#read-cloud-drive-file-step) 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](https://seclai.com/docs/agent-steps/integration#web-fetch-step) 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). | <code>{`{{step.<id>.attachments[…]}}`}</code> — 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 <code>{`{{attachments[…]}}`}</code> form.

### What does not produce attachments

- [**Web Fetch**](https://seclai.com/docs/agent-steps/integration#web-fetch-step) returns **text** (markdown / HTML / plain text) by default. The page content flows as <code>{`{{step.<id>.output}}`}</code> — feed it to a `prompt_call` as text. _Exception:_ when its <code>media_types</code> 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 <code>{`{{step.<id>.attachments}}`}</code> exists just because `media_types` was set; see [Web Fetch](https://seclai.com/docs/agent-steps/integration#web-fetch-step). **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 <code>{`{{…}}`}</code> 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                                                                                                                                                                   |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>{`{{attachments}}`}</code>                        | **All** attachments on this step's input                                                                                                                                  |
| <code>{`{{attachments[0]}}`}</code>                     | The input attachment at zero-based index `0` (out-of-range ⇒ no match)                                                                                                    |
| <code>{`{{attachments[*.pdf]}}`}</code>                 | Input attachments whose filename matches the glob (`*` = any run, `?` = one char)                                                                                         |
| <code>{`{{attachments[invoice.pdf]}}`}</code>           | Input attachment by exact filename                                                                                                                                        |
| <code>{`{{agent.attachments[…]}}`}</code>               | The original **trigger uploads** — available from any step, same index / glob forms                                                                                       |
| <code>{`{{step.<id>.attachments[…]}}`}</code>           | 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}}`) |
| <code>{`{{step.<for_each_id>.item.attachment}}`}</code> | Inside a [`for_each` in `iterate_attachments` mode](https://seclai.com/docs/agent-steps/control#for-each-step), 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 <code>{`{{input}}`}</code>, <code>{`{{agent.input}}`}</code>, or <code>{`{{step.<id>.input|output}}`}</code> pulls in **all** attachments carried on that source. Existing templates keep working unchanged.
- **Explicit** — any <code>{`{{attachments[…]}}`}</code> 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.

   ```text
   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.

   ```json
   ["{{attachments[*.pdf]}}", "{{step.report.attachments[0]}}"]
   ```

   Leaving the list empty defaults to "every attachment on the step's input" — except the [result steps](https://seclai.com/docs/agent-steps/output) (`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.

*Figure: 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.*

```text
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.

### 2. 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.

```text
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`); <code>{`{{step.<for_each_id>.item.attachment}}`}</code> 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 <code>{`{{step.<for_each_id>.output}}`}</code> — 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](https://seclai.com/docs/agent-steps/control#for-each-step).

### 3. Generate an image and deliver it

A `prompt_call` on an image-generation model **produces** a file; downstream steps reference it via <code>{`{{step.<id>.attachments[…]}}`}</code>.

*Figure: 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.*

```text
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](https://seclai.com/docs/agent-steps/integration#send-email-inline-images)). To store it instead, point a [Write to S3](https://seclai.com/docs/agent-steps/integration#write-s3-step) step at the same reference:

```text
  → 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](https://seclai.com/docs/agent-steps/output#display-result-step) step the reference `["{{step.gen.attachments}}"]` (result steps attach nothing by default).

### 4. 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.

*Figure: 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.*

```text
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](https://seclai.com/docs/agent-steps/integration#call-agent-step).

### 5. 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:

*Figure: The same manifest flows to both consumers; Send Email narrows to {{attachments[contract.pdf]}} while Write to S3 takes {{attachments}} (every file).*

```text
Prompt Call → (manifest flows downstream)
  → Send Email:
      attachments: ["{{attachments[contract.pdf]}}"]
  → Write to S3:
      object_key: "archive/{{index}}-{{name}}"
      attachments: ["{{attachments}}"]
```

### 6. 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](https://seclai.com/docs/agent-steps/memory#memory-write-step) carries the running thread, and the [Add Chat Turn](https://seclai.com/docs/agent-steps/memory#memory-write-step) 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.

*Figure: 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.*

```text
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.

<blockquote>

**How memory carries attachments.** When an [Add Chat Turn](https://seclai.com/docs/agent-steps/memory#memory-write-step) 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.

</blockquote>

## Per-step cheat sheet

| Step                                                                                                 | Role     | How it handles attachments                                                                                                                       |
| ---------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [**Prompt Call**](https://seclai.com/docs/agent-steps/ai-generation#prompt-call-step)                                  | Both     | Consumes via **inline** `{{attachments[…]}}` in the prompt/system template; **produces** files on image-gen models / the `image_generation` tool |
| [**Extract Data**](https://seclai.com/docs/agent-steps/core#extract-data-step) / **Evaluate Step**                     | Consumes | Inline `{{attachments[…]}}` placeholders in the prompt, same as Prompt Call                                                                      |
| [**For Each**](https://seclai.com/docs/agent-steps/control#for-each-step)                                              | Consumes | `iterate_attachments: true` loops once per file; body uses `{{step.<id>.item.attachment}}`                                                       |
| [**Send Email**](https://seclai.com/docs/agent-steps/integration#send-email-step)                                      | Consumes | **Attachments** field → RFC-5322 file attachments; inline `<img src>` placeholders → CID-embedded images                                         |
| [**Write to S3**](https://seclai.com/docs/agent-steps/integration#write-s3-step)                                       | Consumes | **Attachments** field → one S3 object per match (use `{{name}}`/`{{index}}`/`{{ext}}` in `object_key`)                                           |
| [**Webhook Call**](https://seclai.com/docs/agent-steps/integration#webhook-call-step)                                  | Consumes | **Attachments** field → encoded into the request body per `payload_shape`                                                                        |
| [**Call Agent**](https://seclai.com/docs/agent-steps/integration#call-agent-step)                                      | Both     | **Attachments** field forwards files to the sub-agent; sub-agent outputs may carry files back                                                    |
| [**Publish Content**](https://seclai.com/docs/agent-steps/content#publish-content-step) / **Write Content Attachment** | Consumes | **Attachments** field — **single-file** consumers (reject zero or >1 match; wrap in a `for_each` to fan out)                                     |
| [**Display Result**](https://seclai.com/docs/agent-steps/output#display-result-step) / **Streaming Result**            | Consumes | **Attachments** field — default is **text only**; attach files only when a reference is set                                                      |
| [**Web Fetch**](https://seclai.com/docs/agent-steps/integration#web-fetch-step)                                        | Produces | Text by default; produces a manifest only when <code>media_types</code> 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**](https://seclai.com/docs/agent-steps/memory#memory-write-step)                                      | 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)](https://seclai.com/docs/agent-steps/memory#memory-write-step)               | 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](https://seclai.com/docs/prompt-scanner) 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](https://seclai.com/docs/agents#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](https://seclai.com/docs/agent-traces) (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](https://seclai.com/docs/api), and [MCP](https://seclai.com/docs/mcp).

## Further reading

- [Attachments references field](https://seclai.com/docs/agent-steps/integration#attachment-references) — the complete reference table and per-consumer behaviour.
- [File Attachments (Agent Steps)](https://seclai.com/docs/agent-steps#file-attachments) — selector syntax and multi-modal routing internals.
- [File Attachments (Agents)](https://seclai.com/docs/agents#file-attachments) — uploading files to a run, the attachment contract, and downloading run outputs.
- [For Each step](https://seclai.com/docs/agent-steps/control#for-each-step) — iterating over attachments.
