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 — wait ms milliseconds before the next step.
  • branch— evaluate a predicate against the profile's traits or the triggering event's properties; jump to step index then on true, else on false.
  • wait_for_event — pause until a matching event fires for this profile or the timeoutMs elapses. onTimeout can 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 -copy slug, 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 on active campaigns — pause or archive first. Sends survive the delete because sends.campaign_run_id is 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.