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.

Running a fleet on this hardware?

Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.