# Email Attachments

Read and delete email attachment files captured by email-trigger workflows.

These methods are available in [Email Trigger](/triggers/email-trigger/) workflows. When an email arrives with file attachments, each attachment descriptor in `data.attachments` includes a `key`. Use that key to retrieve and clean up the file in a later retryable step.

## Attachment descriptor shape

```js
{
  filename:    "invoice.pdf",
  contentType: "application/octet-stream",
  key:         "shop-name/workflow-id/email-<uuid>/invoice.pdf"
}
```

## api.getAttachment(key)

Retrieves an attachment by its storage key and returns the raw file content.

```js
const buffer = await api.getAttachment(key);
```

**Parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `key` | `string` | The `key` value from `data.attachments[n].key` |

**Returns** `Promise<ArrayBuffer \| null>`

Returns an `ArrayBuffer` containing the raw file bytes, or `null` if the key does not exist or has expired.

**Example**

```js
async processAttachment(data, _headers, api) {
  const buffer = await api.getAttachment(data.key);

  if (!buffer) {
    console.log('Attachment not found or expired:', data.key);
    return;
  }

  console.log(`Got ${buffer.byteLength} bytes: ${data.filename}`);

  // Work with the buffer
  const blob    = new Blob([buffer], { type: data.contentType });
  const text    = new TextDecoder().decode(buffer);   // if text-based
}
```

`api.getAttachment()` returns the `ArrayBuffer` directly. Do not read a `.buffer` property from its result.

## api.deleteAttachment(key)

Deletes an attachment from storage. Safe to call even if the key no longer exists. It is a no-op in that case.

```js
await api.deleteAttachment(key);
```

**Parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `key` | `string` | The `key` value from `data.attachments[n].key` |

**Returns** `Promise<void>`

**Example**

```js
async processAttachment(data, _headers, api) {
  const buffer = await api.getAttachment(data.key);
  if (!buffer) return;

  // ... process the file ...

  // Clean up once done, don't leave files in storage indefinitely
  await api.deleteAttachment(data.key);
}
```

## Attachment retention

Attachments are stored for **7 days** from when the email was received. After that, `api.getAttachment()` returns `null` for those keys. Plan your workflow so that any attachment processing happens well within that window.

## Attachment size guidance

This page does not define a hard attachment size limit. However, `api.getAttachment()` reads the full file into an `ArrayBuffer`, so very large attachments are still a poor fit for normal step processing.

Use attachments for small to moderate files. If a workflow needs to handle large CSV-style inputs, move quickly into a chunked processing pattern and never pass raw attachment buffers between steps.

## The retryable step pattern

The `start` step is **not retryable**. If an error occurs mid-execution, `start` does not run again, which means you cannot re-fetch the attachment there safely. The correct pattern is to pass the `key` string (not the file content) into a subsequent step:

```js
export class Workflow {
  async start(data, _headers, api) {
    // Pass keys to retryable steps, do NOT read file content here
    for (const att of data.attachments) {
      await api.scheduleNextStep({
        delay: 10,
        action:  'handleFile',
        payload: { key: att.key, filename: att.filename, contentType: att.contentType }
      });
    }
  }

  async handleFile(data, _headers, api) {
    // This step is retryable, safe to call api.getAttachment() here
    const buffer = await api.getAttachment(data.key);
    if (!buffer) {
      console.log('File unavailable:', data.key);
      return;
    }
    // ... process and then clean up ...
    await api.deleteAttachment(data.key);
  }
}
```

Pass only the `key` into the scheduled payload. Do not pass the full attachment buffer between steps.

> Note: `api.getAttachment()` and `api.deleteAttachment()` are only available in email-trigger workflows. Calling them in other trigger types throws an error.

> Caution: `api.getAttachment()` and `api.deleteAttachment()` are not available inside `onWorkflowComplete` or `onWorkflowError` lifecycle hooks.