Sequences

Automated email sequences (Resend automations) triggered by events, list signups, new contacts or segments, with delays, waits, condition and random splits, contact updates and per-step stats.

A sequence is an automated series of steps for each contact who enters it: onboarding, trial nurture, re-engagement. Resend calls them automations; /automations is an alias of /sequences (see Resend aliases).

Triggers

trigger.typeEnrolls a contact when
eventYour app records the custom event event for it
list_joinedIt becomes a subscribed member of list_id (after double opt-in, if the list has it)
contact_createdIt is created, by the API, an import or a form (a double opt-in form signup enters when it confirms)
segment_enteredIt starts matching segment_id (checked every few minutes; contacts already matching when the sequence is activated are not enrolled)

A contact is enrolled once per sequence unless allow_reentry is true. POST /sequences/:id/enrollments enrolls up to 1,000 contacts by hand, ignoring the trigger but honoring re-entry. Unsubscribed contacts are never enrolled.

Steps

steps is a graph: an entry step id and a map of steps, each naming the step that follows (null ends the sequence).

{
  "name": "Trial onboarding",
  "trigger": { "type": "event", "event": "trial.started" },
  "steps": {
    "entry": "welcome",
    "steps": {
      "welcome": { "type": "send_email", "template_id": "tpl_…", "next": "wait" },
      "wait": {
        "type": "wait_for_event", "event": "project.created",
        "timeout": { "amount": 3, "unit": "days" },
        "next": "tips", "timeout_next": "nudge"
      },
      "nudge": { "type": "send_email", "template_id": "tpl_…", "next": null },
      "tips": { "type": "delay", "duration": { "amount": 2, "unit": "days" }, "next": "is_pro" },
      "is_pro": {
        "type": "condition",
        "filter": { "type": "property", "key": "plan", "operator": "eq", "value": "pro" },
        "if_true": null, "if_false": "upgrade"
      },
      "upgrade": { "type": "send_email", "template_id": "tpl_…", "subject": "Ready for Pro?", "next": null }
    }
  }
}
StepDoes
send_emailSends a published template, optionally overriding from and subject
delayWaits a fixed duration, until_time (the next of some weekdays at a time in a time zone) or until_date (a date on the contact, with an offset)
wait_for_eventWaits for event, then goes to next; after timeout, goes to timeout_next
conditionEvaluates a segment filter for the contact and goes to if_true or if_false
condition_splitChecks ordered paths, each with a segment filter; the contact takes the first that matches, else else_next
random_splitSends each contact down one of 2–5 paths at random, by whole-number weights that add up to 100
update_contactSets first_name, last_name or properties, inside a set object: {"type": "update_contact", "set": {"properties": {"plan": "pro"}}, "next": "..."}
add_to_list, remove_from_listChanges a list membership. add_to_list never overrides the contact: an opt-out or a pending double opt-in confirmation is kept, and a double opt-in list is skipped (only the contact's confirmation subscribes them)
exitEnds the enrollment

Delays

A delay sets exactly one of:

{ "type": "delay", "duration": { "amount": 2, "unit": "days" }, "next": "…" }
{ "type": "delay", "until_time": { "weekdays": ["mon", "thu"], "time": "09:00", "timezone": "Europe/Berlin" }, "next": "…" }
{ "type": "delay", "until_date": { "property": "renews_at", "offset": { "amount": 3, "unit": "days", "direction": "before" } }, "next": "…" }
{ "type": "delay", "until_date": { "property": "renews_at", "time": "09:00", "timezone": "America/New_York" }, "next": "…" }

until_time continues at the first listed weekday and time strictly after the contact arrives, in the given IANA time zone (daylight saving changes included). until_date waits for a custom date property, or created_at, moved by the optional offset. A date-only value (2026-12-25) is stored as midnight UTC, which is the evening before in the Americas: add time and timezone (both or neither) to continue at that wall-clock time on the contact's date in that zone, then apply the offset (with "direction": "before" and 1 day, the 24th at 09:00 New York time). A full timestamp is read on that zone's clock. If the contact has no valid date, or the moment has passed, the contact continues at once and the enrollment history records why (continued:property_missing, continued:property_invalid or continued:date_passed).

Splits

{
  "type": "condition_split",
  "paths": [
    { "name": "Pro", "filter": { "type": "property", "key": "plan", "operator": "eq", "value": "pro" }, "next": "pro_tips" },
    { "name": "Team", "filter": { "type": "property", "key": "plan", "operator": "eq", "value": "team" }, "next": "after" }
  ],
  "else_next": "after"
}
{
  "type": "random_split",
  "paths": [
    { "name": "A", "weight": 50, "next": "subject_a" },
    { "name": "B", "weight": 50, "next": "subject_b" }
  ]
}

A path may continue straight to the step where the paths meet again (an empty path). A random split assigns each enrollment by a hash of the enrollment and step ids, so a retried or resumed enrollment always takes the same path. The two-way condition step keeps working.

The graph is checked when you save it: every referenced step, template, list and date property must exist, every step must be reachable, every loop must pass a timed wait (a delay for a duration or until a weekday and time, or a wait_for_event), random split weights must add up to 100, and every filter must compile. A sequence has up to 100 steps.

Properties of the triggering event become template variables of the emails it sends, so a trial.started event with { "trial_end": "Nov 3" } fills {{ trial_end }}.

Lifecycle

StatusMeaning
draftEditable, not enrolling
activeEnrolling on its trigger and running enrollments
pausedNot enrolling; enrollments hold where they are until it is activated again
archivedFinal: every live enrollment is canceled

POST /sequences/:id/activate needs the project's physical address when the sequence sends email. Changing steps creates a new graph version: contacts already enrolled finish on the version they started.

Each email is checked before it goes out. A contact who unsubscribed (or left the trigger list of a list_joined sequence: opted out, removed, or the list was deleted) exits the sequence; a suppressed address or an opt-out of a list is skipped and the sequence continues. A step that fails temporarily is retried with backoff from 5 minutes up to 6 hours (8 attempts, then the enrollment fails). A step blocked by something you fix outside the sequence (the monthly marketing send quota, an unverified sender domain, an unpublished template, a missing physical address or sender) waits on that step, checked again hourly, and continues once it is fixed: it never fails the enrollment. Sequence emails count as marketing emails and carry the same unsubscribe headers and footer as campaigns; their unsubscribe link opts the contact out of all marketing email.

Test and monitor

  • POST /sequences/:id/test is a dry run for one contact: conditions are evaluated against the real contact, emails are rendered, and nothing changes. assume_events lists events a wait should treat as received. From a dashboard session, send_to with your own address also sends each email to you, with [Test] in the subject.
  • GET /sequences/:id/enrollments lists enrollments with their status (active, waiting, completed, exited, canceled, failed), current step and step history. Cancel one with POST /sequences/:id/enrollments/:enrollment_id/cancel.
  • GET /sequences/:id/stats returns per-step counters: entered, completed, sent, skipped, failed, exited, branch outcomes, events received and timeouts, emails opened and clicked (for send_email steps), plus how many enrollments are on each step now.

Reference: Sequences.

On this page