# Integration Steps

For common fields, string substitutions, metadata filters, caching, and execution order, see the [Agent Steps Overview](https://seclai.com/docs/agent-steps).

---

## Webhook Call Step

Makes an HTTP request to an external URL. Use this to integrate with external APIs, trigger workflows in other systems, post to messaging platforms, or send data to any HTTP endpoint.

### Fields

| Field                            | Type   | Required | Default                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| -------------------------------- | ------ | -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <code>url</code>                 | string | Yes      | —                             | The endpoint URL (must start with <code>http://</code> or <code>https://</code>). Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| <code>method</code>              | enum   | No       | <code>POST</code>             | HTTP method: <code>POST</code> or <code>PUT</code>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| <code>content_type</code>        | enum   | No       | <code>application/json</code> | Request body content type: <code>application/json</code>, <code>text/plain</code>, <code>text/html</code>, <code>application/xml</code>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| <code>headers</code>             | object | No       | <code>null</code>             | Custom HTTP headers as key-value pairs. Header values support substitutions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| <code>payload</code>             | string | No       | <code>null</code>             | The request body. If null, the step's input is sent as the body. Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| <code>payload_shape</code>       | enum   | No       | <code>json_with_urls</code>   | Multi-asset payload encoding when the step receives a [manifest input](https://seclai.com/docs/agent-steps#file-attachments): <code>json_with_urls</code> (download URLs in JSON), <code>json_with_base64</code> (inline base64, 5 MB cap), or <code>multipart_form</code> (classic multipart/form-data). Ignored for plain text/JSON inputs.                                                                                                                                                                                                                                                                                                                      |
| <code>attachment_url_mode</code> | enum   | No       | <code>presigned_s3</code>     | Active only when <code>payload_shape = json_with_urls</code>. <code>presigned_s3</code> emits 1-hour signed AWS S3 URLs the receiver can fetch without a Seclai key (no expiry control, no per-fetch audit). <code>seclai_auth</code> emits URLs that hit <code>/api/v2/agent-runs/&#123;run_id&#125;/attachments/&#123;attachment_id&#125;</code>; receivers must send an X-API-Key or Bearer token belonging to the same account — no expiry, revocable, every fetch logged. Pick <code>seclai_auth</code> for audited integrations and receivers that already hold a key; keep <code>presigned_s3</code> for low-trust short-lived consumers. |
| <code>attachments</code>         | list   | No       | <code>null</code>             | Selects which attachments are encoded into the request body — a list of <code>{`{{…}}`}</code> [attachment reference](#attachment-references) expressions (e.g. <code>{`{{attachments[*.pdf]}}`}</code>, <code>{`{{step.gen.attachments[0]}}`}</code>). Null / empty list ⇒ every attachment on the step's input manifest.                                                                                                                                                                                                                                                                                                                       |

**Prompt-injection scanning:** Webhook response bodies are automatically scanned for prompt injection attacks by the [Prompt Scanner](https://seclai.com/docs/prompt-scanner#output-scanning). If the response is flagged as unsafe, downstream steps that would consume the tainted data are blocked. The scan result appears as an **Output Scan** pseudo-step in the agent trace.

**Rate limits:** Outgoing webhook calls are rate-limited per destination host. Your plan sets a per-minute budget per host (see [Outgoing webhook calls / minute](https://seclai.com/pricing) on the plan comparison); a separate global ceiling protects each host across all accounts. When a limit is hit the step is automatically deferred and retried after a short backoff (the same handling as a `429` from the receiver), so bursts are throttled rather than failed. Build polling loops (`webhook_call → wait → webhook_call`) with a [Wait step](https://seclai.com/docs/agent-steps/control#wait-step) interval comfortably above your per-host budget.

### Receiver-side signature verification

Every outgoing webhook delivery carries two headers so the receiver can prove the request came from Seclai (and was not modified in transit):

| Header               | Value                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `X-Seclai-Signature` | `sha256=<hex>` — HMAC-SHA256 over `"{timestamp}.{body}"` with your account's signing secret       |
| `X-Seclai-Timestamp` | Unix milliseconds at the moment the delivery was signed. Reject deliveries older than ~5 minutes. |

The signing secret is per-account, fetched / rotated via:

- `GET /authenticated/accounts/{account_id}/webhook-signing-secret` — read the current secret (one-time, audited).
- `POST /authenticated/accounts/{account_id}/webhook-signing-secret/rotate` — rotate and return the new secret. Old in-flight deliveries fail verification once rotated, so coordinate with receivers.

**Python receiver example:**

```python
import hmac, hashlib, time

def verify_seclai_webhook(secret: str, headers: dict, body: bytes) -> bool:
    signature = headers.get("X-Seclai-Signature", "")
    timestamp_ms = headers.get("X-Seclai-Timestamp", "")
    if not signature.startswith("sha256=") or not timestamp_ms.isdigit():
        return False
    # Reject replays older than 5 minutes.
    if abs(time.time() * 1000 - int(timestamp_ms)) > 5 * 60 * 1000:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        f"{timestamp_ms}.".encode("utf-8") + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
```

**Node receiver example:**

```javascript
const crypto = require("crypto");

function verifySeclaiWebhook(secret, headers, body) {
  const signature = headers["x-seclai-signature"] || "";
  const timestamp = headers["x-seclai-timestamp"] || "";
  if (!signature.startsWith("sha256=") || !/^\d+$/.test(timestamp))
    return false;
  if (Math.abs(Date.now() - parseInt(timestamp, 10)) > 5 * 60 * 1000)
    return false;
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(body)
      .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

Signing is on by default — there's no per-step opt-out. Receivers that don't care about authenticity can simply ignore the headers.

### Use Case Examples

**Post to Slack:**

```
URL: https://hooks.slack.com/services/YOUR/WEBHOOK/URL
Method: POST
Content-Type: application/json
Payload:
{
  "text": "Agent {{agent.name}} completed: {{step.summary.output}}"
}
```

*Figure: Post to Slack — the agent generates a summary, formats a Slack JSON payload, and posts it via webhook.*

**Send to a custom API:**

```
URL: https://api.example.com/ingest
Method: POST
Content-Type: application/json
Headers:
  Authorization: Bearer {{metadata.api_token}}
  X-Source: seclai-agent
Payload:
{
  "agent_id": "{{agent.id}}",
  "run_id": "{{agent.run_id}}",
  "result": {{step.extract_content.output}},
  "processed_at": "{{datetime UTC}}"
}
```

**Trigger an external workflow:**

```
URL: https://automation.example.com/webhooks/{{metadata.workflow_id}}
Method: POST
Content-Type: application/json
Payload:
{
  "event": "content_processed",
  "data": "{{step.final.output}}"
}
```

*Figure: Trigger external workflow — the agent analyzes content and triggers an external automation system via webhook.*

*Figure: Webhook Notify — after processing, the agent sends results to an external system via webhook.*

---

## MCP Client Call Step

Calls a tool on an external [MCP (Model Context Protocol)](https://seclai.com/docs/mcp-clients) server and returns the tool's result as the step output. Use this when an agent needs to invoke a capability hosted on an MCP server you've connected (for example, a third-party tool server or your own internal MCP service).

Before using this step you must add an **MCP Client** under **MCP Clients** in the left navigation — that's where the server URL and authentication live. The step references the saved client by id; it does not hold any connection details or secrets itself.

### Fields

| Field      | Type            | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| MCP Client | select          | Yes      | The saved MCP client (external server) to connect to. Manage clients under **MCP Clients**.                                                                                                                                                                                                                                                                                                                              |
| Tool Name  | string          | Yes      | The name of the tool to call on the external server. Use **Refresh tools** in the editor to fetch the server's tool list, or type a name. Supports string substitutions.                                                                                                                                                                                                                                                 |
| Arguments  | key/value pairs | No       | The arguments passed to the tool. Each value supports string substitutions (e.g. `{{input}}`, `{{step.<id>.output}}`), **including [attachment references](#attachment-references)** (e.g. `{{attachments[*.pdf]}}`) — the referenced attachment's extracted **text** is inlined into that argument value (MCP arguments are JSON, so binaries are passed as text). There is no separate Attachments field on this step. |

### Output

If the tool returns structured content, the step output is that content as JSON. Otherwise the output is the tool's text result. If the external server is unreachable or rejects the call, the step fails; transient network errors are retried automatically.

---

## Web Fetch Step

Fetches a single web page and returns its content. Use this to bring external page content into an agent run when the URL is known up front.

### Fields

| Field                    | Type     | Required | Default               | Description                                                                                                                                                                     |
| ------------------------ | -------- | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>url</code>         | string   | Yes      | —                     | The URL of the page to fetch. Must start with <code>http://</code> or <code>https://</code>. Supports substitutions.                                                            |
| <code>format</code>      | enum     | No       | <code>markdown</code> | Output format: <code>markdown</code> (best for LLM consumption), <code>rawHtml</code> (original HTML), or <code>text</code> (plain text).                                       |
| <code>media_types</code> | string[] | No       | <code>[]</code>       | Media to download from the page and attach to the output. Any subset of <code>images</code>, <code>video</code>, <code>audio</code>, <code>documents</code>. Empty = text-only. |

**Caching:** Results are cached for 5 minutes per URL + format — repeated fetches of the same URL with the same format within this window return instantly without an external request, but each step run is still billed at the standard `web_fetch` rate.

**Prompt-injection scanning:** Fetched content is automatically scanned for prompt injection attacks by the [Prompt Scanner](https://seclai.com/docs/prompt-scanner#output-scanning). If the content is flagged as unsafe, downstream steps that would consume the tainted data are blocked. The scan result appears as an **Output Scan** pseudo-step in the agent trace.

**Governance:** The resolved URL is screened against step-input governance policies _before_ the fetch executes — blocking policies can prevent the request entirely. The fetched content itself is not governance-screened at this step, but when it flows into a terminal step (`display_result`, `send_email`, etc.) output governance policies evaluate it there. Additionally, the downstream `prompt_call` input scan provides injection-detection enforcement at the LLM boundary.

**Step vs. tool:** Use this step when the URL is known and you want predictable, auditable fetch behavior. For autonomous, model-driven page fetching, use the `seclai_web_tools` tool inside a [Prompt Call](https://seclai.com/docs/agent-steps/ai-generation#prompt-call-step) step instead.

**Attachments:** By default Web Fetch returns **text** (markdown / HTML / plain text), which flows downstream as <code>{`{{step.<id>.output}}`}</code> and feeds a `prompt_call` as text. Set <code>media_types</code> to also download page media and attach it: <code>images</code> pulls <code>&lt;img&gt;</code> tags (inline <code>data:</code> URIs decoded, remote URLs downloaded, obvious ads filtered); <code>video</code> / <code>audio</code> pull embedded <code>&lt;video&gt;</code> / <code>&lt;audio&gt;</code> sources; <code>documents</code> follows <code>&lt;a href&gt;</code> links to downloadable files (PDF, Office, CSV, …). When a kind is selected and at least one asset is successfully downloaded, the output becomes a multi-asset manifest (text + attachments) that downstream `prompt_call`, `send_email`, and S3 steps can consume. If no assets are found, or every candidate is skipped (failed fetch / over a size cap), the step falls back to plain text output and produces **no** `attachments` — so don't assume <code>{`{{step.<id>.attachments}}`}</code> exists merely because `media_types` was set. Note that `prompt_call` only receives attachments its configured model can ingest — routing is gated by the model's `supported_input_media` (typically images, and video / PDF on multi-modal models); other kinds (audio, non-PDF documents like Office/CSV) are dropped for `prompt_call` even though they remain fully usable for `send_email` and S3 steps. Each asset is fetched through an SSRF guard and bounded by per-asset and total size caps; assets that fail to download or exceed a cap are skipped, leaving the text body intact. Reference individual assets with <code>{`{{step.<id>.attachments[*]}}`}</code> selectors — see the [Attachments guide](https://seclai.com/docs/agent-attachments).

### Use Case Examples

**Fetch a known article and summarize it:**

```
Web Fetch:
  url: {{metadata.article_url}}
  format: markdown
  → Prompt Call: Summarize the following article in 3 bullet points.
    {{input}}
    → Streaming Result
```

**Search → fetch top result → derive insight:**

```
Web Search:
  query: {{agent.input}}
  limit: 5
  → Extract Content (json_path: "$[0].url")
    → Web Fetch:
        url: {{input}}
        format: markdown
        → Extract Data: What are the three most important takeaways?
          → Display Result
```

*Figure: Search → Extract → Fetch → Extract Data — the agent searches the web, extracts the top URL, fetches the page, and derives key takeaways.*

*Figure: Web Fetch + Summarize — the agent fetches a URL, summarizes the page content, and streams the result.*

---

## Web Search Step

Searches the web for a query and returns matching pages with titles, descriptions, and short content snippets. Use this to discover relevant pages for a topic before deciding whether to fetch any of them in full.

### Fields

| Field              | Type    | Required | Default        | Description                                        |
| ------------------ | ------- | -------- | -------------- | -------------------------------------------------- |
| <code>query</code> | string  | Yes      | —              | The search query. Supports substitutions.          |
| <code>limit</code> | integer | No       | <code>5</code> | Maximum number of search results to return (1–20). |

**Output shape:** A JSON array of result objects, each with `url`, `title`, `description`, and `content` fields. Content snippets are truncated to ~1000 characters.

**Prompt-injection scanning:** Search results are automatically scanned for prompt injection attacks by the [Prompt Scanner](https://seclai.com/docs/prompt-scanner#output-scanning). If the content is flagged as unsafe, downstream steps that would consume the tainted data are blocked. The scan result appears as an **Output Scan** pseudo-step in the agent trace.

**Step vs. tool:** Use this step when you want a predictable, deterministic search with a fixed query template and downstream processing. For autonomous, model-driven web research, use the `seclai_web_tools` tool inside a [Prompt Call](https://seclai.com/docs/agent-steps/ai-generation#prompt-call-step) step instead.

### Use Case Examples

**Search and synthesize from snippets only (no fetch):**

```
Web Search:
  query: latest developments in {{agent.input}}
  limit: 8
  → Prompt Call: Summarize what these snippets say about {{agent.input}}.
    {{input}}
    → Streaming Result
```

**Search → fetch first result → publish:**

```
Web Search:
  query: "{{metadata.topic}} site:example.com"
  limit: 3
  → Extract Content (json_path: "$[0].url")
    → Web Fetch:
        url: {{input}}
        format: markdown
        → Publish Content
```

*Figure: Search → Fetch → Publish — the agent searches for a topic on a specific site, fetches the top result, and publishes it to a content store.*

*Figure: Web research pipeline: search the web, extract the top URL, fetch the page, analyze it with Extract Data, and display the result.*

---

## Write AWS S3 Object Step

Saves content to an AWS S3 bucket. Use this to archive agent results, store generated reports, export data, or create file backups.

### Fields

| Field                            | Type    | Required | Default                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| -------------------------------- | ------- | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>bucket_name</code>         | string  | Yes      | —                       | The S3 bucket name. Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| <code>object_path</code>         | string  | No       | <code>null</code>       | Optional path prefix prepended to <code>object_key</code> (joined with a single <code>/</code>). May reference run-level substitutions (e.g. <code>{`{{agent.run_id}}`}</code>, <code>{`{{datetime UTC}}`}</code>) but MUST NOT contain per-attachment placeholders <code>{`{{name}}`}</code> / <code>{`{{index}}`}</code> / <code>{`{{ext}}`}</code> — those belong on <code>object_key</code>.                                                                     |
| <code>object_key</code>          | string  | Yes      | —                       | The key (filename) within the S3 bucket, appended after <code>object_path</code> if set. For multi-asset prompt outputs (image generation, etc.) include <code>{`{{name}}`}</code>, <code>{`{{index}}`}</code>, or <code>{`{{ext}}`}</code> so each attachment writes to a distinct key (e.g. <code>{`{{index}}-{{name}}`}</code>). Multi-asset input without a placeholder is rejected at runtime to prevent silent overwrites.                                     |
| <code>content_type</code>        | enum    | No       | <code>text/plain</code> | Content type for the stored object: <code>text/plain</code>, <code>text/html</code>, <code>application/json</code>, <code>application/xml</code>. Ignored for multi-asset prompt outputs — each attachment retains its native MIME.                                                                                                                                                                                                                                  |
| <code>content</code>             | string  | No       | <code>null</code>       | The content to write. If null, the step's input is used. Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                                     |
| <code>write_manifest_text</code> | boolean | No       | <code>false</code>      | When the input is a [multi-asset manifest](https://seclai.com/docs/agent-steps#file-attachments), also write the manifest's text body to a separate object derived from `object_key` (the placeholder pattern is replaced with `text.txt`).                                                                                                                                                                                                                                            |
| <code>attachments</code>         | list    | No       | <code>null</code>       | Selects which attachments are written — a list of <code>{`{{…}}`}</code> [attachment reference](#attachment-references) expressions (e.g. <code>{`{{attachments[*.png]}}`}</code>, <code>{`{{step.gen.attachments[0]}}`}</code>). Each match becomes its own S3 object using `object_key` as a template with the placeholders above (a multi-match without a placeholder is rejected at runtime). Null / empty list ⇒ every attachment on the step's input manifest. |

### Use Case Examples

**Store a daily report:**

```
Bucket: my-reports-bucket
Key: reports/{{date UTC}}/daily-summary.html
Content Type: text/html
Content: (uses step input — the HTML report from previous step)
```

*Figure: Daily S3 report — the agent generates analysis, formats it as JSON, and writes it to a date-partitioned S3 path.*

**Archive agent results as JSON:**

```
Bucket: agent-results
Key: agents/{{agent.id}}/runs/{{agent.run_id}}/output.json
Content Type: application/json
Content: {{step.extract_content.output}}
```

**Organize by metadata:**

```
Bucket: content-exports
Key: {{metadata.category}}/{{metadata.article_id}}/analysis.json
Content Type: application/json
```

*Figure: Metadata-organized S3 — the agent classifies content by category, then writes it to S3 using metadata-derived paths.*

*Figure: Write S3 Archive — the agent processes content and archives the result to S3 for long-term storage.*

---

## Send Email Step

Sends an email notification. Use this to deliver reports, send alerts, or notify team members about agent results. By design it can only send to **registered Seclai users in the account's organization** — not arbitrary addresses. The one exception is `reply_to_sender`, which replies to the person who emailed an [Email Received](https://seclai.com/docs/agent-triggers#email-received) agent (voluntary opt-in). See [Email → Who agents can email](https://seclai.com/docs/email#sending-policy) for the full policy and rationale.

### Fields

| Field                             | Type    | Required | Default                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------------- | ------- | -------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>recipient_user_id</code>    | string  | No       | Account owner / workspace default recipient | Optional user ID to send the email to. If omitted, delivery uses the workspace default recipient (account owner for personal accounts). If provided for org accounts, it must be a valid member UUID.                                                                                                                                                                                                                                                                     |
| <code>reply_to_sender</code>      | boolean | No       | <code>false</code>                          | **Email Received agents only.** When true, reply to the sender of the triggering email — the recipient is resolved at run time from the message's authenticated sender, so you never set an address yourself (`recipient_user_id` is ignored). Only sends when the inbound message passed SPF or DMARC; the step fails on any other trigger type or an unauthenticated sender. See [Replying to the Sender](https://seclai.com/docs/agent-triggers#email-reply-to-sender).                  |
| <code>reply_to_agent_inbox</code> | boolean | No       | <code>false</code>                          | For agents with an [Email Received](https://seclai.com/docs/agent-triggers#email-received) trigger. When true, sets the email's `Reply-To` to the agent's own inbox so the recipient's reply routes back to and re-triggers the agent. Use it on a `recipient_user_id` notification (not a reply) to enable a sustained back-and-forth; it has no effect if the agent has no email trigger. `reply_to_sender` already threads replies to the inbox, so don't combine them.                  |
| <code>subject</code>              | string  | Yes      | —                                           | The email subject line. Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                                                                           |
| <code>html_body</code>            | string  | No       | <code>null</code>                           | The HTML email body. Supports substitutions. When omitted: if the input is HTML (<code>text/html</code>) it becomes the body; if the input is Markdown (<code>text/markdown</code>) it is rendered to HTML for the body; else if a <code>text_body</code> is set, that is rendered as the body; else if attachments are present (e.g. a `for_each` image loop) with no meaningful text, the body is a clean <code>See attached.</code>; otherwise the raw input is shown. |
| <code>text_body</code>            | string  | No       | <code>null</code>                           | The plain text email body (fallback for email clients that don't support HTML). Supports substitutions.                                                                                                                                                                                                                                                                                                                                                                   |
| <code>attachments</code>          | list    | No       | <code>null</code>                           | Selects which attachments are attached to the email as RFC-5322 attachments — a list of <code>{`{{…}}`}</code> [attachment reference](#attachment-references) expressions (e.g. <code>{`{{attachments[*.pdf]}}`}</code>, <code>{`{{step.report.attachments[0]}}`}</code>). Null / empty list ⇒ every attachment on the step's input manifest. Total attachment size is capped at 25 MB (SES per-message limit); oversized runs fail loudly.                               |

**Branding:** The sender display name, logo, accent color, and footer of agent-sent emails are configured once for the whole account on the **Email Branding** settings page (co-branding on Team and Pro plans; full white-label on Pro) — not per step. Outward agent emails also carry an unsubscribe footer so recipients can opt out of that agent, of all the account's agents, or of every Seclai email; opted-out recipients are skipped automatically.

### Use Case Examples

**Daily report delivery:**

```
Subject: Daily Summary — {{date America/New_York}}
HTML Body:
<h1>Daily Summary</h1>
{{step.report.output}}
<p>Generated by {{agent.name}} at {{time America/New_York}}</p>
```

**Alert notification:**

```
Subject: Alert: {{metadata.alert_type}} detected
HTML Body:
<h2>Alert Details</h2>
<p><strong>Type:</strong> {{metadata.alert_type}}</p>
<p><strong>Source:</strong> {{metadata.source_url}}</p>
<p><strong>Details:</strong></p>
{{step.analysis.output}}
Text Body:
Alert: {{metadata.alert_type}} detected
Source: {{metadata.source_url}}
Details: {{step.analysis.output}}
```

*Figure: Alert email — the agent detects an anomaly and sends an alert notification via email.*

*Figure: Send Email Report — the agent generates a report and delivers it via email notification.*

### Inline images (CID embedding)

HTML email bodies can embed attachment images directly by referencing the attachment-placeholder grammar in an `<img>` `src` attribute:

```html
<p>Today's chart:</p>
<img src="{{step.gen.attachments[0]}}" alt="chart" />
<img src="{{attachments[*.png]}}" alt="screenshots" />
```

Under the per-message budget (20 MB cumulative inline, 10 MB per image; SES caps the whole RFC-5322 envelope at 25 MB) each referenced image is **CID-embedded** — the `<img src>` is rewritten to a `cid:` reference and the image rides inside the message as a `multipart/related` part. The recipient's mail client renders it inline without an internet fetch.

When the cumulative budget would be exceeded, remaining `<img src>` placeholders are instead rewritten to an `app.seclai.com` link the recipient OAuth-authenticates against — they click → log in → see the image in their browser. Per-image overflow (>10 MB single image, >25 MB total) emits an `EMAIL_ATTACHMENT_DROPPED_OVERSIZED` event in the trace; the dropped attachment is silently omitted from the email.

Standalone `attachments` matches still go as RFC-5322 file attachments. If the same attachment is referenced inline AND matches the `attachments` references, it is CID-embedded only (no duplicate file attachment).

**Automatic downscaling.** To keep the message renderable, a large inline image (over ~250 KB) is **downscaled to at most 1280 px and re-encoded as JPEG _for the email copy only_** — the full-resolution original is untouched in storage (trace, file attachments, S3). Because JPEG has no transparency, a transparent PNG (logo, cut-out) is **flattened onto a white background** in the email. If you need pixel-exact or transparent images, send them as `attachments` (file attachments are not downscaled) rather than inline `<img>`.

**Only raster images are inlined.** `image/png`, `image/jpeg`, `image/gif`, and `image/webp` are CID-embedded; any other type (notably `image/svg+xml`) is routed to the OAuth-authenticated `app.seclai.com` link instead, so SVG is never embedded directly in the message.

---

## Call Agent Step

Calls another agent as a sub-agent, running it synchronously and using its output as this step's output. Use this to compose complex workflows from smaller, reusable agents.

### Fields

| Field                           | Type    | Required | Default           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------- | ------- | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>agent_id</code>           | string  | Yes      | —                 | The ID of the agent to call.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| <code>pass_input</code>         | boolean | No       | <code>true</code> | When enabled, forwards this step's input as the called agent's input.                                                                                                                                                                                                                                                                                                                                                                                                       |
| <code>pass_metadata</code>      | boolean | No       | <code>true</code> | When enabled, forwards the parent agent run's metadata to the called agent.                                                                                                                                                                                                                                                                                                                                                                                                 |
| <code>content_version_id</code> | string  | No       | <code>null</code> | An optional content version ID to pass to the called agent for content-based triggers.                                                                                                                                                                                                                                                                                                                                                                                      |
| <code>attachments</code>        | list    | No       | <code>null</code> | Selects which attachments are propagated to the sub-agent's <code>\_seclai.input_attachments</code> metadata — a list of <code>{`{{…}}`}</code> [attachment reference](#attachment-references) expressions (e.g. <code>{`{{attachments}}`}</code>, <code>{`{{step.gen.attachments[0]}}`}</code>). The sub-agent's first prompt step receives the selected subset as if it were a `dynamic_input` upload. Null / empty list ⇒ every attachment on the step's input manifest. |

### Use Case Examples

**Chain agents for multi-stage processing:**

A summarization agent calls an analysis agent first, then processes its output:

```
Step 1: Call Agent (agent_id: "analysis-agent", pass_input: true, pass_metadata: true)
Step 2: Prompt Call — Summarize the analysis output
```

*Figure: Agent chaining — the outer agent calls a specialized agent for analysis, then summarizes the result.*

**Fan-out to specialized agents:**

Use parallel branches with call_agent steps to run multiple agents on the same input simultaneously:

```
Branch 1: Call Agent → "sentiment-agent"
Branch 2: Call Agent → "keyword-agent"
Merge: Combine results from both agents
```

*Figure: Multi-agent fan-out: parallel call_agent steps invoke specialized agents, then a merge combines their results.*

### Important Considerations

- **Recursion protection**: A maximum call depth is enforced to prevent infinite loops. If an agent calls itself (directly or indirectly), the run will fail once the depth limit is reached.
- **Synchronous execution**: The called agent runs to completion before the parent continues. Long-running sub-agents will increase the overall run time.
- **Credit usage**: Each sub-agent run consumes credits independently based on its own steps.

---

## List Cloud Drive Folder Step

Lists the files in a folder on a connected cloud drive and emits a **JSON array**, one entry per file (`name`, `path`, `file_id`, `size`, `is_folder`, `modified_at`). Connect a drive under **Integrations → Cloud Drives** first (Dropbox is supported today). See the [Cloud Drives guide](https://seclai.com/docs/cloud-drives) for the full walkthrough and illustrated use cases.

### Fields

| Field                                  | Type   | Required | Default            | Description                                                                                                                    |
| -------------------------------------- | ------ | -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| <code>cloud_drive_connection_id</code> | uuid   | Yes      | —                  | The connection to list files from.                                                                                             |
| <code>folder_path</code>               | string | No       | <code>""</code>    | The folder to list (blank lists the connection root). Supports substitutions.                                                  |
| <code>recursive</code>                 | bool   | No       | <code>false</code> | List nested subfolders too. Pair with <code>limit</code> — a recursive listing is bounded by a system hard cap regardless.     |
| <code>limit</code>                     | int    | No       | <code>null</code>  | Cap on entries returned (1–1000). The result is hard-capped at 1000 either way.                                                |
| <code>path_pattern</code>              | string | No       | <code>null</code>  | Case-insensitive glob (e.g. <code>\*\*/\*.pdf</code>) applied to the full path. Blank keeps everything.                        |
| <code>content_types</code>             | string | No       | <code>null</code>  | Comma-separated MIME allowlist (<code>application/pdf</code>, <code>image/\*</code>) matched against each file's guessed type. |

Filenames are attacker-influenceable, so the listing is treated as **untrusted** — its output is [prompt-injection scanned](https://seclai.com/docs/prompt-scanner#output-scanning) before any downstream `prompt_call`. Consume it with [`extract_content`](https://seclai.com/docs/agent-steps/core#extract-content-step) or [`for_each`](https://seclai.com/docs/agent-steps/control#for-each-step), not a `prompt_call`.

### Use Case Example

**Batch-process a folder:** the listing is a JSON array, so a [`for_each`](https://seclai.com/docs/agent-steps/control#for-each-step) consumes it directly — point its input at the list step's output and reference each file with `{{step.<for_each_id>.item.<field>}}`.

```
List Cloud Drive Folder (id: list): { connection: my-dropbox, folder_path: /reports }
  → For Each (id: each, input: {{step.list.output}}):   # iterates the JSON array, one file per item
      → Read Cloud Drive File: { file_path: {{step.each.item.path}} }
          → Prompt Call (id: analysis): "Analyze this report."
          → Write Cloud Drive File: { destination_path: /processed/{{step.each.item.name}}.txt, content: {{step.analysis.output}} }
```

*Figure: Batch processing — list a folder, then read each file, analyze it, and write the result back to a processed folder.*

---

## Read Cloud Drive File Step

Downloads a file by path and brings it into the workflow, mirroring [Web Fetch](#web-fetch-step). How the file arrives depends on its type:

- **Text files** (`.txt`, `.md`, `.csv`, `.json`, HTML, …) are returned as **text output** (HTML keeps its content type for an [`extract_content`](https://seclai.com/docs/agent-steps/core#extract-content-step) step).
- **Images, audio, video, PDF, DOCX, XLSX** are carried forward as a multimodal **[attachment](https://seclai.com/docs/agent-attachments)** for a downstream `prompt_call` on a vision/document model. Scannable text is **also derived** and travels alongside — documents/images via extraction (OCR), audio/video via **transcription** — so a text-only downstream model still gets the content and it is scanned.
- **Anything else** (e.g. legacy `.doc`/`.xls`) has its text extracted and returned as text output.

Because derived text is always available, the file's content reaches downstream steps even when the model can't render the binary natively. See the [Cloud Drives guide](https://seclai.com/docs/cloud-drives) for connection setup, transcription, and credits.

### Fields

| Field                                  | Type   | Required | Default           | Description                                                                                                       |
| -------------------------------------- | ------ | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| <code>cloud_drive_connection_id</code> | uuid   | Yes      | —                 | The connection to read the file from.                                                                             |
| <code>file_path</code>                 | string | Yes      | —                 | The path of the file to read (e.g. <code>/reports/summary.pdf</code>). Supports substitutions.                    |
| <code>max_file_mb</code>               | int    | No       | <code>null</code> | Per-file size cap in MB (bounded by the 100 MB platform ceiling). A larger file is refused rather than truncated. |

The read step ingests untrusted external content, so its output (including extracted text and transcripts) is [prompt-injection scanned](https://seclai.com/docs/prompt-scanner#output-scanning) before a downstream `prompt_call` runs. **Audio/video transcription** is metered as `TRANSCRIPTION` credits by audio duration (the same rate as knowledge-base ingestion) and sends the file's audio to Seclai's transcription subprocessor — see [Transcription](https://seclai.com/docs/cloud-drives#transcription).

### Use Case Examples

**Summarize a document:** read a file and hand its attachment (plus extracted text) to a model.

```
Read Cloud Drive File: { connection: my-dropbox, file_path: /reports/{{metadata.file_name}} }
  → Prompt Call: "Summarize this document in five bullet points."
  → Display Result
```

*Figure: Summarize a drive document — read the file, hand the attachment (and its extracted text) to a prompt call, and stream the summary.*

**Transcribe a voice memo:** pair a [cloud file trigger](https://seclai.com/docs/agent-triggers#cloud-file-triggers) on an audio folder with a read step so each recording is transcribed, summarized, and routed.

```
Read Cloud Drive File: { file_path: {{metadata.file_path}} }
  → Prompt Call: "Summarize this call and list action items."
  → Send Email: deliver the summary to the team
```

*Figure: Transcribe a voice memo — the audio is transcribed on read, summarized by a prompt call, and emailed to the team.*

---

## Write Cloud Drive File Step

Writes content to a file on a connected drive — a delivery/sink step (like [Write to S3](#write-s3-step)) for the end of a branch. Missing parent folders are created as needed.

### Fields

| Field                                  | Type   | Required | Default           | Description                                                                                                                             |
| -------------------------------------- | ------ | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| <code>cloud_drive_connection_id</code> | uuid   | Yes      | —                 | The connection to write the file to.                                                                                                    |
| <code>destination_path</code>          | string | Yes      | —                 | The destination path (e.g. <code>/reports/{`{{datetime UTC}}`}.txt</code>). Missing parent folders are created. Supports substitutions. |
| <code>content</code>                   | string | No       | <code>null</code> | Content to write. Blank defaults to the step input (equivalent to <code>{`{{input}}`}</code>). Supports substitutions.                  |
| <code>overwrite</code>                 | bool   | No       | <code>true</code> | Replace an existing file at the destination. When false, a name clash auto-renames the new file.                                        |
| <code>max_file_mb</code>               | int    | No       | <code>null</code> | Size cap in MB for the written content (bounded by the platform ceiling). A larger payload is refused.                                  |

### Use Case Example

**Save a generated report:** produce content with a model, then write it to the drive.

```
Prompt Call: "Write a weekly status report from {{input}}."
  → Write Cloud Drive File: { connection: my-dropbox, destination_path: /reports/{{datetime UTC}}.md, content: {{step.report.output}} }
```

*Figure: Save a generated report — a prompt call generates the content and the write step saves it to the connected drive.*

---

## Attachments references field

> New to attachments? Start with the [**Attachments guide**](https://seclai.com/docs/agent-attachments) for the high-level model — where files come from, the two ways to reference them, and worked examples. This section is the complete field reference.

Every consumer step that can ingest files — **Webhook Call**, **Write to S3**, **Send Email**, **Call Agent**, **Publish Content**, **Write Content Attachment**, [**For Each**](https://seclai.com/docs/agent-steps/control#for-each-step), [**Display Result**](https://seclai.com/docs/agent-steps/output#display-result-step), and [**Streaming Result**](https://seclai.com/docs/agent-steps/output#streaming-result-step) — exposes an optional `attachments` config field. It is a **list of `{{…}}` reference expressions** and is the mechanism for selecting which attachments the step consumes. Each expression resolves to zero or more files from a [multi-asset manifest](https://seclai.com/docs/agent-steps#file-attachments); the step unions the matches across all expressions in list order, then dedupes (first-seen wins).

The same `{{…}}` grammar already used inline in prompt templates, S3 keys, email HTML bodies, and webhook payload strings applies here:

| Reference                                               | Selects                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>{`{{attachments}}`}</code>                        | **All** attachments on this step's input manifest.                                                                                                                                                                                                                                              |
| <code>{`{{attachments[0]}}`}</code>                     | This step's input attachment at the given zero-based index.                                                                                                                                                                                                                                     |
| <code>{`{{attachments[*.pdf]}}`}</code>                 | This step's input attachments whose filename matches the glob (`*` / `?`).                                                                                                                                                                                                                      |
| <code>{`{{agent.attachments[…]}}`}</code>               | The agent's trigger-level uploads (requires a `dynamic_input` trigger). Same `[…]` index / glob forms as above.                                                                                                                                                                                 |
| <code>{`{{step.<id>.attachments[…]}}`}</code>           | A specific upstream producer step's output files — a `generate_image` / `generate_audio` / `generate_video`, `prompt_call`, `call_agent`, or a `for_each` (which aggregates the media its body produced — reference the loop id from a post-loop step). Same `[…]` index / glob forms as above. |
| <code>{`{{step.<for_each_id>.item.attachment}}`}</code> | Inside a [`for_each` running in `iterate_attachments` mode](https://seclai.com/docs/agent-steps/control#for-each-step), the current iteration's file.                                                                                                                                                             |

The filename glob in `[*.pdf]` / `[*.png]` forms uses `fnmatch` semantics (`*` matches any chars, `?` matches one); literal strings reduce to an exact name match. The index in `[0]` forms is zero-based — a negative index is invalid and out-of-range indices match nothing.

**Defaults:** `null` / omitted / empty list ⇒ every attachment on the step's input manifest — **except** the result steps (`display_result`, `streaming_result`) and `add_chat_turn`, which default to **text only** and attach files only when at least one reference is set.

**Zero matches** resolve to a no-op (logged at INFO). **Multi-match** is normally allowed — the consumer iterates (S3 writes one object per match, webhook encodes a list, email attaches each file). The two **single-attachment consumers** — `write_content_attachment` and `publish_content` — instead reject zero-or-more-than-one with a validation error; wrap them in a [`for_each` with `iterate_attachments=true`](https://seclai.com/docs/agent-steps/control#for-each-step) to fan out.

**Builder UI:** the step editor's **Attachments** field has an Add Reference button that opens an expression row with a live match-count preview against the agent's most recent successful run. Drag-handles reorder references; the Delete button has a one-click undo banner.

**Inline template references:** the same `{{attachments[…]}}` / `{{agent.attachments[…]}}` / `{{step.<id>.attachments[…]}}` placeholder family also works inside prompt templates, S3 keys, email HTML bodies, and webhook payload strings. Used in the `attachments` field, an expression selects which attachments the consumer step ingests; used inline in a string, it injects one specific attachment reference into that string.

---

[← Back to Agent Steps Overview](https://seclai.com/docs/agent-steps)
