intermediate
journeys
drips
automation

Journey Builder

Multi-step drips that react to ride events - entry triggers, waits, branches, sends, goals, and exits.

Levy Fleets TeamMay 18, 202613 min read

Journey Builder

A journey is a multi-step drip that reacts to events as they happen. While a campaign is "send this message to this segment right now," a journey is "when this thing happens, kick off this sequence."

Navigation

Access journeys from Engage > Journeys.

Node Types

Every journey is a graph of these six node types:

NodePurpose
EntryEvent-based trigger that admits a rider into the journey
WaitPause for a fixed duration or until another event
SendEmail, SMS, or push using a template
BranchPredicate on customer state - splits into if_true / if_false paths
GoalConversion checkpoint that exits the journey on success
ExitTerminal node

Entry Triggers

These are the events Engage can listen for:

EventFires when
customer.createdNew rider signs up
ride.endedA ride completes
ride.force_endedA ride was force-ended (you ended it server-side)
wallet.low_balanceWallet drops below a configured threshold
violation.createdA violation is logged against a rider
subscription.lapsedA subscription transitions to canceled or expired
rider_score.tier_changedRider score tier moves up or down
referral.convertedA referred rider takes their first ride

Each entry trigger accepts an optional filter expression so you can narrow it. Example: customer.created with filter marketing_email_consent = true only admits riders who opted in at signup.

Re-Entry Rules

What happens if the trigger fires twice for the same rider?

RuleBehavior
neverOnce per customer ever (recommended for Welcome Series)
after_cooldownRe-enters after N days (recommended for Win-Back)
every_triggerRe-enters on every matching event (rare - used for Low Balance)

Re-entry is enforced at journey ingest, not at step time.

Sample Journey - Welcome Series

This is the journey that ships as the Welcome Series preset. It is the canonical example.

  ┌────────────┐
  │  ENTRY     │ customer.created (consent = true)
  │  signup    │
  └─────┬──────┘
        │
        ▼
  ┌────────────┐
  │ SEND email │ tpl_welcome_intro
  └─────┬──────┘
        │
        ▼
  ┌────────────┐
  │ WAIT 48h   │
  └─────┬──────┘
        │
        ▼
  ┌────────────┐         ┌───────────────┐
  │ BRANCH     │── true ─►│ GOAL          │
  │ rides ≥ 1  │          │ first_ride    │
  └─────┬──────┘          └───────────────┘
        │ false
        ▼
  ┌────────────┐
  │ SEND email │ tpl_how_to_ride
  └─────┬──────┘
        │
        ▼
  ┌────────────┐
  │ WAIT 72h   │
  └─────┬──────┘
        │
        ▼
  ┌────────────┐
  │ SEND email │ tpl_promo_first_ride
  └─────┬──────┘
        │
        ▼
       EXIT

The underlying JSON definition that Engage stores looks like this:

{
  "entry": {
    "type": "event",
    "event": "customer.created",
    "filters": { "marketing_email_consent": true }
  },
  "reentry": "never",
  "steps": [
    { "id": "s1", "type": "send", "channel": "email", "template_id": "tpl_welcome_intro", "next": "s2" },
    { "id": "s2", "type": "wait", "duration_hours": 48, "next": "s3" },
    {
      "id": "s3",
      "type": "branch",
      "predicate": { "rides.total_count": { "gte": 1 } },
      "if_true": "goal_hit",
      "if_false": "s4"
    },
    { "id": "s4", "type": "send", "channel": "email", "template_id": "tpl_how_to_ride", "next": "s5" },
    { "id": "s5", "type": "wait", "duration_hours": 72, "next": "s6" },
    { "id": "s6", "type": "send", "channel": "email", "template_id": "tpl_promo_first_ride", "next": "exit" },
    { "id": "goal_hit", "type": "goal", "name": "first_ride_taken" },
    { "id": "exit", "type": "exit" }
  ]
}

Built-In Presets

Engage ships with four journey presets you can spin up in one click:

PresetTriggerSteps
Welcome Seriescustomer.created3 sends + 1 branch + 1 goal (see above)
Win-Back 30/60/90Lapsed scheduled scan3 sends with branches that exit on activity
Low Balancewallet.low_balancePush first, email after 4h if no top-up, exit on top-up
BlankChoose your ownJust an entry + exit - you fill in the rest

Editing Journeys

The dashboard shows a read-only SVG canvas plus a JSON editor. You edit by:

  1. Spinning up a preset.
  2. Modifying the JSON to swap templates, change wait durations, or rearrange branches.
  3. Saving (bumps the journey version automatically).
  4. Publishing.

Why a JSON editor?

The first version of Engage uses a read-only canvas to ship faster. A drag-and-drop canvas is a planned upgrade. Today, the JSON editor with syntax highlighting covers every real-world editing need.

Branch Predicates

Branches evaluate against the same attribute surface as segments. Examples:

{ "wallet.current_balance": { "gt": 5 } }
{ "rides.total_count": { "gte": 3 } }
{ "subscription.status": { "eq": "active" } }
{ "rider_score_tier": { "in": ["gold", "platinum"] } }

Operators supported: eq, ne, gt, gte, lt, lte, in, nin, exists.

Wait Nodes

Two kinds:

Wait typeUse
Fixed durationduration_hours: 48 - simple delay
Until next eventPause until a specific event fires (e.g., until: "ride.ended")

If an "until next event" wait is pending and the customer is force-exited (account deletion, RTBF), the wait terminates and the journey records an exit.

Goal Tracking

A goal node has a name and is treated as a successful exit. Goals show up in journey analytics with their own conversion rate.

Common goal patterns:

GoalTriggers
first_ride_takenWelcome Series - exits when the customer takes a ride
wallet_topped_upLow Balance - exits when the customer adds funds
subscription_renewedRenewal Series - exits when they renew
rider_reactivatedWin-Back - exits when they take a ride

Race Conditions and Locks

Multiple workers can advance the same journey simultaneously. Engage uses Postgres row-level locks to keep them safe:

  • Due journey_runs are claimed via engage_claim_due_journey_runs using FOR UPDATE SKIP LOCKED.
  • Two workers calling that RPC at the same moment receive disjoint sets of runs.
  • next_step_due_at is updated atomically on claim so the run does not re-appear in the queue.

You do not configure any of this - it just works.

Stale Data Inside a Journey

A common bug class in other tools: a journey publishes with {{wallet.balance}} rendered at journey-publish time, then 30 days later the actual send uses that stale snapshot.

Engage avoids this completely. Every send re-resolves variables and re-evaluates branches at the moment of send. There is no published snapshot of rider state.

Customer in Multiple Journeys

A single rider can be in any number of journeys simultaneously. The 24-hour dedup across campaigns prevents the same template from firing twice in 24 hours, even if two journeys both target it.

If a rider deletes their account mid-journey, GDPR erasure sets journey_runs.exited_at on every active run and drops PII from engagement_events.

Publishing and Pausing

ActionEffect
SaveBumps version; does not affect already-active runs
Publish (active = true)Journey accepts new entries
Pause (active = false)No new entries; in-flight runs continue until exit
ArchiveNo new entries; in-flight runs are exited

The publish/unpublish action is logged in engage_compliance_audit_log for the audit trail.

Best Practices

  • Always include a branch that exits on success. Otherwise the journey keeps sending to riders who already did the thing.
  • Keep wait durations realistic. A 48-hour delay is gentle; 5-minute delays for "marketing" sends feel spammy.
  • Use goals. The conversion-rate metric is only meaningful when you tell Engage what success looks like.
  • Test with a small segment first. You can wrap an entry trigger with customer.id = "your-test-account-id" to dry-run the journey against yourself before opening the floodgates.

Troubleshooting

Journey is "published" but runs aren't being created

Check the entry trigger and any filters. A trigger like customer.created with filter marketing_email_consent = true only admits new signups who opted in - if your signup flow isn't capturing consent, no one will enter.

Run stuck at a wait node

Check next_step_due_at on the row. If it's in the past, the engage-journey cron is stalled. If it's in the future, the wait is still in effect.

Same template fired twice for the same rider

Check both the 24-hour dedup window and the re-entry rule on the journey. every_trigger re-entry plus an entry event that fires frequently will trigger this - switch to after_cooldown with N days.


Need Help?

For journey help, contact support@levyelectric.com.