Skip to main content
This page is the canonical reference for everything inside the Series Rules side panel (open it from any series detail page). It has three tabs: Entry, Goal, and Exit. Each section below maps one-to-one to a tab and a setting key on automations.settings.

Entry tab

The Entry tab answers two questions in order:
  1. Trigger: when does enrollment happen?
  2. Audience: among the contacts the trigger fires for, who is allowed in?
Plus an Options block for re-enrollment and company priority.

Triggers

Every series has an entry trigger stored in settings.entry_trigger. The four options in the dashboard map to these values:
In-app labelentry_trigger valueWhen it fires
Manual activationmanualYou activate the series; everyone in the audience is enqueued at once
User signs upuser_createdEach new contact, the moment Halo first inserts the row
Triggers an eventeventWhen a named event (event_type) fires from the SDK or API
Matches the audience filtersegment_matchWhen a contact starts matching the audience filter set above

Manual activation

The simplest trigger. When you click Activate:
  1. Halo evaluates the audience filters
  2. Every matching contact is added to the queue with scheduled_for based on the first email’s send window + delay
  3. Contacts who match later (e.g. via a segment update) are not added; manual is one-shot at activation
Use this for one-time campaigns where you want explicit control. When a manual series has no pending or processing recipients, it auto-completes (status flips to completed).

User signs up

Halo enrolls every newly-identified contact automatically. Useful for welcome sequences:
  • New signup → “Welcome” email
    • 1 day → “Get started” email
    • 3 days → “Have you tried [feature]?”
This trigger fires on Halo’s row creation, specifically the first time any of these happens for a given external ID:
  • A first SDK identify() call
  • The chat-widget lead form captures a new email
  • A teammate adds the contact from the dashboard
It is not keyed off the signed_up_at trait. You do not need to send signed_up_at for “User signs up” series to work, and the trigger fires once per new row regardless.
Welcome series with imported users. If you bulk-import users via CSV, the Series API, or an integration sync, every imported contact is “newly created” from Halo’s point of view. Either keep the welcome series in paused until your import completes, or use an event trigger keyed on a real product signup event.

Triggers an event

Enroll when a specific event type fires. Configure:
  • Event name (settings.entry_event_name): the event_type to listen for, e.g. trial_started, integration_connected, plan_upgraded
Events come from:
  • The SDK’s automatic activity tracking
  • Manual POST /api/sdk/events calls from your backend
  • Some integrations (e.g. Stripe webhook events can map to internal event types)
This is the most flexible trigger. Anything you can track as an event can start a series. The activate route returns 400 if you pick this trigger without setting an event name.

Matches the audience filter

Enroll when a contact newly matches the audience filter set in the Audience section below. Requires:
  • Audience source set to Filter by criteria (audience_type: "segment")
  • At least one filter
Example: contact’s plan changes from free to pro → enroll in a “Welcome to Pro” series. Segment matches are evaluated:
  • On every SDK identify() call (when traits or company traits change)
  • By periodic background re-evaluation
When you activate a segment_match series, existing matches enroll immediately. After that, new matches enroll on their next SDK identify. The “On activate” card in the side panel shows the current match count for this reason. The activate route returns 400 if entry_trigger is segment_match and audience source isn’t “Filter by criteria”.

Audience

The Audience section narrows down who the trigger applies to. Two dropdowns drive everything:

Contact type (settings.audience_contact_type)

Filters the underlying end_users rows by contact_type:
In-app labelValueBehavior
Users and leads (default)bothEvery contact, whether they have an external ID or not
Users onlyuserOnly contacts where contact_type = 'user'
Leads onlyleadOnly contacts captured as leads (no external ID, lead form / sales agent / etc.)
This filter applies on every trigger path: the activate route honors it for manual sends, and the enrollment runtime honors it for user_created / event / segment_match so a lead can’t slip into a “Users only” series.

Source (audience_type on the automation row)

Picks how the audience is defined:
In-app labelaudience_typeWhat it means
All contacts with emailallEvery contact at the org with a non-null email
Filter by criteriasegmentBuild a filter set inline (or pick a saved segment)
Subscription listsubscriptionSend only to contacts subscribed to a specific list, with consent rules. See Subscription Lists.

Quick filters

When source is All contacts with email or Filter by criteria, the side panel shows one-click presets that seed the filter set:
PresetFilter applied
On trialstripe:subscription_status is trialing
Active subscribersstripe:subscription_status is active
Past duestripe:subscription_status is past_due
Set to cancelrenewal_status is set_to_cancel
From SDKsource is sdk
Signed up in 30 dayssigned_up_at after <today minus 30 days>
Inactive 30 days+last_seen before <today minus 30 days>
Clicking a preset switches Source to Filter by criteria if it isn’t already, and appends the preset filter into the first AND group. Repeat clicks are idempotent.
The Signed up in 30 days preset reads the signed_up_at column directly. Contacts who do not have signed_up_at set are excluded. There is no fallback to created_at. If you want this preset to behave correctly across imports, make sure your identify() calls send signed_up_at. See User Traits → How automations and segments read signed_up_at.

Filter by criteria

When source is Filter by criteria, the audience builder is a list of AND/OR filter groups. Each filter is { field, operator, value }. The full list of fields is defined in getAllAudienceFields() and combines:
  • All user fields (identity, lifecycle, revenue, activity, renewal, Stripe)
  • All company fields, prefixed with company.
  • All lead fields, prefixed with lead:
  • Any custom fields discovered on end_users.custom_fields and companies.custom_fields
Operators by field type:
Field typeAvailable operators
stringis, is_not, contains
numberis, gt, lt, gte, lte
booleanis
datebefore, after
Date operators expect ISO 8601 (YYYY-MM-DD is fine for date-only comparisons; full timestamps work too). The recipient count under “On activate” recomputes whenever the filter set changes. Click View who will enroll / View who currently matches to inspect the actual contacts. If the count stays at 0 but you expect matches, see Troubleshooting. A common case is filtering on Role without passing role in identify().

Options

Allow re-enrollment

SettingBehavior
settings.reenroll_enabled = false (default)Once a contact enrolls, they can’t enroll again, even after completing or exiting
settings.reenroll_enabled = trueContacts can re-enroll after completing or exiting
settings.reenroll_delay_daysOptional minimum days between completing the series and re-entering. Skips re-enrollment when Date.now() - latest_enrollment.created_at < reenroll_delay_days * 86400000.
When re-enrolling, prior automation_recipients rows are deleted to satisfy the (step_id, end_user_id) unique index. Aggregate stats stay counter-based. If a contact has an active pending or processing recipient row, new enrollment is skipped (no reset, no duplicate) regardless of reenroll_enabled.

Company priority (settings.company_priority)

When enabled, at most one contact per company is enrolled. If another seat from the same company already has an active recipient row, this contact is skipped. Contacts without a company are unaffected. Use this to avoid emailing the same company multiple times from a series like “Renewal coming up” or “Trial ending soon”.

Goal tab

The Goal tab is purely for measurement. Toggling it on does not change who gets enrolled.
SettingTypeDefaultPurpose
settings.goal_enabledbooleanfalseMaster toggle. Goal tracking only runs when this is true.
settings.goal_filtersfilter set[]Match conditions that count as a conversion
settings.goal_window_daysnumber30How long after enrollment a recipient is eligible to convert
A recipient counts as “converted” when their traits, custom fields, Stripe data, or events start matching goal_filters within goal_window_days of their enrollment timestamp. Common goals:
  • Subscription activated (stripe:subscription_status is active)
  • Plan upgraded (plan is_not free)
  • Ticket resolved (custom event filter)
Adding goal_enabled = true without any goal filters is flagged in the side panel. No conversions will be tracked because there’s nothing to match.

Exit tab

Exit rules remove recipients from the series before they reach the final step. Multiple rules can be active at once; any one match is enough to exit. They live on settings.exit_rules as an array of strings.
In-app labelValueWhen it fires
Completes the seriescompletedRecipient finishes the last step. Always implicitly active; cannot be removed.
UnsubscribesunsubscribedRecipient unsubscribes from the linked subscription list (only meaningful when audience source is Subscription list)
Replies to an emailrepliedRecipient replies to any email step. Their reply lands in the inbox as a normal ticket.
No longer matches the audience filtersegment_leaveRecipient stops matching the audience filter. Only meaningful when audience source is Filter by criteria with at least one filter.
Triggers an eventeventA specific event fires for the recipient. Configure with settings.exit_event_name.
Matches an exit filterfilter_matchRecipient starts matching settings.exit_filters (independent from the audience filter). Multiple groups for OR logic.
Manual removal onlymanualLets a teammate explicitly remove a recipient from the recipient list in the dashboard. Always available; this rule just makes the action explicit in the config.
Exit rules are evaluated:
  • At enrollment time (filter_match only): if the contact already matches the exit filter, they aren’t enrolled at all
  • On every cron tick the recipient is processed: for replied, event, segment_leave, filter_match
  • On the unsubscribe webhook: for unsubscribed
Recipients exited mid-flight are stamped with the matching exit_reason on automation_recipients. Recipients who reach the final step are stamped exit_reason: "completed" automatically.

Conditions (mid-series branches)

Conditions are different from exit rules. They live as steps inside the series (added in the Manual Builder canvas), not as series-level settings. They check something about the recipient’s history at the moment that step runs.
TypeWhat it checksRequired condition_value
email_openedDid the recipient open a previous email step?(none; checks the most recent prior email)
link_clickedDid they click any link in a previous email?(none)
user_propertyDoes a user trait, custom field, or Stripe field match an expression?A key=value expression like plan=pro, mrr>=100, status!=churned, or email contains @acme.com
event_firedHas a specific event fired since the recipient reached this step?The event name

user_property operators

OperatorMeaning
=string equality
!=string inequality
>, >=, <, <=numeric comparison (returns false if either side isn’t numeric)
containscase-insensitive substring (note the spaces around the keyword)
Behavior on missing values:
  • =, >, <, >=, <=, and contains evaluate to false when the field is unset
  • != evaluates to true when the field is unset and the expected value is non-empty

event_fired scoping

event_fired matches events that fired at or after the recipient reached this condition step (we use the recipient row’s created_at as the lower bound). This means the pattern “trial_started → 3-day wait → paid?” only passes for users who paid during the wait, not anyone who paid in their account history.

Condition evaluation outcome

When a condition step is reached, Halo checks the condition:
ResultBehavior
TrueStatus completed, recipient advances to next step
FalseStatus skipped with reason: "condition_not_met", recipient does not advance
There is no second branch for the false path. Conditions are gates, not splits; failed users exit the series at that step. The canvas only exposes a single output handle to make this behavior explicit. To create true branching, use multiple parallel series triggered by different segments.

Activation blockers

When you click Activate, the activate route runs validation and returns a 400 with a specific error message if anything is wrong. The side panel pre-flags these in the “Cannot activate yet” amber card so you can fix them up front. Order matches the activate route’s own validation.
ConditionError
audience_type = "subscription" and no subscription_id”Pick a subscription list under Audience before activating.”
audience_type = "segment" and audience_filters is empty”Audience source is ‘Filter by criteria’ but no filter is set.”
entry_trigger = "segment_match" and audience_type !== "segment"“‘Matches the audience filter’ trigger needs Audience source set to ‘Filter by criteria’.”
entry_trigger = "event" and no entry_event_name”Set an event name under Trigger before activating.”
entry_trigger = "manual" and 0 recipients post-suppression”No contacts match this audience right now.”
Marketing series without a verified custom domain”A verified custom domain is required to send broadcasts and email series.”
Marketing series without a company footer”Marketing emails require a company footer.”
user_created / event / segment_match triggers are forward-only. Activating one of them with 0 currently-eligible contacts is allowed; the series sits ready to enroll new matches as they arrive. (segment_match is the exception: existing matches enroll immediately at activation time, then ongoing matches enroll on identify.)

Combining triggers and conditions

A common pattern:
  1. Trigger: event with entry_event_name = trial_started
  2. Step 1: “Welcome to your trial” email
  3. Step 2: Wait 3 days
  4. Step 3: Condition event_fired with condition_value = paid_subscription_started
    • True → next step (e.g. “Welcome to the Pro plan”)
    • False → recipient exits with condition_not_met
The combination of trigger + audience + waits + conditions + content + exit rules + goal is what makes a series flexible.

Where to go next

Series

Build the steps that triggers and conditions live within.

Subscription Lists

Consent management for the unsubscribed exit rule.

User Traits

Reference for the trait fields available in audience filters and conditions.

Troubleshooting

Why matched count is 0 and how to verify filters against real data.

Company Traits

Reference for the company-prefixed fields in audience filters.