Skip to content

Function

sendEmail

Back to Index

Queue 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
} => uuid

Parameters

ParameterTypeRequiredDescription
totextYesPrimary recipient email address
subjecttextYesEmail subject line
bodyRecordYesEmail body (see Body Parameter below)
cc[text]NoList of CC recipient email addresses
bcc[text]NoList of BCC recipient email addresses
fromtextNoSender email address (overrides system default)
fromNametextNoDisplay name for sender
categorytextNoCategory for filtering/reporting (default: "reactor")
replyTotextNoReply-to email address
dynamic{ text -> any }NoCustom 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 email
  • html: 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'
replyTo = '[email protected]'
category = 'password_reset'
dynamic = {
userId = user.id
resetToken = resetToken
}
}

Common Patterns

Order Confirmation

import 'resources'
// In an order.confirmed reactor
from 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 reactor
let 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'
replyTo = '[email protected]'
dynamic = {
userId = user.id
}
}

Shipping Notification

import 'resources'
// In an order.shipped reactor
from 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: Neither plainText nor html provided in body
  • RESERVED_CATEGORY_PREFIX: Category starts with system: prefix (reserved for internal platform use)
  • INVALID_EMAIL_ADDRESS: From address is missing @ or domain
  • INTERNAL_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 field
uri: /resources/registry/graph/sending/fields/orderId
spec:
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 to recipient (returned SendingId)
  • CC and BCC recipients are delivered best-effort
  • Only the primary to recipient 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 recipient
let sendingId = sendEmail {
to = '[email protected]' // Tracked with this sendingId
cc = ['[email protected]'] // Best-effort delivery, not individually tracked
bcc = ['[email protected]', '[email protected]'] // Best-effort delivery, not individually tracked
subject = 'Test'
body = { plainText = 'Test message' }
dynamic = {}
}
// sendingId tracks only the primary 'to' recipient
// CC/BCC delivery success/failure is not tracked individually

Asynchronous Processing

Processing behavior:

  • Emails are queued as pending status
  • 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 sent or bounced

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' // Good
category = 'email' // Not helpful

3. 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>'
}
Back to Index