Campaigns + DSL
A campaign is a sequence of steps fired when a trigger event happens for a profile. Use the visual journey builder, or author the JSON definition directly via the API.
Visual builder
Open /dashboard/campaigns/[slug]/builder. Click steps to edit, drag to reorder, ✕ to delete, + buttons to extend. Save validates the definition before writing — branch and wait-for-event jump targets are checked at save time so structurally-valid but index-out-of-bounds campaigns can't reach production.
Step types
send— render a template and send to the profile (respects suppression + consent). Required:templateSlug. Optional:variables,from,replyTo.delay— waitmsmilliseconds before the next step.branch— evaluate a predicate against the profile's traits or the triggering event's properties; jump to step indexthenon true,elseon false.wait_for_event— pause until a matching event fires for this profile or thetimeoutMselapses.onTimeoutcan be'continue','exit', or{ jumpTo: number }.exit— terminate the run immediately (typically used as a branch target for "don't send this person").
Variables
Three forms — pick whichever fits the step:
// 1. Literal object
{ "kind": "send", "templateSlug": "welcome", "variables": { "firstName": "Ada" } }
// 2. Read from the recipient's profile traits
{ "kind": "send", "templateSlug": "welcome",
"variables": { "fromProfile": ["firstName", "plan"] } }
// 3. Read from the triggering event's properties
{ "kind": "send", "templateSlug": "receipt",
"variables": { "fromTrigger": ["amount", "currency"] } }Full definition example
{
"trigger": { "eventType": "user.signed_up" },
"steps": [
{ "kind": "send", "templateSlug": "welcome", "variables": { "fromProfile": ["firstName"] } },
{ "kind": "delay", "ms": 172800000 },
{ "kind": "branch",
"predicate": { "source": "profile_traits", "field": "plan", "op": "eq", "value": "paid" },
"then": 4, "else": 2 },
{ "kind": "send", "templateSlug": "free-tier-tip", "variables": {} },
{ "kind": "exit" },
{ "kind": "send", "templateSlug": "paid-tier-tip", "variables": {} }
]
}How it runs
When the trigger event fires for a profile, PAD enqueues a campaign_runs row. The campaign-tick cron (every 5 min) advances runs whose current step is ready. Each delay step sets next_run_at; the cron picks it up on the next tick after the delay elapses.
Per-step analytics
The detail page at /dashboard/campaigns/[slug] groups send analytics by (step_index, template_slug) so multiple sendsteps reusing the same template stay distinct. A "Branch outcomes" panel reads the same aggregation to show how many profiles took each path of every branch step — useful for seeing whether your predicates split the cohort as expected.
Manage actions
- Duplicate — clones the campaign to a draft with a
-copyslug, redirects into the builder. Useful for A/B variants without copy-pasting JSON. - Archive — sets status to
archived; preserves history but stops firing. Archived campaigns are hidden from the list by default. - Delete — hard-delete with cascade to
campaign_runs. Refused onactivecampaigns — pause or archive first. Sends survive the delete becausesends.campaign_run_idis a plain uuid, not a FK.
Idempotency
One run per (campaign, profile). Re-firing the trigger event for a profile already in the campaign is a no-op. To re-enroll, duplicate the campaign to a new slug.