intermediate
campaigns
composer
email

Campaign Builder

Compose one-time and recurring email, SMS, and push campaigns - schedule, preview, dry-run, and send.

Levy Fleets TeamMay 18, 202611 min read

Campaign Builder

A campaign is a single send (or a recurring digest) to an audience segment. The composer lets you choose channel, audience, template, schedule, and optional A/B variants.

Navigation

Access campaigns from Engage > Campaigns.

Campaign Types

TypeWhen to use
One-timeA promo blast, an announcement, a one-off win-back
RecurringA daily or weekly digest (e.g., "your weekly ride summary")
Journey stepSent as part of a Journey, not directly

Recurring campaigns evaluate the audience segment fresh on every send, so a "Lapsed 30d" recurring weekly digest will see different riders each week.

Channels

ChannelProviderDefaults
EmailPostmark (Levy domain or your custom)List-Unsubscribe headers set, legal address required
SMSTelnyx primary, Twilio failoverQuiet hours 9 PM - 9 AM recipient-local; STOP keyword enforced
PushExpo (APNs + FCM)Token validation, permanent-failure handling
Multi-channelCascadingTry push first, fall back to email if no token

Composing a Campaign

Step 1 - Basics

  1. Click New Campaign.
  2. Set the name (used internally only).
  3. Choose the channel.
  4. Choose the audience segment.

Step 2 - Template

Assign one of your library templates. The composer shows a side-by-side preview with mobile and desktop toggles. Variables resolve against the first matching customer in your segment so you see real names, not placeholders.

If you have not built templates yet, clone a Levy Recommended template - see Pre-Built Templates.

Step 3 - Variables and UTM Tags

Variables use double-braces and resolve at send time:

Hey {{customer.first_name | default:"there"}},

You have {{wallet.balance | currency}} in your wallet
and your last ride was {{ride.last_at | date:"MMM D"}}.

Supported filters:

FilterExampleOutput
date:"FORMAT"{{ride.last_at | date:"MMM D"}}Apr 12
currency{{wallet.balance | currency}}$4.50
uppercase{{customer.first_name | uppercase}}MARIA
lowercase{{customer.first_name | lowercase}}maria
default:"X"{{customer.first_name | default:"there"}}there if null
truncate:N{{ride.notes | truncate:50}}First 50 chars + ...

Default UTM tags from Settings > Defaults are appended to every link. Override per campaign in the Tracking tab.

Step 4 - Schedule

ScheduleBehavior
ImmediateQueued the moment you hit Send
Scheduled atQueued at a specific timestamp
RecurringDaily, weekly, or monthly

If your segment has per-customer timezones on customers.timezone, you can opt into Send in recipient timezone - the dispatcher staggers the queue to deliver at the target local time for each rider.

Step 5 - A/B Variants (Optional)

You can attach 2-4 variants per campaign. The Bayesian winner picker auto-promotes a winner at 95% confidence after at least 500 sends per variant. See A/B Testing.

Step 6 - Dry Run

Click Dry Run before any large send. The dispatcher walks the full pipeline without calling the provider and reports:

  • How many sends would happen
  • How many would be suppressed (with reasons)
  • How many would be rate-limited (queued for later batches)
  • Which variables resolved cleanly, and which fell back to defaults

A dry run consumes no provider quota.

Step 7 - Send

  1. Click Send.
  2. The campaign status moves from draft to queued to sending to sent.
  3. Engagement events stream into the funnel view as they arrive from providers.

Idempotency and Retries

Engage prevents duplicate sends with multiple safeguards:

  • campaign_sends.idempotency_key is UNIQUE. A retry insert collides at the constraint and is dropped.
  • campaign_versions is UNIQUE on (campaign_id, variant_id). Replaying a send never duplicates the snapshot.
  • 24h dedup across campaigns. The same customer cannot receive the same template twice in 24 hours, even if two different campaigns target them.

If you accidentally hit Send twice, the second call is a no-op.

Suppression Behavior

Before any send, the dispatcher checks the suppression list:

Suppression sourceEffect
Per-subaccount opt-outSkipped, logged as suppressed_subaccount
Global opt-out (hard bounce / STOP / complaint)Skipped, logged as suppressed_global
Missing channel consentSkipped, logged as no_consent
Quiet hours (SMS only)Held in queue until 9 AM local

See Suppression List for managing entries.

Recurring Campaigns

Recurring sends re-evaluate the segment every cycle. To stop a recurring campaign:

  1. Open the campaign.
  2. Click Pause (stops the next cycle but preserves history) or End (terminal).

Duplicating a Campaign

Click Duplicate on any campaign to clone it. The duplicate inherits everything (audience, template, schedule, variants) but starts in draft status so you can edit before sending.

Analytics

Every campaign has a built-in analytics page. See Engagement Analytics.

The headline metrics:

  • Delivered / bounced / complained
  • Open rate (email/push only)
  • Click rate (email/SMS only)
  • Conversion (custom goal hit within attribution window)
  • Per-variant breakdown when A/B is on
  • Per-language breakdown for multilingual templates

Best Practices

  • Always dry-run before a large send. It is free and catches misnamed variables.
  • Use UTM tags. Without them you cannot prove which campaigns drove rides or top-ups.
  • Schedule for the rider, not for you. A 10 AM ET send is 7 AM PT - which is fine, until you have West Coast riders. Use recipient-timezone sending whenever your audience spans timezones.
  • Cap your blast cadence. More than 2 emails or 1 SMS per week per rider tends to spike unsubscribes. Engage will not stop you, but the suppression rate will tell you when you have crossed the line.

Troubleshooting

Campaign stuck in queued

Check engage-dispatch cron status under Engage > Settings > Diagnostics. If the last run was more than 5 minutes ago, the cron is paused or failing - contact support.

High suppression count on a dry run

Often this is missing consent flags. Check the segment definition - does it include marketing_email_consent = true?

Variables show as {{customer.first_name}} literally

The template was not saved with the variable wrapper. Re-open the template, ensure it has double-braces, save, and re-dry-run.

A/B variant traffic split looks wrong

Variant assignment is deterministic per recipient (seeded by customer ID), so the actual split converges to your target only at high N. Below ~200 sends, expect noise.


Need Help?

For campaign help, contact support@levyelectric.com.