# Email Trigger

Trigger a workflow when an email is sent to your workflow's dedicated address.

The Email trigger gives your workflow a dedicated email address. Whenever an email arrives at that address, the workflow fires with the parsed email contents (sender, subject, body, and any attachments) available in `data`.

## The trigger address

Each email-trigger workflow is assigned a unique address in the format:

```
{shop-name}.{workflow-id}@jsworkflow.com
```

The address is shown on the workflow detail page under **Trigger email address**.

> Caution: Treat the trigger address as a secret. Anyone who knows it can trigger your workflow by sending an email to it.

## Sender whitelist

By default, the trigger accepts email from **any sender**. To restrict to specific senders, add a comma-separated list of allowed email addresses under **Trigger email address** on the workflow detail page.

```
orders@myapp.com, alerts@monitoring.io
```

Emails from addresses not on the list are silently rejected with a 403 and any uploaded attachments are cleaned up immediately. Leave the field empty to accept all senders.

## The `data` object

`data` is the parsed email, passed directly to your `start` step:

```js
{
  from:        "sender@example.com",
  to:          "mystore.abc123@jsworkflow.com",
  subject:     "New order confirmation",
  text:        "Plain-text body of the email",
  html:        "<p>HTML body of the email</p>",
  attachments: [
    {
      filename:    "invoice.pdf",
      contentType: "application/pdf",
      key:         "mystore/abc123/email-<uuid>/invoice.pdf"
    }
  ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `from` | `string` | Sender's email address |
| `to` | `string` | The trigger address the email was sent to |
| `subject` | `string` | Email subject line |
| `text` | `string` | Plain-text body (empty string if none) |
| `html` | `string` | HTML body (empty string if none) |
| `attachments` | `array` | List of attachment descriptors (see below) |

### Attachment descriptor

Each item in `data.attachments` describes one attached file:

| Field | Type | Description |
| --- | --- | --- |
| `filename` | `string` | Original filename of the attachment |
| `contentType` | `string` | MIME type, e.g. `application/pdf`, `image/png` |
| `key` | `string` | Storage key; pass to `api.getAttachment()` to retrieve the file |

Attachments are stored for **7 days** from the time the email was received. After that, the key becomes invalid and `api.getAttachment()` returns `null`.

## Accessing attachments

Use `api.getAttachment(key)` to retrieve the raw file content as an `ArrayBuffer`. This method is documented in full on the [Attachments](/workflow-api/attachments/) page.

### Put attachment work in a retryable step

The `start` step is **not retryable**. If your code throws after reading an attachment, `start` cannot be re-run. The safe pattern is to pass the `key` (not the file content) into a subsequent step, which IS retryable:

```js
export class Workflow {
  async start(data, headers, api) {
    for (const att of data.attachments) {
      await api.scheduleNextStep({
        action:  'processAttachment',
        payload: { key: att.key, filename: att.filename, contentType: att.contentType }
      });
    }
  }

  async processAttachment(data, headers, api) {
    // This step is retryable, safe to fetch the file here
    const buffer = await api.getAttachment(data.key);
    if (!buffer) {
      console.log('Attachment expired or not found:', data.key);
      return;
    }

    // Convert to base64 for an API that expects it
    const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
    console.log(`Processed ${data.filename} (${buffer.byteLength} bytes)`);

    // Clean up once you're done
    await api.deleteAttachment(data.key);
  }
}
```

## Example: process an emailed invoice

```js
export class Workflow {
  async start(data, headers, api) {
    console.log('Email from:', data.from);
    console.log('Subject:', data.subject);
    console.log('Attachments:', data.attachments.length);

    if (data.attachments.length === 0) {
      console.log('No attachments, nothing to do.');
      return;
    }

    // Schedule one step per attachment
    for (const att of data.attachments) {
      if (!att.contentType.startsWith('application/pdf')) continue;

      await api.scheduleNextStep({
        action:  'uploadInvoice',
        payload: { key: att.key, filename: att.filename, sender: data.from }
      });
    }
  }

  async uploadInvoice(data, headers, api) {
    const buffer = await api.getAttachment(data.key);
    if (!buffer) {
      console.log('Attachment no longer available:', data.key);
      return;
    }

    // Upload the PDF to an external service
    const formData = new FormData();
    formData.append('file', new Blob([buffer], { type: 'application/pdf' }), data.filename);
    formData.append('sender', data.sender);

    const res = await fetch('https://your-api.example.com/invoices', {
      method: 'POST',
      body: formData,
    });

    console.log('Upload result:', res.status);

    // Remove from storage now that we're done
    await api.deleteAttachment(data.key);
  }
}
```

## Testing email-trigger workflows

After your workflow has been triggered at least once, open the workflow code editor and select **More actions > Workflow runs**. Click the **Email** link in the trigger column for any run to view the full payload in a modal. From there, click **Use as test data** to copy the payload and headers into the test input, useful for re-running with the same email content.

> Note: Attachment files referenced by `key` values in a saved payload remain accessible via `api.getAttachment()` for up to 7 days from when the original email was received.