A series is a multi-step automation. Each step is one of:
Step type
What it does
Email
Send an email
SMS
Send an SMS message (in-builder; SMS sending is currently not wired in production cron)
In-app
Surface an in-app message
Wait
Pause for N hours/days/weeks
Condition
Branch based on a condition (open, click, property, event)
End
Mark the user as done
Steps run in step_order — the recipient advances row by row. The visual graph in the builder is for editing and “Day N” labels; runtime execution follows the linear step order.Use series for welcome flows, re-engagement, onboarding, drip campaigns, and lifecycle sequences.
Pinned to top — defines how users enter (manual / event / segment match / user created)
Email
Subject + body with merge tags
SMS
SMS content (builder only, sending not wired)
Wait/Delay
Pause N hours / days / weeks
Condition
Branch on email opened, link clicked, user property, or event fired
End
Marks the journey complete
The builder shows “Day N” labels on email and SMS nodes computed by walking the graph from the trigger. This is purely visual — runtime advances by step_order regardless of edges drawn.
Keep step order and graph order consistent. The runtime advances strictly by step_order — if your drawn edges and the underlying step list disagree, the order in the step list wins.
Each email step can store variants. The cron splits recipients round-robin across the base + variants and tags each delivery with metadata.variant_id so you can track performance.Use variants to test subject lines, opening copy, or CTAs against each other.
Conditions branch the flow based on what users do. Available condition types:
Type
What it checks
email_opened
Did the user open a previous email step?
link_clicked
Did the user click a link in a previous email?
user_property
Does a user property match key=value?
event_fired
Has a specific event been tracked?
When a condition is met, the user advances. When it isn’t met, the user is skipped with reason: "condition_not_met" and stops at that step. There’s no separate “No” branch in the engine — failed users are removed from the journey at that point.
Halo respects each recipient’s end_users.timezone when set; otherwise falls back to the series’ default timezone.When a send falls outside the window or on a weekend (with business days only), it’s deferred to the next valid time, not dropped.
Set max_sends_per_hour on a series to throttle sending. Halo uses Redis to track a rolling-hour cap and defers sends that would exceed it.Use this when you have a large audience and want to spread sending to avoid spikes.
Once a user enrolls, they can’t enroll again, even after completing
reenroll_enabled on
Users can re-enroll after completing
reenroll_delay_days
Optional delay before re-enrollment is allowed
When re-enrolling, prior rows are deleted to satisfy uniqueness; aggregate stats stay counter-based.If a user has an active pending or processing row, new enrollment is skipped (no reset, no duplicate).
When a series uses a subscription audience or has an unsubscribed exit rule, recipients can unsubscribe from the linked list mid-series and exit cleanly. See Subscription Lists.