> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sequenzy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sequences

> Build automated email workflows that respond to subscriber behavior

# Sequences (Automations)

Sequences are automated email workflows that send the right message at the right time based on subscriber actions, tags, segments, or events. They're the heart of marketing automation in Sequenzy.

## What Are Sequences?

A sequence is a series of connected steps that execute automatically:

```
┌─────────────────┐
│    TRIGGER      │  ← Starts the sequence
│ (Tag or Event)  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│     ACTION      │  ← Does something
│  (Send Email)   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│     LOGIC       │  ← Controls flow
│ (Wait 3 days)   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│     ACTION      │
│ (Send Email #2) │
└─────────────────┘
```

## Sequence Components

### Triggers

Triggers define **when** a sequence starts. Each sequence has exactly one trigger.

| Trigger Type      | Description                                     | Use Case             |
| ----------------- | ----------------------------------------------- | -------------------- |
| `contact_added`   | Subscriber added to list                        | Welcome sequence     |
| `tag_added`       | Tag applied to subscriber                       | Customer onboarding  |
| `segment_entered` | Subscriber enters segment                       | Lifecycle automation |
| `event_received`  | Event triggered via API                         | Post-purchase flow   |
| `inbound_webhook` | Event from a connected app such as Cal.com      | Booking follow-up    |
| `inactivity`      | Contact has not triggered an event for N days   | Win-back             |
| `frequency`       | Contact triggers an event N times within Y days | Power-user outreach  |

### Segment-Entered Trigger Behavior

The `segment_entered` trigger is designed to avoid surprise sends.

* When you activate a sequence, Sequenzy uses the current segment members as a baseline and does **not** enroll them automatically.
* Only contacts who enter the segment after that baseline are enrolled.
* Segment triggers are evaluated after relevant subscriber data changes, with a background check at least every 8 minutes as a fallback.
* You can optionally stop active contacts if they later leave the triggering segment.
* If you edit the segment definition or reactivate the sequence later, Sequenzy creates a new baseline instead of backfilling historical matches.

If you want to send the sequence to contacts who are already in the segment, use **Enroll Existing Subscribers** manually.

### What does not start a sequence

Only the trigger types above start a sequence. In particular, **updating a subscriber's custom attributes never enrolls anyone**. Attributes describe a contact, so writing one is not something that happened at a point in time and nothing listens for it.

If your system updates an attribute and you want a sequence to run, pick one of these instead:

* **Send an event** for the thing that happened. This is the better option in almost every case: enrollment is immediate, and the event payload stays available to every email in the run as `{{event.*}}` merge tags.
* **Build a segment** on the attribute and use a `segment_entered` trigger. This works, but membership is recomputed after relevant changes rather than at the moment you write the attribute, so it is a poor fit when timing matters.

Keeping the attribute is still worthwhile - attributes are what you filter segments on and personalize with. They just are not a starting gun.

### Actions

Actions perform operations on the subscriber.

| Action Type         | Description                                           |
| ------------------- | ----------------------------------------------------- |
| **Send Email**      | Send an email from your templates                     |
| **Update Tags**     | Add a tag or remove a tag from a subscriber           |
| **Update Lists**    | Add a subscriber to a list or remove them from a list |
| **Create Discount** | Mint a per-subscriber Stripe or Shopify discount code |
| **Webhook**         | Call an external URL with subscriber data             |

When you add an **Update Tags** step in the builder, you choose whether that step should add a tag or remove one. You can change that choice later by editing the step.

When you add an **Update Lists** step, you choose whether the step should add the subscriber to a list or remove them from one. This is useful for routing contacts into new subscription tracks, suppressing them from a list mid-sequence, or chaining list-triggered sequences together.

### Discount Codes In Emails

A **Create Discount** step mints a fresh code for each subscriber who reaches it, using your connected Stripe or Shopify account. Because the code is generated per subscriber, you reference it with merge tags rather than typing it into the email:

| Merge tag                     | Renders as                                        |
| ----------------------------- | ------------------------------------------------- |
| `{{discount.code}}`           | The generated code, for example `WINBACK-8F2A`    |
| `{{discount.value}}`          | Pre-formatted offer, for example `20% off`        |
| `{{discount.percentOff}}`     | `20`                                              |
| `{{discount.amountOff}}`      | Amount in the smallest currency unit, e.g. `1000` |
| `{{discount.currency}}`       | `usd`                                             |
| `{{date.discount.expiresAt}}` | Expiry formatted for the recipient's locale       |

These resolve for any recipient who has passed the discount step earlier in their run. The cleanest way to present the code is a **Discount Code** block, which is pre-filled with `{{discount.code}}`, but the tag also works inline in any text block.

<Note>
  If a step can be reached by branches that don't all pass through the discount
  step, the tags stay empty for the recipients who took a branch that skipped
  it, and only for those. Preview an email with [Render Sequence Step
  HTML](/api-reference/sequences/render) to check the copy: it fills the tags
  with a placeholder `TEST-CODE` and reports anything it could not resolve. A
  preview has no way to know which branch a recipient took, so it only fills the
  tags when every path into the step runs the same discount step - and in the
  branchy case it lists them as `no_value` rather than as unrecognized names,
  because they do resolve for the recipients who took the discount path.
</Note>

### Logic Nodes

Logic nodes control the flow of the sequence.

| Logic Type         | Description                                       |
| ------------------ | ------------------------------------------------- |
| **Delay**          | Wait a fixed time, or until a date from the event |
| **Condition**      | Branch based on a single condition                |
| **Branch**         | Multi-path branching (if/else if/else)            |
| **Wait for Event** | Pause until an event occurs or timeout            |

### Delay: fixed duration or "wait until a date"

A Delay step has two modes, toggled by the **Wait until a date** switch in the
step editor:

* **Fixed duration** (default): wait a set number of days, hours, and minutes
  before continuing.
* **Wait until a date**: resume on a date pulled from the event that triggered
  the sequence. Point the step at a property in the event payload (for example
  `renews_at` or `booking.startsAt`). The value must be an ISO date string or a
  Unix timestamp.

In "wait until" mode you can add an optional offset to resume before or after
the resolved date, for example **1 day before** a renewal or **2 hours after** a
booking. If the date is missing or cannot be read, the step either continues to
the next step or exits the sequence, depending on the fallback you choose. If
the resolved date is already in the past, the step continues immediately.

Because the date is read from the trigger event, "wait until a date" is most
useful with event-triggered sequences. It is configured in the dashboard
sequence builder.

### Sending Windows

Sequences can have an optional local-time sending window, such as weekdays from
08:00 to 20:00. A trigger can still enroll the subscriber immediately, but every
email action waits until the next allowed local time before sending. This applies
to the first email and later emails after delays, branches, or wait steps.

### BCC Copies

You can set one or more optional BCC addresses in a sequence's settings
(separate multiple addresses with commas, up to 10). Every email the sequence
sends then delivers a hidden copy to those addresses, which is useful when a
team inbox (like customer support) needs visibility into what subscribers
receive. BCC recipients are invisible to the subscriber. Leave the field empty
to turn it off.

### File Attachments

Email steps can carry real file attachments - useful for lead magnets, guides,
or any file the subscriber expects after triggering the sequence. Attachments
are URL-backed: you provide a public HTTPS URL plus a filename, and Sequenzy
fetches the file at send time and attaches it to the email. This keeps the
sequence config small and lets you update the file at its source without
editing the sequence.

In the dashboard, open an email step, click the settings (gear) icon in the
top-right corner of the editor, and use the **Attachments** section to upload
a file - it's stored on Sequenzy's file hosting and attached from there at
send time. Choose **From trigger** when every enrollment has a different file,
such as an eTicket or invoice. The URL and filename can use values from the
event that enrolled the subscriber:

```json theme={null}
[
  {
    "filename": "file-{{event.order_id}}.pdf",
    "path": "{{event.file_url}}"
  }
]
```

The same shape works through the API, CLI, and MCP tools. Static public HTTPS
URLs remain supported, and an empty array removes all attachments. Dynamic
attachment values are resolved from the enrollment's original event snapshot,
then the final filename and public URL are validated before the file is fetched.
If a required event value is missing or invalid, the step fails without sending
an attachment-less email.

Limits: up to 10 attachments per email and 7MB total per email (after
fetching). If the URL stops resolving, the send is retried and then recorded
as a failed step, so keep attachment URLs stable while a sequence is enabled.
Signed URLs must remain valid until the step sends and its fetch retries finish.

For deliverability, prefer a hosted download link (which is also click-tracked)
when the file is large or the audience is broad; use a real attachment when
recipients expect the file directly in their inbox, such as a lead-magnet
delivery.

## Building Sequences

### The Visual Editor

Sequences are built in a visual drag-and-drop editor:

1. **Add a trigger** - How does the sequence start?
2. **Add actions** - What should happen?
3. **Add logic** - When and how should it happen?
4. **Connect nodes** - Define the flow between steps
5. **Activate** - Turn on the sequence

<Tip>
  Already have subscribers who match your trigger? Use [Enroll Existing
  Subscribers](/concepts/enroll-existing-subscribers) to add them retroactively.
</Tip>

### Editor Shortcuts and Tools

The editor supports fast, keyboard-friendly workflows:

* **Undo / redo** - Cmd+Z undoes any unsaved canvas edit (add, edit, delete,
  reorder, and even Discard changes); Cmd+Shift+Z redoes it. History clears
  once changes are saved.
* **Step finder** - Cmd+F opens a search across every step by name or
  subject and jumps the canvas to the selected one.
* **Arrow navigation** - Arrow keys walk between steps (left/right move
  across branch paths). Enter opens the focused email step, Delete removes
  the focused step, Escape clears the focus.
* **Duplicate step** - Every action and delay step has a Duplicate button in
  its editor. Duplicating an email step also clones the email itself, so you
  can save the sequence and then edit the copy without touching the original.
* **Collapse branches** - Wide branch sections collapse into a compact
  summary card (hover the branch step for the toggle). The hidden steps stay
  part of the sequence; this only tidies up the canvas.
* **View toggles** - M toggles per-step metrics, F toggles the funnel view,
  G opens goals, T opens a test run, and Comma opens settings.

### Deleting Steps While People Are In Them

You can delete an otherwise-deletable action, delay, or logic step while
subscribers are inside it. Anyone currently in the deleted step moves to the
next step immediately and continues through the sequence - the same path new
enrollees take after the edit. If the deleted step was the last one, those
subscribers complete the sequence. The editor shows a confirmation with the
number of affected people before the delete, and the canvas displays a notice
if any subscribers ended up stuck on a step that was removed in the past.

Trigger and end steps cannot be deleted. A/B test steps also remain protected
while variants are generating or while a test is running or has selected a
winner.

Recipient migration is refused when people are inside a deleted branch step
and multiple outgoing paths survive. There is no single next step to move them
to, so reconnect the graph to one continuation first or wait for them to leave
the step.

### Simulate A Subscriber's Path

Use **Simulate path** in the toolbar to dry-run a subscriber through the
sequence without sending anything. Sequenzy evaluates every condition and
branch with the same logic the send path uses, based on the subscriber's
current tags, lists, segments, and attributes, then highlights the resulting
path on the canvas with a breakdown of each decision.

Conditions that depend on what happens during the run - like "clicked a link
in this sequence" or event-payload fields - cannot be known ahead of time.
The simulation flags these as assumptions and follows the fallback path, the
same default the live run would take if the activity never happened.

Wait-for-event steps are shown as pass-through steps in the preview. The
preview does not predict whether their event will arrive or whether they will
time out; use a live test when that timing-dependent outcome matters.

### Funnel View

On an active or paused sequence, press F (or use the funnel toggle in the
toolbar) to see where the sequence leaks. The funnel only annotates what
matters:

* **Entry volume** - one chip at the top shows how many subscribers ever
  entered the sequence.
* **Losses** - a connection only gets a chip when subscribers were actually
  lost between the two steps, shown as `-N` with the continuation
  percentage. Hover the chip for the breakdown: how many failed, stopped
  early (stop condition or manual removal), or finished the sequence there.
  Subscribers still waiting in a delay are not counted as lost.
* **Branch splits** - fan-out connections show how traffic distributes
  across the paths.
* **Engagement decay** - each email step shows its open-rate change versus
  the previous email (for example `opens -4.2pts vs prev`). In a linear
  sequence almost everyone reaches the next step, so this is usually the
  real drop-off signal: it tells you which email to rewrite.

Connections where nothing was lost stay unannotated, so anything you see in
funnel view is worth looking at. Combine it with the metrics view (M) to see
absolute opens, clicks, and conversions per email at the same time.

### Create From A Dashboard Prompt

You can also start a sequence directly from the dashboard landing page or onboarding flow with a prompt.

* Try prompts like `Create a welcome sequence` or `Build a re-engagement sequence`
* During onboarding, the assistant also suggests starter questions like `What do I start with?` and `How can I add more about my company?`
* You can also ask review questions like `Show my top performing sequence` to jump from a performance check into the sequence itself
* If Sequenzy recognizes the intent, it opens a matching starter template in the builder
* If your request is more custom, Sequenzy creates a blank draft sequence and takes you straight into editing
* If you keep the conversation going, the assistant opens a follow-up chat so you can ask for more detail without repeating the earlier request

### Run A Live Sequence Test

Use **Run sequence test** in the sequence builder when you want to check the whole workflow with one real subscriber before activating or changing a live sequence.

Sequence test runs execute the same graph that production uses:

* The run starts from the sequence trigger and follows connected steps in order
* Branches, conditions, event-property merge tags, discounts, tags, lists, subscriber attributes, and webhooks use the selected subscriber's real data
* Delay and wait steps are compressed with the selected speed multiplier, such as 60x, 120x, 240x, or 480x
* Sequence emails are sent through the test-email path with a `[TEST]` subject prefix
* Recent test runs show step-by-step logs, accelerated wait timing, sent-email references, and failures

<Warning>
  Non-email actions run for real on the selected subscriber. Use a safe internal
  or dedicated test subscriber when checking flows that update tags, lists,
  attributes, discounts, or webhooks.
</Warning>

Before starting a run:

1. Save any pending changes in the builder.
2. Choose one existing active subscriber. Direct email recipients are not available for live sequence tests because the run needs subscriber state.
3. Pick a speed multiplier. `120x` turns a 1-hour wait into about 30 seconds.
4. Confirm the run. Sequenzy prevents another queued or running test for the same sequence and subscriber.

Test emails are excluded from the normal sequence sent-email filters, but the run log keeps references to the email send records created during the test.

### Restrict A Live Trigger To An Allowlist

A sequence test starts from the trigger node, so it does not tell you whether
your real event or segment trigger actually fires and matches. When you want the
production trigger to run for real but only a chosen set of contacts to receive
the sequence - a first pass on the live trigger, a phased rollout, or a pilot
group - use a negative stop condition as an allowlist.

Stop conditions are re-evaluated before every step, including the first one. So
`does_not_have_tag` cancels the run before anything sends for anyone who does
not carry the tag:

1. Tag the contacts you want to receive the sequence, for example `allowlist`.
2. Set the sequence stop condition to `does_not_have_tag` with value `allowlist`.
3. Activate the sequence and fire the real trigger.

Everyone the trigger picks up is cancelled at the trigger node except your
tagged contacts. `removed_from_list` works the same way against a list instead
of a tag. Clear the stop condition, or replace it with the real lifecycle stop,
before you open the sequence to everyone.

```bash theme={null}
sequenzy sequences update seq_123 \
  --stop-condition-json '{"type":"does_not_have_tag","value":"allowlist"}'
```

<Note>
  Guarded-out contacts still enroll and are then cancelled at the trigger node,
  so no step runs for them. They show up as cancellations on the trigger node,
  not in the active or waiting enrollment counts, and the funnel entry chip
  (which counts contacts that reached the first step) does not include them.
</Note>

<Warning>
  Clearing the guard does not retry the contacts it already cancelled. Nothing
  re-enrolls them on its own, so they only receive the sequence if the trigger
  fires for them again - which one-shot triggers such as a purchase event or
  `contact_added` never do. On the `one_time` enrollment mode it is permanent
  even then: enrollment is blocked by the existence of any earlier run, not by
  its outcome, so a repeated trigger is ignored too. Guard a duplicate sequence
  and leave the real one ungated, or enroll the missed contacts afterwards with
  `sequenzy sequences enroll`, which skips the `one_time` check.
</Warning>

A sequence has one stop condition, so while the guard is in place you cannot
also use a stop condition for its normal lifecycle purpose. For that reason this
works best on a duplicate of the sequence rather than the live one.

### Example: Welcome Sequence

```
┌─────────────────────┐
│ Trigger: Tag Added  │
│ "new-signup"        │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Send: Welcome Email │
│ "Getting Started"   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Delay: 2 days       │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Send: Tips Email    │
│ "5 Pro Tips"        │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Delay: 3 days       │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Send: Feature Email │
│ "Discover Features" │
└─────────────────────┘
```

### Example: Post-Purchase Sequence

```
┌────────────────────────┐
│ Trigger: Event         │
│ "saas.purchase"        │
└───────────┬────────────┘
            │
            ▼
┌────────────────────────┐
│ Send: Thank You Email  │
└───────────┬────────────┘
            │
            ▼
┌────────────────────────┐
│ Delay: 7 days          │
└───────────┬────────────┘
            │
            ▼
┌────────────────────────┐
│ Wait for Event:        │
│ "feature_activated"    │
│ Timeout: 14 days       │
└───────────┬────────────┘
            │
       ┌────┴────┐
       │         │
  Event Fired  Timeout
       │         │
       ▼         ▼
┌───────────┐ ┌───────────┐
│ Add Tag   │ │ Send Help │
│ "engaged" │ │ Email     │
└───────────┘ └───────────┘
```

## Personalizing Sequence Emails With Event Properties

When a sequence starts from an event trigger, Sequenzy stores that event's properties on the sequence run. You can use those values in any later email in the same sequence with `event.` merge tags.

Examples:

* `{{event.city}}`
* `{{event.windMaxSpeed}}`
* `{{event.alert.maxSpeed}}`

This is useful when the event contains context that should stay stable across multiple emails, such as a weather alert region, the purchased plan, or the trial length that started the flow.

### How It Works

* The event snapshot is stored on the automation run when the subscriber enters the sequence
* Every later email in that same sequence reads from the same stored snapshot
* A later event for the same subscriber does not overwrite earlier sequence runs

That means your second or third email still references the original event data that enrolled the subscriber.

### Example

If your app sends:

```json theme={null}
{
  "event": "weather.wind_alert",
  "properties": {
    "city": "Tel Aviv",
    "windMaxSpeed": 75,
    "duration": {
      "hours": 6
    }
  }
}
```

You can write:

* Subject: `Wind alert for {{event.city}}`
* Body: `Winds may reach {{event.windMaxSpeed}} km/h for {{event.duration.hours}} hours.`

### Defaults

You can use fallback values if a property may be missing:

* `{{event.city|your area}}`
* `{{event.duration.hours|a few}}`

### Using Event Properties in Update Subscriber Steps

The same merge tags work in **Update Subscriber** step values, so a sequence can copy event data onto the subscriber record:

* Set a `plan` attribute to `{{event.plan}}`
* Set an `mrr` number attribute to `{{event.amount}}` (the resolved value is stored as a number)
* Set first name to `{{event.firstName}}`

In the dashboard, type `{{` in a value field to autocomplete properties from the trigger event's recent payloads, plus subscriber attributes and built-in fields. If a referenced property is missing from the event and the tag has no fallback, that field is skipped rather than being set to an empty value.

API, CLI, and MCP callers can create the same step explicitly:

```json theme={null}
{
  "type": "update_subscriber",
  "nodeType": "action_update_attributes",
  "config": {
    "firstName": "{{event.firstName}}",
    "customAttributeUpdates": [
      { "name": "plan", "value": "{{event.plan}}", "valueType": "text" },
      { "name": "mrr", "value": "{{event.amount}}", "valueType": "number" },
      { "name": "active", "value": "{{event.active}}", "valueType": "boolean" }
    ]
  }
}
```

Number and boolean values must be literals or one standalone merge tag. Use `subscriberUpdateSteps` with the node ID to replace an existing Update Subscriber step's config through the API, CLI, or MCP.

## Enrollment Modes

Enrollment mode controls whether the same subscriber can enter a sequence again. It defaults to `unlimited`, so a tag-triggered drip sends again if the tag is ever re-applied - set `one_time` unless repeat runs are what you want. Back-in-stock and replenishment sequences are the exception: they default to `matching_field` so each product is tracked separately.

You can change the mode at any time, on a draft or a live sequence: in the dashboard under the sequence's enrollment settings, or by passing `enrollmentMode` to [Update Sequence](/api-reference/sequences/update), `sequenzy sequences update`, or the `update_sequence` MCP tool. Changing it affects future enrollments only; runs already in flight continue.

| Mode             | Behavior                                                                                        |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| `unlimited`      | A subscriber can re-enter after a previous run finishes, but cannot have duplicate active runs. |
| `one_time`       | A subscriber can enter the sequence once ever.                                                  |
| `matching_field` | Event-triggered sequences can have one active run per matching event field value.               |

`matching_field` is only available when the sequence starts from an event. Use it when the same subscriber may legitimately have multiple active runs, but each run belongs to a different event-scoped object.

Examples:

* `order.id` - one active run per order
* `product.providerVariantId` - one active run per product variant
* `subscription.id` - one active run per subscription

In the dashboard, the **Matching field path** input suggests fields from recent payloads for the selected trigger event. Through the API, CLI, and MCP, set `enrollmentMode` to `matching_field` and pass `enrollmentFieldPath`.

The path must resolve to one scalar value. Array traversal such as `lineItems[].providerProductId` is not supported for enrollment keys; use `propertyFilters` to filter on array items, then choose a scalar order, product, variant, or subscription identifier for `enrollmentFieldPath`.

```json theme={null}
{
  "name": "Post-purchase follow-up",
  "trigger": "event_received",
  "eventName": "ecommerce.order_placed",
  "enrollmentMode": "matching_field",
  "enrollmentFieldPath": "order.id",
  "goal": "Follow up about this specific order"
}
```

For Shopify back-in-stock and replenishment sequences, you can leave `enrollmentFieldPath` empty. Sequenzy uses built-in product and variant matching defaults for those events.

### Unsubscribed and Bounced Contacts

Unsubscribed and bounced contacts still enroll when a trigger matches. This keeps sequences useful beyond marketing email: tag changes, attribute updates, webhooks, and emails marked as transactional (for example an order-shipped notification) all still run for them. Marketing email steps are skipped automatically for these contacts.

If a contact unsubscribes while already moving through a sequence, the remaining enrollment is cancelled instead - the sequence's own emails were the most likely cause, so continuing would only produce more skipped sends.

## Commerce Sequence Stop Behavior

Commerce event triggers get product-aware defaults so subscribers do not keep receiving product sequences after the sequence is no longer relevant.

### Replenishment Due

Sequences triggered by `ecommerce.replenishment_due` default to stopping when the same product is purchased again.

Sequenzy matches purchases by:

* `provider`
* `providerProductId`
* `providerVariantId` when the replenishment setting uses variant-level matching
* `lineItems[].providerProductId` and `lineItems[].providerVariantId` on later `ecommerce.order_placed` events

This means a replenishment sequence for one product does not stop just because the subscriber bought a different product.

### Back In Stock

Sequences triggered by `ecommerce.back_in_stock` default to stopping when either of these happens:

* The subscriber buys the same item
* The same item goes out of stock again

Back-in-stock matching checks:

* `provider`
* `providerProductId` or `product.providerId`
* `providerVariantId` or `product.providerVariantId`
* `lineItems[].providerProductId` and `lineItems[].providerVariantId` on later purchase events
* `ecommerce.back_in_stock_out_of_stock` events for the same product and variant

If you trigger these commerce events manually, send the same product fields that Shopify and WooCommerce send. See [Events](/concepts/events#commerce-product-matching) for the expected shape.

## Condition Types

Conditions let you branch sequences based on subscriber data.

### Tag Conditions

```
Condition: has_tag "customer"
  ├─ Yes → Send upsell email
  └─ No  → Send conversion email
```

### List Conditions

```
Condition: in_list "VIP Customers"
  ├─ Yes → Send exclusive offer
  └─ No  → Send standard offer
```

### Segment Conditions

```
Condition: in_segment "Project invite recipients"
  ├─ Yes → Send project onboarding
  └─ No  → Send standard onboarding
```

### Event Conditions

```
Condition: event_received "project.invite.accepted"
  ├─ Yes → Start accepted-invite flow
  └─ No  → Send reminder
```

Event checks can be scoped with `activityScope`: `this_sequence` checks events
received since the contact entered this sequence, `previous_email` checks events
tied to the last sequence email, and `ever` checks the contact's full event
history.

### Clicked Link Conditions

```
Condition: link_clicked "project-invites"
  ├─ Yes → Start project setup flow
  └─ No  → Send invite reminder
```

Leave the URL empty to match any tracked click, or enter part of the URL to
match a specific link. Click checks support the same `activityScope` values:
`this_sequence`, `previous_email`, and `ever`.

### Field Conditions

```
Condition: field_equals "plan" "enterprise"
  ├─ Yes → Assign to sales team
  └─ No  → Send self-serve resources
```

### Available Operators

| Operator             | Description                        | Example                                                        |
| -------------------- | ---------------------------------- | -------------------------------------------------------------- |
| `has_tag`            | Subscriber has tag                 | `has_tag "customer"`                                           |
| `does_not_have_tag`  | Subscriber does not have tag       | `does_not_have_tag "customer"`                                 |
| `in_list`            | Subscriber in list                 | `in_list "Newsletter"`                                         |
| `in_segment`         | Subscriber matches a saved segment | `in_segment "Project invite recipients"`                       |
| `event_received`     | Event was received                 | `event_received "project.invite.accepted" scope=this_sequence` |
| `link_clicked`       | Link was clicked                   | `link_clicked "project-invites" scope=previous_email`          |
| `field_equals`       | Field exact match                  | `field_equals "plan" "pro"`                                    |
| `field_contains`     | Field contains string              | `field_contains "company" "Inc"`                               |
| `field_greater_than` | Numeric comparison                 | `field_greater_than "mrr" 100`                                 |
| `field_less_than`    | Numeric comparison                 | `field_less_than "age" 30`                                     |
| `has_phone`          | Subscriber has a phone number      | `has_phone`                                                    |
| `sms_subscribed`     | Subscriber opted in to SMS         | `sms_subscribed`                                               |

## Wait for Event

The "Wait for Event" node pauses execution until a named event is recorded for the contact.

### What you can wait for

Choose any built-in or custom event name:

| Event name               | Waits for                             |
| ------------------------ | ------------------------------------- |
| `email.opened`           | The contact opens an email            |
| `email.clicked`          | The contact clicks a link in an email |
| `email.replied`          | The contact replies to an email       |
| `ecommerce.order_placed` | The contact places an order           |
| Any custom event         | The event you send through the API    |

The node matches on the event **name** only. Unlike a trigger, it cannot be narrowed with property filters, so any occurrence of that event name resolves the wait - a contact waiting on `ecommerce.order_placed` after buying one product is released by an order for any other product too.

If you need the run to be about one specific product, order, or variant, scope the whole sequence with [`matching_field` enrollment](/api-reference/sequences/create) rather than trying to narrow the wait. That gives each contact a separate run per event field value, so each run's wait is already about the right object.

### Timeout behavior

Every wait has a timeout measured in days. A wait has a **single outgoing path** - the timeout does not create a second branch. When the timeout expires, the contact takes one of two actions you choose:

| Timeout action | Result                                                               |
| -------------- | -------------------------------------------------------------------- |
| **Continue**   | The contact moves to the next step, the same step the event leads to |
| **Exit**       | The run ends and the contact leaves the sequence                     |

Because "continue" reuses the same path, a step placed after the wait runs both for contacts whose event arrived and for contacts who timed out. To treat those two groups differently, put a Condition after the wait:

```
┌──────────────────────┐
│ Wait for Event:      │
│ "demo_scheduled"     │
│ Timeout: 5 days      │
│ On timeout: continue │
└──────────┬───────────┘
           │  event received OR timed out
           ▼
┌──────────────────────┐
│ Condition:           │
│ event_received       │
│ "demo_scheduled"     │
└──────────┬───────────┘
      ┌────┴────┐
     Yes        No
      │         │
      ▼         ▼
┌──────────┐ ┌──────────┐
│ Send     │ │ Send     │
│ Prep     │ │ Follow   │
│ Email    │ │ Up Email │
└──────────┘ └──────────┘
```

## Branching

### Simple Condition (2 paths)

```
┌─────────────────────┐
│ Condition:          │
│ has_tag "customer"  │
└──────────┬──────────┘
           │
      ┌────┴────┐
      │         │
     Yes        No
      │         │
      ▼         ▼
┌──────────┐ ┌──────────┐
│ Upsell   │ │ Convert  │
│ Email    │ │ Email    │
└──────────┘ └──────────┘
```

### Multi-Branch (3+ paths)

```
┌─────────────────────────────┐
│ Branch:                     │
│ ├─ plan = "enterprise"      │
│ ├─ plan = "pro"             │
│ └─ else (default)           │
└─────────────┬───────────────┘
              │
     ┌────────┼────────┐
     │        │        │
 Enterprise  Pro    Default
     │        │        │
     ▼        ▼        ▼
┌────────┐ ┌────────┐ ┌────────┐
│ VIP    │ │ Pro    │ │ Basic  │
│ Email  │ │ Email  │ │ Email  │
└────────┘ └────────┘ └────────┘
```

### Empty paths

A path does not have to contain a step. Subscribers who take an empty path pass
straight through to whatever comes after the branch, so you can save and
activate a sequence with empty paths. The editor shows where each empty path
leads and highlights empty paths while the sequence is live. When an empty path
continues into a step that only it feeds, you can also remove the empty path
tile without changing where its subscribers go.

## Sequence States

| State      | Description                                         |
| ---------- | --------------------------------------------------- |
| `draft`    | Being edited, not active                            |
| `active`   | Running, processing subscribers                     |
| `paused`   | Holding existing subscribers and blocking new entry |
| `archived` | No longer in use                                    |

Paused sequences do not execute nodes. Subscribers already in the sequence stay
at their current step, and new subscribers who match the trigger are not
enrolled while the sequence is paused. When the sequence resumes, due
subscribers continue gradually from the step where they were held.

You can also stop new enrollments without pausing the sequence. In that mode,
current subscribers keep moving until completion, but new trigger matches are
not enrolled.

Waits are wall-clock based. If a subscriber is waiting three days and the wait
expires while the sequence is paused, the subscriber becomes due but does not
continue until the sequence resumes.

### Sending Paused

Company sending pauses and real account bans are parent locks, not
manual sequence pauses. An active sequence remains active, still captures new
entrants, and keeps tokens at their current step while email sending is blocked.
The dashboard shows this as **Sending paused** so you can distinguish it from a
sequence you paused yourself.

When sending is restored, due sequence work wakes automatically. If you do not
want a specific sequence to continue after sending is restored, pause that
sequence manually; the manual pause remains until you resume it.

## Tokens (Subscriber Journeys)

When a subscriber enters a sequence, a "token" tracks their progress:

```json theme={null}
{
  "subscriberId": "sub_abc123",
  "automationId": "auto_xyz",
  "currentNode": "node_456",
  "status": "active",
  "context": {
    "entryPoint": "tag_added",
    "emailsSent": 2,
    "waitingFor": "email_opened"
  }
}
```

### Token States

| State       | Description                                 |
| ----------- | ------------------------------------------- |
| `active`    | Currently progressing through sequence      |
| `waiting`   | Held at a wait/delay or wait-for-event node |
| `completed` | Finished all steps                          |
| `failed`    | Error occurred                              |
| `cancelled` | Manually stopped                            |

## Real-World Sequence Examples

### SaaS Trial to Paid

```
Trigger: Event "saas.trial_started"
    │
    ▼
┌───────────────────┐
│ Send: Trial       │
│ Welcome Email     │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│ Wait: 3 days      │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│ Send: Tips Email  │
│ "Get the Most..." │
└─────────┬─────────┘
          │
          ▼
┌───────────────────┐
│ Wait: 4 days      │
└─────────┬─────────┘
          │
          ▼
┌───────────────────────┐
│ Condition: has_tag    │
│ "customer"            │
└───────────┬───────────┘
            │
       ┌────┴────┐
       │         │
      Yes        No
       │         │
       ▼         ▼
   (End)    ┌───────────────┐
            │ Send: Last    │
            │ Chance Email  │
            └───────────────┘
```

### E-commerce Abandoned Cart

```
Trigger: Event "ecommerce.cart_started"
Continue while: Tag "ecommerce.in_cart"
    │
    ▼
┌───────────────────┐
│ Wait: 1 hour      │
└─────────┬─────────┘
          │
          ▼
┌───────────────┐
│ Send: Cart    │
│ Reminder      │
└───────┬───────┘
        │
        ▼
┌───────────────┐
│ Wait: 1 day   │
└───────┬───────┘
        │
        ▼
┌───────────────┐
│ Send: 10% Off │
│ Coupon        │
└───────────────┘
```

### Re-engagement Campaign

```
Trigger: Tag Added "inactive-30d"
    │
    ▼
┌───────────────────┐
│ Send: We Miss You │
└─────────┬─────────┘
          │
          ▼
┌─────────────────────┐
│ Wait for Event:     │
│ "session_started"   │
│ Timeout: 7 days     │
└─────────┬───────────┘
          │
     ┌────┴────┐
     │         │
   Event    Timeout
     │         │
     ▼         ▼
┌──────────┐ ┌──────────┐
│ Remove   │ │ Send     │
│ Tag      │ │ Special  │
│ inactive │ │ Offer    │
└──────────┘ └────┬─────┘
                  │
                  ▼
             ┌──────────┐
             │ Wait     │
             │ 14 days  │
             └────┬─────┘
                  │
                  ▼
             ┌──────────┐
             │ Add Tag  │
             │ "churned"│
             └──────────┘
```

## Best Practices

### 1. Start Simple

Begin with 3-5 step sequences before building complex flows:

```
Trigger → Email → Delay → Email → End
```

### 2. Use Tags for State

Track sequence progress with tags:

```
- "onboarding-started" (at trigger)
- "onboarding-complete" (at end)
```

### 3. Set Reasonable Delays

* Welcome emails: immediate to 30 minutes
* Follow-ups: 2-4 days
* Re-engagement: 1-2 weeks

### 4. Always Have Exit Conditions

Check if the goal is achieved before sending more emails:

```
Condition: has_tag "customer"
  ├─ Yes → End sequence
  └─ No  → Continue emails
```

### 5. Personalize with Conditions

Different paths for different subscribers:

```
Branch on plan:
  ├─ Enterprise → High-touch content
  ├─ Pro → Feature-focused content
  └─ Free → Conversion content
```

## Related

<CardGroup cols={2}>
  <Card title="Tags" icon="tags" href="/concepts/tags">
    Use tags to trigger sequences
  </Card>

  <Card title="Events" icon="bolt" href="/concepts/events">
    Trigger sequences with events
  </Card>

  <Card title="Campaigns" icon="paper-plane" href="/concepts/campaigns">
    One-time broadcasts vs sequences
  </Card>

  <Card title="Transactional" icon="code" href="/concepts/transactional-emails">
    Immediate emails via API
  </Card>
</CardGroup>
