Send Email
Send an email through your own SMTP server, with files attached.
A Send Email node (send_email) sends one message through an SMTP server you set in the node itself — the one your company already sends mail with (Microsoft 365, Google Workspace, your hosting provider). You can change the server, the sender or the recipients at any time by opening the node.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
smtpHost | string | '' | The SMTP server. |
smtpPort | integer 1–65535 | 587 | Usually 587 for STARTTLS and 465 for TLS. |
smtpSecurity | starttls | tls | none | starttls | none is for a relay on a trusted network only. |
smtpUsername | string | '' | Empty for a server that needs no login. |
smtpPassword | {{secret.KEY}} reference | '' | Only a secret reference. |
fromAddress | string | '' | The sender address. |
fromName | string | '' | The sender's display name. |
to | string | '' | Comma-separated addresses, or an expression resolving to a list. |
cc | string | '' | As to. |
bcc | string | '' | As to. |
subject | string | '' | Expressions allowed. |
body | string | '' | Expressions allowed. |
bodyFormat | text | markdown | html | text | Markdown is sent as formatted HTML, with the Markdown as the plain-text part. |
attachments | string[] | [] | One expression per file, such as {{ upstream.<nodeId>.output.file }}. |
messageId | string | '' | The Message-ID to send with. Empty: one is generated. See Email threading. |
inReplyTo | string | '' | The Message-ID of the email this one answers. |
references | string | '' | The thread's ids, separated by spaces or commas, or an expression resolving to a list. |
replyTo | string | '' | Where replies go. As to; counts towards the 50-recipient limit. |
SMTP server
- Host, Port and Security — STARTTLS (usually port 587), TLS / SSL (usually 465), or None for a relay on a trusted network only.
- Username and Password — the password is never typed into the node. Add it under Settings → Secrets and pick it, so the field holds a reference such as
{{secret.SMTP_PASSWORD}}. The secret is read only at the moment the email is sent and never appears in the run's history. Leave both empty for a server that needs no login.
Message
- From address and From name.
- To, Cc, Bcc — separate several addresses with commas. An expression that resolves to a list of addresses works too. At most 50 recipients per message.
- Subject and Body, both with expressions. The body can be Plain text, HTML, or Markdown — Markdown is sent as formatted HTML, with the Markdown itself as the plain-text version.
- Attachments — one expression per row, each pointing at a file: a Convert to File output such as
{{ upstream.<nodeId>.output.file }}, or a Form Trigger upload. Up to 20 MB in total.
Advanced: threading (collapsed until opened)
- Message-ID, In-Reply-To, References and Reply-To — see Email threading. All optional; leave them empty and the email is sent exactly as before.
Output
| Field | Type | Description |
|---|---|---|
messageId | string | The Message-ID the email was sent with — the one you set, or the generated one. Store it to match replies. |
accepted | string[] | The addresses the server took. |
rejected | string[] | The addresses it refused. |
When a data policy's redact rule masked the message, the output also carries sent: the subject and body actually sent, with placeholders such as [IBAN_1] in place of the masked values.
Limits and errors
If the server refuses the login, cannot be reached, or rejects the recipients, the node fails with the reason.
A Send Email step never sends the same message twice. If the step is retried — for example after a restart — it recognises that it has already sent and does not send again. Inside a Loop it sends once per item.
For security, the SMTP host must be a public server: addresses on a private network, on the platform's own machine, and cloud metadata addresses are refused. A self-hosted installation can allow its own private mail relay with WORKFLOW_SMTP_ALLOW_PRIVATE_HOSTS=true.
Email threading
Mail clients group a conversation by three headers: every email has a Message-ID, and a reply carries the id it answers in In-Reply-To and the whole chain in References. Set them here and your follow-ups land in the customer's existing thread; read them on the Email Trigger and you can tell which of your emails a reply answers.
Every value is checked before anything is sent, and the node fails naming the field if one is wrong:
- A value containing a line break is refused — it would add a header you did not write.
- A message id must look like
<local@domain>. Angle brackets are added if they are missing (invoice-42@acme.debecomes<invoice-42@acme.de>). - References takes at most 50 ids (and 4 KB). Duplicates are dropped.
- Reply-To must be valid addresses and counts towards the 50-recipient limit.
When Message-ID is set, the email carries exactly that id and output.messageId returns it. If the step is retried, it returns the id it already sent with, so the id you stored stays right.
The Cash Collector pattern
- Send the reminder and store its id. Set Message-ID to something unique, such as
<invoice-{{ loop.item.Number }}.r1@acme.de>, or leave it empty and use the generated one. Then write{{ upstream.<send node id>.output.messageId }}into the invoice's row in a Data Table. - When a reply arrives, an Email Trigger run looks up
{{ trigger.inReplyTo }}in that table's message-id column. If the customer's client did not set it, try each of{{ trigger.references }}— the reminder's id is in the chain. - Reply in the same thread. In the next Send Email, set In-Reply-To to
{{ trigger.messageId }}(the customer's email) and References to{{ trigger.references }} {{ trigger.messageId }}— the chain so far, then the customer's email. An email with noReferencesheader gives an empty list, so the same value works for the first reply too.
Example
name: Send report
description: Emails the weekly report as a PDF attachment.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data: { description: Send this week's report. }
- id: to-pdf
type: convert_to_file
position: { x: 280, y: 0 }
data:
{
sourceExpression: '# Weekly report',
format: pdf,
filename: weekly-report,
shareWith: workflow,
}
- id: send
type: send_email
position: { x: 560, y: 0 }
data:
subject: Weekly report
body: 'The report for the week of {{ run.startedAt }} is attached.'
bodyFormat: text
attachments: ['{{ upstream.to-pdf.output.file }}']
edges:
- { id: start-to-pdf, source: start, target: to-pdf }
- { id: pdf-to-send, source: to-pdf, target: send }In a Package
When a Package that contains a Send Email node is installed, the message (subject, body and attachments) comes with it, but the SMTP server and the sender do not — they belong to the author's company. Open the node after installing and fill them in; until then, a run fails the node with "Send Email isn't configured".
The recipients follow the rule a Human Review approver does — people never travel, expressions do. Each of To, Cc and Bcc is judged on its own:
- A field made only of
{{ }}expressions, such as{{ loop.item.Contact email }}or{{ trigger.from.address }}, {{ upstream.owner.output.email }}, names nobody until a run resolves it, so it travels and is in place after install. - A field with anything else in it — a literal address such as
billing@acme.de, an expression next to one ({{ … }}, cfo@acme.de), or a{{secret.…}}reference — is stripped.buildandvalidatewarn about it (SEND_EMAIL_RECIPIENT_STRIPPED), naming the node and the field; the installer sets it on the canvas, and until then a run fails the node with "Send Email has no recipients". - An empty field has nothing to carry.
When an update to the Package arrives, a Workflow whose recipients you set on the canvas counts as edited: if the new version changes that Workflow, you choose Keep mine (your recipients stay) or Take theirs (the new version's node, with its expression recipients if it carries any, or none if it strips them). A Workflow the new version leaves unchanged keeps your recipients.
Reply-To is a people field too and follows the same rule: it travels only when it is expression-only. Message-ID, In-Reply-To and References always travel — they are ids over run data, not people.
- Travels:
subject,body,bodyFormat,attachments,messageId,inReplyTo,references - Converted: none
- Never carried:
smtpHost,smtpPort,smtpSecurity,smtpUsername,smtpPassword,fromAddress,fromName,to,cc,bcc,replyTo(each of the last four unless expression-only)