Sequences
Defining email sequences with send, wait, choice, and condition steps.
A sequence is an ordered series of email sends, delays, and branching logic, orchestrated by AWS Step Functions. Each sequence is defined in a sequence.config.ts file and auto-discovered by CDK at deploy time.
File structure
sequences/<sequenceId>/
sequence.config.ts Sequence definition (steps, trigger, timeout)
src/
emails/ React Email templates (.tsx)
render.ts Renders .tsx → .html with Liquid placeholders
package.json
tsconfig.jsonSequence config
A sequence config satisfies the SequenceDefinition type from @mailshot/shared:
import type { SequenceDefinition } from "@mailshot/shared";
export default {
id: "trial-expiring",
trigger: {
detailType: "trial.expiring",
subscriberMapping: {
email: "$.detail.email",
firstName: "$.detail.firstName",
attributes: "$.detail",
},
},
timeoutMinutes: 43200, // 30 days
steps: [
{ type: "send", templateKey: "trial-expiring/warning", subject: "Your trial ends soon" },
{ type: "wait", days: 2 },
{ type: "send", templateKey: "trial-expiring/last-chance", subject: "Last chance" },
],
} satisfies SequenceDefinition;Required fields
| Field | Description |
|---|---|
id | Unique kebab-case identifier |
trigger.detailType | EventBridge detail-type that starts this sequence |
trigger.subscriberMapping | JSONPath expressions to extract subscriber fields from the event payload |
timeoutMinutes | Maximum execution duration before Step Functions times out |
steps | Array of step objects |
Step types
Send
Invokes SendEmailFn to deliver an email:
{ type: "send", templateKey: "onboarding/welcome", subject: "Welcome!" }templateKeymaps to an S3 path:s3://bucket/onboarding/welcome.html- Pre-send checks run automatically (unsubscribed, suppressed). If a check fails, the step returns
{ sent: false }and the sequence continues — it does not throw.
Wait
Pauses the Step Functions execution. No compute runs during the wait — this is free.
{ type: "wait", days: 2 }
{ type: "wait", hours: 12 }
{ type: "wait", minutes: 30 }Choice
Native Step Functions branching on a field in the execution input. No Lambda invocation — the state machine evaluates this directly. Use for data that's available when the sequence starts (subscriber attributes from the triggering event).
{
type: "choice",
field: "$.subscriber.attributes.plan",
branches: [
{ value: "pro", steps: [/* ... */] },
{ value: "free", steps: [/* ... */] },
],
default: [/* fallback steps */],
}Choices can be nested. All branches converge automatically — steps after a choice run for every branch.
Condition
Lambda-based check that queries DynamoDB at runtime. Use when the data isn't in the execution input (e.g., checking if an email was already sent, or if a profile field changed after the sequence started).
{
type: "condition",
check: "has_been_sent",
templateKey: "onboarding/welcome",
then: [], // skip if already sent
else: [{ type: "send", templateKey: "onboarding/welcome", subject: "Welcome!" }],
}Available checks:
has_been_sent— requirestemplateKey. True if subscriber has received this template.subscriber_field_exists— requiresfield. True if the attribute exists and is non-empty.subscriber_field_equals— requiresfieldandvalue. True if the attribute matches.
Choice vs Condition
| Choice | Condition | |
|---|---|---|
| Evaluated by | Step Functions (native) | Lambda (CheckConditionFn) |
| Data source | Execution input (event payload) | DynamoDB (live query) |
| Cost | Free (state transition only) | Lambda invocation + DynamoDB read |
| Use when | Branching on subscriber attributes from the triggering event | Checking send history or profile changes after sequence start |
Fire-and-forget events
Optional one-off emails triggered by events during a sequence's lifetime:
events: [
{
detailType: "customer.first_sale",
templateKey: "onboarding/first-sale-congrats",
subject: "Congrats on your first sale!",
},
],These create separate EventBridge rules that invoke SendEmailFn directly (no Step Functions). The email is sent immediately when the event fires.
Auto-discovery
CDK scans sequences/*/sequence.config.ts at deploy time. You never need to manually register a sequence — just create the folder and deploy. The CDK constructs automatically:
- Create a Step Functions state machine from the config
- Create EventBridge rules for the trigger and any fire-and-forget events
- Upload rendered HTML templates to S3
Execution lifecycle
- Event arrives → EventBridge matches
detailType→ starts Step Functions execution - Register → SendEmailFn upserts subscriber profile, checks
unsubscribed/suppressedflags, records active execution. If the subscriber is unsubscribed or suppressed, registration throws and the execution fails immediately. - Steps execute → Send/wait/choice/condition steps run in order
- Complete → SendEmailFn deletes the execution record
- Timeout → If the execution exceeds
timeoutMinutes, Step Functions stops it
If a subscriber is already in an active execution of the same sequence, the old execution is stopped and replaced with the new one.
Visualizing sequences
Generate a Mermaid flowchart diagram of any sequence:
pnpm diagram onboardingOutputs build/onboarding/diagrams/diagram.mmd and diagram.png.