Function
sendEmail
Back to IndexQueue an email for delivery through Hantera’s centralized sending system. The email is processed asynchronously with automatic retry logic and status tracking.
Signature
import 'resources'
sendEmail { to: text subject: text body: { plainText: text | nothing html: text | nothing } cc: [text] | nothing bcc: [text] | nothing from: text | nothing fromName: text | nothing category: text | nothing replyTo: text | nothing dynamic: { text -> any } | nothing} => uuidParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
to | text | Yes | Primary recipient email address |
subject | text | Yes | Email subject line |
body | Record | Yes | Email body (see Body Parameter below) |
cc | [text] | No | List of CC recipient email addresses |
bcc | [text] | No | List of BCC recipient email addresses |
from | text | No | Sender email address (overrides system default) |
fromName | text | No | Display name for sender |
category | text | No | Category for filtering/reporting (default: "reactor") |
replyTo | text | No | Reply-to email address |
dynamic | { text -> any } | No | Custom data to store for querying (default: {}) |
Body Parameter
The body parameter must be a record with at least one of these fields:
plainText: Plain text version of the emailhtml: HTML version of the email
You must provide at least one, but you can provide both for multi-part emails.
Returns
Returns a uuid representing the sendingId for the primary recipient (to address). Each CC and BCC recipient gets their own Sending record with unique IDs.
Examples
Simple Text Email
import 'resources'
from sendEmail { subject = 'Welcome to Hantera' body = { plainText = 'Thank you for signing up!' } dynamic = {}}HTML Email with Interpolation
import 'resources'
from sendEmail { to = order.customer.email subject = $'Order #{order.orderNumber} Confirmed' body = { plainText = $'Thank you for your order #{order.orderNumber}.' html = $' <h1>Order Confirmed</h1> <p>Thank you for your order <strong>#{order.orderNumber}</strong>.</p> <p>Total: {order.total} {order.currencyCode}</p> ' } category = 'order_confirmation' dynamic = { orderId = order.id orderNumber = order.orderNumber }}Email with CC and BCC
import 'resources'
from sendEmail { subject = 'Important Update' body = { html = '<p>This is an important update about your account.</p>' } category = 'account_update' dynamic = { customerId = customer.id }}Custom From Address
import 'resources'
from sendEmail { to = user.email subject = 'Password Reset Request' body = { plainText = $'Click this link to reset your password: {resetLink}' html = $'<p>Click <a href="{resetLink}">here</a> to reset your password.</p>' } fromName = 'MyCompany Support' category = 'password_reset' dynamic = { userId = user.id resetToken = resetToken }}Common Patterns
Order Confirmation
import 'resources'
// In an order.confirmed reactorfrom sendEmail { to = order.customer.email subject = $'Order #{order.orderNumber} Confirmed' body = { html = $' <h1>Thank You for Your Order!</h1> <p>Order Number: <strong>{order.orderNumber}</strong></p> <p>Total: {order.total} {order.currencyCode}</p> <p>We''ll send you updates as we process your order.</p> ' } category = 'order_confirmation' dynamic = { orderId = order.id orderNumber = order.orderNumber customerId = order.customer.id }}Password Reset
import 'resources'
// In a password reset reactorlet resetLink = $'https://myapp.com/reset-password?token={resetToken}'
from sendEmail { to = user.email subject = 'Password Reset Request' body = { plainText = $'Click this link to reset your password: {resetLink}' html = $' <p>You requested a password reset.</p> <p><a href="{resetLink}">Click here to reset your password</a></p> <p>This link expires in 1 hour.</p> ' } category = 'password_reset' dynamic = { userId = user.id }}Shipping Notification
import 'resources'
// In an order.shipped reactorfrom sendEmail { to = order.customer.email subject = $'Your Order #{order.orderNumber} Has Shipped' body = { html = $' <h1>Your Order Has Shipped!</h1> <p>Order Number: {order.orderNumber}</p> <p>Tracking Number: <a href="{trackingUrl}">{trackingNumber}</a></p> <p>Estimated Delivery: {estimatedDelivery}</p> ' } category = 'order_shipped' dynamic = { orderId = order.id orderNumber = order.orderNumber trackingNumber = trackingNumber }}Error Handling
The function returns errors for invalid input:
import 'resources'
from sendEmail { subject = 'Test' body = { plainText = nothing, html = nothing } // ERROR: At least one body required dynamic = {}} match Error |> $'Failed to send email: {result.error.message}' uuid |> $'Email queued with ID: {result}'Common Errors
INVALID_BODY: NeitherplainTextnorhtmlprovided in bodyRESERVED_CATEGORY_PREFIX: Category starts withsystem:prefix (reserved for internal platform use)INVALID_EMAIL_ADDRESS: From address is missing@or domainINTERNAL_ERROR: Unexpected error occurred
Dynamic Fields for Querying
The dynamic parameter stores custom data in the Sending record’s dynamic field (JSON). You can then define custom Graph fields to query this data:
# Define a custom fielduri: /resources/registry/graph/sending/fields/orderIdspec: value: type: text source: dynamic->'orderId'// Query sendings by orderId[ { "edge": "sendings", "filter": "orderId == '550e8400-e29b-41d4-a716-446655440000'", "node": { "fields": ["sendingId", "recipient", "status"] } }]Multi-Recipient Behavior
When you provide cc or bcc lists, the system creates one Sending record for the email:
- One record for the primary
torecipient (returned SendingId) - CC and BCC recipients are delivered best-effort
- Only the primary
torecipient has tracked delivery status
The cc and bcc recipients are stored as comma-separated strings in the Sending record’s data, but do not have individual status tracking or retry logic.
import 'resources'
// This creates 1 Sending record for the primary recipientlet sendingId = sendEmail { subject = 'Test' body = { plainText = 'Test message' } dynamic = {}}
// sendingId tracks only the primary 'to' recipient// CC/BCC delivery success/failure is not tracked individuallyAsynchronous Processing
Processing behavior:
- Emails are queued as
pendingstatus - Background service processes queue every 10 seconds (configurable)
- Rate limited to 60 emails/minute by default (configurable)
- Failed deliveries are retried up to 3 times with exponential backoff
- Final status is either
sentorbounced
Monitor delivery:
// Query sending status[ { "edge": "sendings", "filter": "sendingId == '550e8400-e29b-41d4-a716-446655440000'", "node": { "fields": ["sendingId", "status", "sentAt", "errorMessage"] } }]Best Practices
1. Always Include Plain Text
Provide both plainText and html for better compatibility:
body = { plainText = 'Your order has shipped.' html = '<p>Your order has <strong>shipped</strong>.</p>'}2. Use Meaningful Categories
Categories help with reporting and filtering:
category = 'order_confirmation' // Goodcategory = 'email' // Not helpful3. Store Query able Data in Dynamic
Store IDs and reference numbers you’ll need to query later:
dynamic = { orderId = order.id orderNumber = order.orderNumber customerId = order.customer.id // Avoid storing large blobs or sensitive data}4. Set Reply-To for Support Emails
Make it easy for customers to respond:
replyTo = '[email protected]'5. Use Text Interpolation
Leverage Filtrera’s text interpolation for dynamic content:
subject = $'Order #{order.orderNumber} Update'body = { html = $'<p>Hi {customer.firstName},</p><p>Your order status: {order.status}</p>'}Related Resources
- Sending Graph Node - Query email delivery status
- Sending Emails Guide - Complete guide with examples
- Custom Fields - Define queryable fields on dynamic data
- Reactors - Using sendEmail in reactor scripts