Loopfour
Triggers

Email Triggers

Trigger workflows when an email arrives at a dedicated workflow inbox address

Email Triggers

The email_trigger block gives each workflow its own dedicated inbound email address. When an email arrives at that address, the workflow runs automatically — with the full email (sender, subject, body, and any PDF attachments) available to every downstream step.

How it works

  1. Add an Email Trigger block to your workflow on the Studio canvas.
  2. Save or deploy the workflow. An address of the form wf_<token>@inbound.loopfour.ai is generated and stored on the workflow. The token is 80 bits of cryptographically random data, making the address unguessable.
  3. Share the address with the senders you want to trigger the workflow (or configure an email client/service to forward to it).
  4. When an email arrives, Resend delivers a signed webhook to the platform. The signature is verified before any run is created (fail-closed — a bad or missing signature drops the request, no run is created).
  5. Optional filters determine whether the email matches the workflow's criteria.
  6. A workflow_run is created with the normalized email payload, and execute-workflow is enqueued.

Authoring in Studio

In the Studio canvas, add the Email Trigger block from the Triggers section of the block palette.

  • Before deploy: the address field shows a placeholder — "Address generated on deploy." No address is provisioned at canvas-drop time.
  • After the first save/deploy: the full address (wf_<token>@inbound.loopfour.ai) appears with a copy button. Re-deploying does not change the address — it is stable for the lifetime of the workflow.

Only one Email Trigger block is allowed per workflow (singleInstance: true).

Configuration

{
  "trigger": {
    "type": "email_event",
    "address": "[email protected]",
    "filters": {
      "fromAllowlist": ["[email protected]", "[email protected]"],
      "subjectContains": "invoice",
      "requireAttachment": true
    }
  }
}
FieldTypeRequiredDescription
typestringYesMust be "email_event"
addressstringNoSet by the server on deploy. Do not author this field manually.
filters.fromAllowliststring[]NoExact sender addresses (case-insensitive). Email is dropped if the sender is not in the list.
filters.subjectContainsstringNoCase-insensitive substring match on the email subject.
filters.requireAttachmentbooleanNoWhen true, the email is dropped if no PDF attachment is stored successfully.

All filter fields are optional and independent. When multiple filters are set, all must match (AND logic). An email with no filters set always triggers the workflow.

Filters

Sender allowlist (fromAllowlist)

Only emails from addresses in the list will trigger the workflow. The check is case-insensitive and matches the bare sender address.

{
  "filters": {
    "fromAllowlist": ["[email protected]"]
  }
}

When fromAllowlist is set, emails from senders whose SPF or DKIM record is a hard fail are dropped before filters are applied. This prevents spoofed senders from triggering workflows even if the address matches.

Subject filter (subjectContains)

Only emails whose subject contains the specified string (case-insensitive) will trigger the workflow.

{
  "filters": {
    "subjectContains": "purchase order"
  }
}

Require attachment (requireAttachment)

When true, the workflow only triggers if at least one PDF attachment was received and stored successfully.

{
  "filters": {
    "requireAttachment": true
  }
}

Attachment support

Inbound email attachments are supported with the following constraints in the current release:

  • PDF only — only application/pdf attachments are stored. Other file types (images, Word documents, spreadsheets) are skipped.
  • 25 MB limit per attachment — attachments larger than 25 MB are skipped.
  • Stored attachments are uploaded to the workspace file store and referenced by a fileId UUID in the trigger payload.

Skipped attachments (wrong type or oversized) are not fatal. The email still triggers the workflow unless requireAttachment: true is set and all attachments were skipped.

Payload

The email payload is available in your workflow steps as {{trigger.inboundEmail}}:

{
  "trigger": {
    "inboundEmail": {
      "source": "inbound-email",
      "emailId": "msg_abc123",
      "to": "[email protected]",
      "from": "[email protected]",
      "fromName": "Vendor Billing",
      "subject": "Invoice #1234 — July 2026",
      "text": "Please find the attached invoice...",
      "html": "<p>Please find the attached invoice...</p>",
      "receivedAt": "2026-07-01T09:00:00.000Z",
      "auth": {
        "spf": "pass",
        "dkim": "pass"
      },
      "attachments": [
        {
          "fileId": "550e8400-e29b-41d4-a716-446655440000",
          "name": "invoice-1234.pdf",
          "contentType": "application/pdf",
          "size": 204800
        }
      ]
    }
  }
}

Payload fields

FieldTypeDescription
source"inbound-email"Discriminator — always "inbound-email" for email triggers.
emailIdstringResend's unique email ID. Used internally for idempotency.
tostringThe workflow's inbound address that received the email.
fromstringSender email address.
fromNamestring?Sender display name, if present.
subjectstringEmail subject (empty string if absent).
textstring?Plain-text body, if provided by the sender.
htmlstring?HTML body, if provided by the sender.
receivedAtstring (ISO 8601)Timestamp when Resend received the email.
auth.spfstringSPF verdict: pass, fail, softfail, neutral, none, or unknown.
auth.dkimstringDKIM verdict: pass, fail, neutral, none, or unknown.
attachmentsarrayStored PDF attachments. Empty if no PDFs were attached or all were skipped.
attachments[].fileIdstring (UUID)Reference to the stored file in the workspace file store.
attachments[].namestringOriginal filename.
attachments[].contentTypestringMIME type (always application/pdf in the current release).
attachments[].sizenumberFile size in bytes.

Using payload fields in steps

{
  "config": {
    "to": "{{trigger.inboundEmail.from}}",
    "subject": "Re: {{trigger.inboundEmail.subject}}",
    "body": "We received your email. Attachment count: {{trigger.inboundEmail.attachments.length}}"
  }
}

Access a specific attachment's file ID:

{
  "config": {
    "fileId": "{{trigger.inboundEmail.attachments.0.fileId}}"
  }
}

Rate limiting

Inbound email delivery is rate-limited per address and per company to protect against abuse:

  • Per address: 10 emails per minute (default). Configurable via INBOUND_EMAIL_RATE_PER_ADDR_MIN.
  • Per company: 60 emails per minute (default). Configurable via INBOUND_EMAIL_RATE_PER_COMPANY_MIN.

Emails that exceed the limit are dropped silently with a 200 response to Resend — they do not trigger runs and are not retried by Resend. If you expect high-volume inbound email, contact support to adjust the limits.

Idempotency

Every email triggers at most one workflow run, even if Resend delivers the webhook more than once (for example, due to retries). Idempotency is enforced at two independent layers:

  1. The process-inbound-email task is enqueued with idempotencyKey = email-{emailId}, so concurrent deliveries of the same email enqueue the task at most once.
  2. The workflow_run insert uses a dedupeKey = email:{emailId} backed by a partial unique index, guaranteeing exactly one run row even across concurrent task executions.

Workflow example

{
  "name": "Process Inbound Invoice",
  "description": "Extract and file invoices received by email",
  "trigger": {
    "type": "email_event",
    "filters": {
      "fromAllowlist": ["[email protected]"],
      "subjectContains": "invoice",
      "requireAttachment": true
    }
  },
  "steps": [
    {
      "id": "log-receipt",
      "type": "action",
      "name": "Log Email Receipt",
      "action": "slack.sendMessage",
      "config": {
        "channel": "#finance-ops",
        "text": "New invoice email from {{trigger.inboundEmail.fromName}} ({{trigger.inboundEmail.from}}): {{trigger.inboundEmail.subject}}"
      }
    },
    {
      "id": "file-attachment",
      "type": "action",
      "name": "File Invoice PDF",
      "action": "files.get",
      "config": {
        "fileId": "{{trigger.inboundEmail.attachments.0.fileId}}"
      }
    }
  ]
}

Troubleshooting

Address not showing in Studio

The address is provisioned at deploy/save time, not when the block is dropped on the canvas. Save or deploy the workflow to generate the address.

Emails not triggering the workflow

  1. Check the address — confirm the sender is emailing the exact wf_<token>@inbound.loopfour.ai address shown in Studio.
  2. Check filters — if fromAllowlist is set, the sender address must match exactly (case-insensitive). If subjectContains is set, the subject must include the substring.
  3. Check SPF/DKIM — if fromAllowlist is set, emails with hard SPF or DKIM failures are dropped before filters run. Inspect the run logs; the auth field in the payload shows the verdicts.
  4. Check the workflow status — the workflow must be in active status.
  5. Check attachments — if requireAttachment: true, at least one PDF attachment ≤ 25 MB must be present.
  6. Check rate limits — if volume is high, the per-address or per-company limit may be dropping emails.

Duplicate emails / no duplicate runs

Email delivery is idempotent by design. If Resend retries a webhook, the same email will not create a second run.

Next Steps

On this page