Email Module
Send transactional and marketing emails via SMTP or Resend API with variable interpolation, HTML templates, and per-organization rate limiting.
What It Does
The Email Module enables workflows to send emails as action nodes. It supports two providers:
- SMTP: Connect to any SMTP server (Gmail, Mailgun, SES, your own Postfix, etc.)
- Resend API: Modern email API with high deliverability and built-in analytics
Use cases include:
- Welcome emails when a user registers
- Order confirmation and shipping updates
- Password reset notifications
- Marketing drip campaigns triggered by events
- Alert emails when workflow executions fail
How to Activate
- Navigate to Settings → Modules in the sidebar
- Find the Email module in the available modules list
- Click Install to register the module in your organization
- Click Activate to enable it
- Click Configure to enter your SMTP or Resend credentials
Permission Required
You need the modules.configure permission to install and configure modules. By default, only owner and admin roles have this permission.
Configuration
SMTP Provider
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Set to "smtp" |
smtp_host | string | Yes | SMTP server hostname (e.g., smtp.gmail.com) |
smtp_port | integer | Yes | Port number: 587 (TLS), 465 (SSL), or 25 (unencrypted) |
smtp_username | string | Yes | Authentication username |
smtp_password | string | Yes | Authentication password (stored encrypted) |
smtp_encryption | string | No | tls (default), ssl, or null |
from_address | string | Yes | Default sender email address |
from_name | string | No | Default sender display name |
Resend Provider
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Set to "resend" |
resend_api_key | string | Yes | Resend API key (starts with re_) |
from_address | string | Yes | Verified sender address in Resend |
from_name | string | No | Default sender display name |
{
"provider": "smtp",
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "your-app@gmail.com",
"smtp_password": "app-specific-password",
"smtp_encryption": "tls",
"from_address": "noreply@yourcompany.com",
"from_name": "Your Company"
}
Node Type: action.email.send
This node sends an email when reached during workflow execution.
| Config Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient email address (supports variables) |
subject | string | Yes | Email subject line (supports variables) |
body | string | Yes | Email body — HTML or plain text (supports variables) |
from | string | No | Override default sender address for this email |
{
"type": "action.email.send",
"config": {
"to": "{{trigger.email}}",
"subject": "Welcome to our platform, {{trigger.name}}!",
"body": "<h1>Hello {{trigger.name}}</h1><p>Thanks for signing up. Your account is ready.</p>",
"from": "welcome@yourcompany.com"
}
}
Variable Interpolation
All text fields in the email node support double-brace variable syntax. Variables are resolved at execution time from the trigger payload and outputs of previous nodes.
| Variable | Source | Example Value |
|---|---|---|
{{trigger.email}} | Event trigger payload | user@example.com |
{{trigger.name}} | Event trigger payload | John Doe |
{{trigger.data.order_id}} | Nested trigger payload | ORD-12345 |
{{step_id.output.field}} | Output from a previous node | (varies) |
Unresolved Variables
If a variable cannot be resolved (e.g., the field does not exist in the trigger payload), it will be replaced with an empty string. The email will still be sent — check your trigger payload structure to avoid missing data.
Rate Limiting
To prevent abuse and protect your SMTP reputation, the Email Module enforces per-organization, per-hour rate limits.
| Limit Type | Default | Configurable |
|---|---|---|
| Emails per hour per organization | 100 | Yes (via module config) |
| Emails per execution | 10 | Yes |
When the rate limit is exceeded, the node execution fails with error code rate_limit_exceeded and the workflow execution step is marked as failed. The email is not queued for retry — it must be re-triggered.
CRLF Injection Protection
The Email Module sanitizes all header fields (to, from, subject) by stripping \r and \n characters before sending. This prevents CRLF injection attacks that could add arbitrary headers (like BCC) to outgoing emails.
// Strips CRLF characters from email headers
$to = str_replace(["\r", "\n"], '', $config['to']);
$subject = str_replace(["\r", "\n"], '', $config['subject']);
$from = str_replace(["\r", "\n"], '', $config['from'] ?? $defaultFrom);
Logging
Every email sent (or attempted) is logged in the email_send_logs table for auditing and debugging:
| Column | Description |
|---|---|
id | Primary key |
organization_id | Owning organization |
execution_id | Workflow execution that triggered this email |
to | Recipient address |
subject | Email subject |
status | sent, failed, or rate_limited |
provider | smtp or resend |
error_message | Error details if failed |
sent_at | Timestamp |
Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Connection timed out | SMTP server unreachable or port blocked | Verify firewall allows outbound on port 587/465. Try telnet smtp.host 587 from your server. |
| Authentication failed | Wrong SMTP credentials | For Gmail, use an App Password (not your account password). Verify username is the full email. |
| Rate limit exceeded | Too many emails sent in the current hour | Wait for the rate limit window to reset, or increase the limit in module configuration. |
| Emails going to spam | Missing SPF/DKIM/DMARC records | Configure DNS records for your sending domain. Use a dedicated IP or service like Resend. |
| Connection timeout (Resend) | Network issue or API outage | Check Resend status page. Verify server has outbound HTTPS access. |