Engagement Branching
A Branch step has two authoring modes:
| Mode | Question it answers | Example |
|---|---|---|
On a rule (condition) | "What is already true about the person?" | tier == "pro" → Pro path |
On engagement (event_response) | "What did they do with the previous Send?" | Opened the welcome email → thank-you path; no action in 3 days → reminder push |
Comparing two letters is not a Branch. Use an Experiment step. The old Adaptive Branch (experiment mode) is retired. See Adaptive Branches (retired).
This page covers On engagement.
When to use it
Drop a Branch step right after a Send and switch the mode to On engagement. Apex auto-fills paths based on the upstream Send's channel:
- email → Opened, Clicked, Bounced, Complained
- push → Opened, Dismissed
- in-app → Opened, Dismissed
- sms → Delivered, Replied
You don't pick event names. You pick where each path goes. The deadline is one slider (default 3 days); the "no action in window" path is always wired separately.
flowchart TD
Trigger[Trigger: user_signed_up]
Send["Send: Welcome email"]
Branch["Branch on engagement<br/>window: 3d"]
Opened[Send: thanks-for-engaging]
Clicked[Exit: converted]
NoAction[Send: reminder push]
Bounced[Exit: bad-address]
EndNode[Exit]
Trigger --> Send --> Branch
Branch -->|Opened| Opened --> EndNode
Branch -->|Clicked| Clicked
Branch -->|"No action in 3d"| NoAction --> EndNode
Branch -->|Bounced| Bounced
How it differs from a Wait step
A Wait until_event step is binary (event-fired vs deadline-elapsed) and listens for ONE event. The operator has to chain three Wait steps to handle Opened / Clicked / No-action separately.
An Engagement Branch is multi-path in a single step, scoped to the upstream Send's messageId, and dedupes Apple Mail Privacy bot-fires from real opens. It also has a publish-time validator that rejects:
- a Branch in
event_responsemode with no resolvable upstream Send - channel-mismatched paths (e.g. a
clickedpath on a push-only Send) - a deadline shorter than
1 hour
Reliability of each path
Some engagement signals are noisier than others:
- Opened — Apple Mail Privacy Protection prefetches every email image from Apple's proxy, so iOS Mail users fire an "opened" event whether they read the email or not. Apex flags MPP-prefetch opens (
openMppFlagged: trueon the COMM record) and skips the journey-resume side-effect for them, but the open count itself is recorded for analytics. Use Clicked as the primary signal whenever possible. - Clicked — Reliable. Apex signs each tracking link with an HMAC and binds it to a specific journey-run, so captured links can't replay against a different recipient.
- Bounced — Reliable, originates from SES SNS webhooks.
- Complained — Reliable, same source. Hard-fails the recipient via the suppression list.
If your Engagement Branch's only enabled path is Opened, the editor warns you. The journey will still publish, but the recommendation is to add Clicked or use a downstream conversion event for routing.
Engagement events are NOT a journey goal
You can't set an engagement event (apex.email.opened, apex.email.clicked, etc.) as a journey's goalEventName. The publish-time validator rejects it. Reason: engagement signal is conditional on receipt + display. Recipients who never received the comm cannot have engaged with it, so the metric isn't comparable across exposed and holdout cohorts. Using it as a goal would corrupt belief updates.
If you want to learn from a journey, use a downstream conversion event (signup, purchase, plan_upgraded) as the goal. The Engagement Branch is for routing, not measurement.
Privacy disclosure for merchants
Email open routing has privacy implications:
- Apple Mail Privacy Protection synthesizes every "open" event for iOS Mail users. Behavioural decisions made on opens for iOS Mail audiences are functionally random.
- GDPR / ePrivacy require disclosure when behavioural decisions are made on engagement events. If your Engagement Branch routes recipients down materially different paths based on whether they opened the email, your privacy notice should disclose that.
Apex never exposes IP / User-Agent metadata into the journey routing decision; the only signal we use is "did the engagement event fire."
Imports from Pinpoint
Pinpoint's Yes/No splits with EventCondition map directly to Engagement Branches. The Pinpoint translator auto-inserts a PT1H Wait step when your Pinpoint export placed a Yes/No event-split immediately after a Send without an explicit evaluationWaitTime — Pinpoint applies that 1-hour wait silently, and we surface it as an explicit step so the imported journey behaves the same way you intended.
See the Pinpoint migration guide.
Runtime guarantees
- Cross-journey isolation. A user in two journeys both waiting for
apex.email.openedcannot have one journey's open resolve the other's wait. The wait row is keyed by the upstream Send'smessageId, which differs across journey-runs. - Once-only routing. When the first matching engagement event fires, the runtime resumes that path AND deletes sibling rows for the same Branch step. A late-arriving Click after an Opened path already resolved cannot re-fire the wait.
- Mixed-pipeline safety. Journeys containing an Engagement Branch are excluded from the belief graph. Engagement-routed paths self-select on the upstream Send's success — the post-engagement converters aren't comparable to the no-action converters, so we abstain from belief updates rather than corrupt the graph with misattributed lift.