Elizon Docs
Workflows

Email (IMAP) trigger

Start one run for every new email in a mailbox folder, with the message parsed and its attachments stored.

Add an Email Trigger node (trigger_email) and Elizon watches a mailbox for you: every new email that matches your filters starts one run, with the message already parsed and its attachments already stored as files.

This is the trigger for work that arrives by email — supplier invoices, signed contracts, support requests, monitoring alerts — where the mail itself is the input.

Email content is untrusted. Read this before you connect an Agent to it.

Anyone who learns the address can put text in front of this workflow, and they can write that text to be read by a language model. Treat {{ trigger.subject }}, {{ trigger.textBody }} and every attachment as something a stranger wrote with your Agent's instructions in mind.

In practice: do not give an Agent that reads email a tool that sends, pays, deletes, publishes or grants access without a Human Review node between the Agent's decision and the action. An Agent with a read-only tool that gets talked into the wrong answer produces a wrong answer; the same Agent with a payment tool produces a payment.

Configuration

FieldTypeDefaultDescription
hoststring''The IMAP server.
portinteger 1–65535993993 is IMAPS; STARTTLS and plain connections normally use 143.
securitytls | starttls | nonetlsSee Connecting a mailbox.
usernamestring''The account's login.
password{{secret.KEY}} reference—Only a secret reference. A typed password is refused by the save.
mailboxstringINBOXThe folder to watch, exactly as the server names it.
filterUnseenbooleantrueOnly unread mail.
filterFromstring''Sender address contains.
filterTostring''Recipient address contains.
filterSubjectContainsstring''Subject contains.
filterSincedate (YYYY-MM-DD)''Arrived on or after this day.
downloadAttachmentsbooleanfalseBring attachments into the run as files.
attachmentExtensionsstring''Comma-separated extensions to keep (pdf,xlsx). Empty keeps every type.
attachmentMaxBytesinteger, up to 25 MB10 MBLarger attachments are skipped; the email still runs.
formatsimple | rawsimpleraw also adds {{ trigger.raw }}, the whole message source.
postActionmarkRead | move | nonemarkReadWhat to do to the message after its run starts — see After processing.
moveToMailboxstring''The folder for move. It must already exist.
pollMinutesinteger 1–605How often to check.
processExistingUnreadbooleanfalseOn the first check, also start runs for mail already in the folder.
processExistingMaxinteger 1–20010How many of the most recent existing emails to take when processExistingUnread is on.

Connecting a mailbox

FieldNotes
IMAP server / port993 is IMAPS. STARTTLS and plain connections normally use 143.
EncryptionTLS (recommended), STARTTLS, or None. STARTTLS requires the upgrade and fails if the server cannot offer it; None never upgrades, even if the server offers it.
UsernameThe account's login — not a credential, so an expression may build it.
PasswordOnly a reference to a secret: {{secret.IMAP_PASSWORD}}. Add the value under Settings → Secrets.
FolderINBOX by default. Use the exact name your server uses — Inbox and INBOX are different on many servers.

A password typed into the node is refused by the save itself. A credential written into a graph is stored with every saved version of that workflow, appears in every version diff, cannot be rotated, and would travel in an export — so it is never accepted in the first place.

Test connection signs in, lists the folders the account can see, and shows the unread count of the one you chose. It reads the saved node, so save the workflow before pressing it; nothing you have typed and not saved is sent anywhere.

OAuth2 for Microsoft 365 and Google Workspace is not available yet — these use a password or an app password today.

Which emails start a run

Every filter is applied by the mail server, not by Elizon after downloading, so a folder with ten years of mail in it is never fetched to find today's three invoices.

FilterNotes
Only unreadOn by default. Combines with "mark as read" into a folder that drains.
From / To containsSubstring match on the address header.
Subject containsSubstring match.
Arrived on or afterA whole day — the mail protocol has no finer filter than a date.

Output

The payload is flat, unlike the webhook and form paths — there is no HTTP request here to have a body:

ExpressionValue
{{ trigger.subject }}The subject line, decoded.
{{ trigger.from.address }}The sender's address (.name for the display name).
{{ trigger.to }} / .cc / .replyToLists of { name, address }.
{{ trigger.textBody }}The plain-text body, falling back to the HTML body converted to text.
{{ trigger.htmlBody }}The HTML body, if the message has one.
{{ trigger.date }}When it arrived, ISO-8601. Falls back to the server's own timestamp when the message has no usable Date header.
{{ trigger.headers }}Every header, lower-cased ({{ trigger.headers.x-spam-score }}). Repeated headers are joined.
{{ trigger.attachments }}Stored files, in the same shape a form upload or a Convert to File node produces — so an Agent can read one and a Send Email node can forward it.
{{ trigger.messageId }}The Message-ID header, or null.
{{ trigger.inReplyTo }}The <id> this email replies to (its In-Reply-To header), or null.
{{ trigger.references }}The thread's <id>s from the References header, in order, without duplicates. An empty list when there is none.

Attachments are only fetched when Bring attachments into the run is on. You can narrow them by extension and cap their size; an attachment that is too large or the wrong type is skipped and the email still runs. Inline images that the HTML body refers to (a signature logo) are not treated as attachments.

Set Format to Raw to also get {{ trigger.raw }}, the whole message source. Every parsed field above is still there.

Email threading

{{ trigger.inReplyTo }} and {{ trigger.references }} tell you which earlier email a reply answers. They are read from the parsed headers, so a References header folded over several lines, or with commas between its ids, still comes out as a clean list of <id>s that you can compare. {{ trigger.headers.references }} keeps the raw header text, as before.

To match a customer's reply to the reminder you sent, store the reminder's messageId when Send Email sends it, then look up {{ trigger.inReplyTo }} — or any of {{ trigger.references }} — against what you stored. The Send Email page walks through the whole pattern.

After processing

OptionEffect
Mark it as readThe default. With "Only unread" on, the folder drains as it is processed.
Move it to another folderThe folder must already exist — the trigger does not create one.
Leave it aloneNothing is changed on the server.

This is a convenience for whoever reads the mailbox, not part of the exactly-once guarantee below. A message whose run started but whose flag could not be set — a read-only shared folder, say — is not processed twice.

How often, and what happens on the first run

Check every is 1 to 60 minutes, 5 by default. At most 50 emails start runs per check; anything more waits for the next one, so a burst is drained rather than dropped.

Publishing an Email Trigger does not start a run per existing email

By default only mail that arrives after you publish starts a run. A folder with four thousand unread messages in it does not produce four thousand runs — the first check records where the folder has got to and fires nothing.

Turn on On first run, also process the emails already in the folder if you want the opposite, and set how many of the most recent ones to take.

Exactly once, even across a crash

Each email starts exactly one run, forever. Elizon records its position in the folder (the IMAP UIDVALIDITY and the highest message it has handled) and gives each run an identity derived from the message itself, which the workflow engine refuses to start twice. So:

  • a crash between starting a run and recording the position does not produce a second run;
  • a crash before the run starts leaves the message to be picked up by the next check, rather than skipped;
  • if the folder is recreated or renumbered by the server, Elizon re-reads where it has got to and continues, without re-firing mail it has already processed.

When it cannot sign in

A failed check does not retry in a tight loop. The error appears on the trigger in Studio — the message the server gave, when it happened, and how many checks in a row have failed — and the interval between attempts widens (double, then quadruple, up to eight times your configured interval) until it works again. After five consecutive failures the workflow's owner is notified. Nothing is consumed by a failed check: the next successful one picks up exactly where the last successful one left off.

The schedule itself is created, updated, or removed when you publish or unpublish — see Publishing.

Example

Forward each supplier invoice to an agent that extracts its fields:

workflows/supplier-invoices.yaml
name: Supplier invoices
description: Reads invoices that arrive in the invoices mailbox.
graph:
  nodes:
    - id: invoice-mail
      type: trigger_email
      position: { x: 0, y: 0 }
      data:
        filterUnseen: true
        filterSubjectContains: Invoice
        downloadAttachments: true
        attachmentExtensions: pdf
        postAction: markRead
        pollMinutes: 5
    - id: extract
      type: ai_agent
      position: { x: 280, y: 0 }
      data:
        mode: existing
        agent: agents.invoice-reader
        agentName: Invoice reader
        inputMappings: []
  edges:
    - { id: mail-to-extract, source: invoice-mail, target: extract }

The mailbox itself — host, port, security, username, password, mailbox — is not in a portable definition; whoever installs or builds the workflow sets it on the node.

In a Package

A Package can declare type: email in its triggers, and it declares nothing else — no mailbox, no server, no credential. Those are set by whoever installs it, in their own organization, on the installed workflow's Email Trigger node. A carried INBOX would point the trigger at whichever account the installer supplied, and a carried secret reference would resolve to a different value in their vault; both are worse than a node that plainly needs configuring.

elizon.yaml
triggers:
  - id: invoices-inbox
    type: email

On the node:

  • Travels: postAction, filterUnseen, filterFrom, filterTo, filterSubjectContains, filterSince, downloadAttachments, attachmentExtensions, attachmentMaxBytes, format, pollMinutes, processExistingUnread, processExistingMax
  • Converted: none
  • Never carried: host, port, security, username, password, mailbox, moveToMailbox

A password typed into the node fails the build (SLOT_CREDENTIAL_VALUE_REJECTED); only a {{secret.KEY}} reference is accepted, and even that does not travel.