See also: Runs REST API reference for request/response shapes, status codes, and error semantics.
Overview
A campaign defines a recurring batch execution configuration. Each campaign targets a Decision Flow and a customer Segment, with schedule settings, frequency caps, and file output configuration. When a campaign runs, it creates a campaign run — an individual execution instance that processes every customer in the segment through the Decision Flow.Campaign vs Campaign Run
How Campaigns Work
1
Create campaign
Select a Decision Flow, target segment, schedule, frequency caps, and file output config.
2
Configure schedule
Choose manual, daily, weekly (pick day), or monthly (pick day or last working day). Set time and timezone.
3
Set frequency caps
Optionally limit max recommendations per run, per offer, or per channel.
4
Configure file output
In the campaign’s File Output section, choose a destination (download-only, or an S3 connector), the format (CSV/TSV/JSON/JSONL), and build the columns — each column reads a built-in decision field or a segment-schema column, with an optional transform chain. A live preview shows one sample row.
5
Run
Click “Run Now” for manual execution, or activate the campaign for scheduled runs.
6
Review run history
Each execution appears as a numbered run with full results, offer breakdown, and file outputs.
Schedule Configuration
Scheduled campaigns actually fire. A
GET /api/v1/cron/campaign-scheduler route (bearer-authed with CRON_SECRET, same as the rest of the cron tier) scans every status: "active" campaign whose scheduleType is not manual, evaluates daily/weekly/monthly as an RRULE and custom via scheduleCron, and triggers a campaign run the same way POST /api/v1/runs/:id/campaign-runs does when a schedule is due. It runs on a 5-minute cadence in the in-process maintenance scheduler (or your own external scheduler, on self-hosted deployments) and is self-healing: if a tick is missed (deploy, restart, scheduler downtime), the next tick still fires as long as the schedule’s most recent occurrence is after the campaign’s lastRunAt — no exact-minute alignment is required. draft, paused, and archived campaigns are never auto-fired.Frequency Caps
File Output Configuration
File output is configured on the campaign (Run.fileConfig), not the channel. Every file-mode (and manual) channel the campaign targets writes a file using this one shape — so two campaigns that share a channel can still produce different files. Edit it in the File Output section of the full-window campaign editor (/runs/new or /runs/[id]/edit).
File output moved off the Channel and onto the campaign in the
Run.fileConfig v2 redesign. A Channel’s deliveryMode: "file" now just means “this channel produces a batch file”; its columns, format, and destination come from the campaign. Any fileConfig still stored on a Channel row is ignored by the batch executor.Run.fileConfig v2 fields:
The engine derives whether to upload: when
destinationConnectorId is set and resolves to a connector for the tenant, the file is uploaded; otherwise it degrades to a local downloadable artifact. connectorId is still accepted as a legacy alias for destinationConnectorId.
Run.fileConfig is validated on campaign create/update (validateRunFileConfig → RunFileConfigSchema): format must be one of the values above, each columns[] entry needs both name and source, and each transform op must be a known op. The check is permissive/additive — an omitted, empty, or legacy fileConfig is always accepted; only a genuinely malformed one is rejected.
Column Sources
Each entry incolumns[] maps an output column name to a source — a namespaced string resolved per row at file-generation time (src/lib/delivery/file-format.ts). A source that doesn’t resolve for a given customer (no such attribute, no contact address found, etc.) outputs an empty string — it never fails the row. In the editor, a column is either a built-in field (a fixed decision field, chosen from a dropdown) or a schema column (a column of the campaign segment’s entity schema, which maps to attribute.<column>).
offerName, creativeName, and channelName (no dot) are still accepted as column sources for backward compatibility, but new campaigns should use the namespaced offer.name / creative.name / channel.name forms above. personalization.<key> is intentionally not offered — batch runs don’t populate it (they run their own qualification/scoring pipeline, not the Recommend API’s Enrich/Compute stages).Transforms
Each column can carry an optionaltransforms chain — operations applied left-to-right to the resolved source value before it’s written. Semantics live in applyTransforms (src/lib/delivery/file-format.ts), shared verbatim with the editor’s live preview.
prefix and suffix read the transform’s value field; the other ops ignore it. A missing/empty source is never fabricated — transforms only run when the value is present, so a blank source stays blank rather than becoming a bare prefix.
Formula-injection guard (CSV/TSV)
Outbound files are delivered to third parties and routinely opened in Excel or Google Sheets, which interpret any cell beginning with=, +, -, @, a tab, or a carriage return as a formula. Because customer-derived column values (raw attribute.<column> fields, contact addresses) are untrusted, the CSV and TSV serializers neutralize any such cell by prefixing it with a single apostrophe (') so the spreadsheet treats it as text — the standard OWASP “CSV injection” mitigation (neutralizeFormula in src/lib/delivery/file-format.ts).
The guard is number-aware: a cell that is a well-formed number — including a negative value (-500), a signed value (+3), scientific notation (-1.2e-3), or an E.164 phone (+15551234567) — is left untouched, so numeric data round-trips intact. Only genuinely formula-shaped strings are prefixed. JSON/JSONL output is not guarded (it isn’t spreadsheet-interpreted, and a ' prefix would corrupt the value).
Ingesting responses
Outbound files carry decisions to a vendor; the vendor’s responses (opens, clicks, conversions) come back through a Data pipeline, not this config. Build an ingestion pipeline with an outcomes node under Data → Pipelines to write those responses into interaction history. The campaign editor links to it from the File Output section.Retrieving Generated Files
fileOutputs[].filePath in the run summary is a real, retrievable path:
- When the campaign’s
destinationConnectorIdresolves and the S3 upload succeeds,filePathis ans3://bucket/keyreference. - Otherwise (download-only, or the S3 upload failed), the file’s content is persisted server-side and
filePathis/api/v1/runs/artifacts/{id}—GETthat path (tenant-scoped, same auth as the rest of the API) to download the file with the correctContent-TypeandContent-Disposition.
Locally-persisted artifacts have no retention/cleanup job yet — they accumulate in the
batch_file_artifacts table indefinitely. For very large or frequent runs, set the campaign’s destinationConnectorId to an aws_s3 connector.Batch Channels
Only channels with file, integration, or manual delivery mode are included in batch campaigns. API (real-time) channels are excluded because batch runs produce file-based output. When creating a campaign, you can optionally select specific batch channels viachannelIds — if the list is empty (the default), every active batch-compatible channel in the tenant is included.
Campaign Status
Campaign Run Status
Scoring and Ranking in Batch Runs
A batch run resolves the target Decision Flow’s runnable config (latest published snapshot, falling back to the draft) and looks for ascore node. When that node is configured with method: "formula" and a strategyProfileId, the batch executor loads that RankingProfile’s weights and ranks every candidate with them — the same composite scoring (computeArbitratedScore) /recommend uses, instead of the flat priority × weight × fitMult ordering batch runs used previously. When the flow’s Score node weights clv, the executor looks up each customer’s CustomerCLV.clvScore and folds it into the composite score; a customer with no CLV row has the clv term dropped from their score entirely rather than diluting it with a zero.
Scope of the fix. Only the Score node’s base
strategyProfileId is applied in batch — strategyOverrides (per-productType/category/channel profile switching, which operates per-candidate in realtime) are not yet reproduced in the batch loop. A flow that relies purely on its base strategy profile now ranks correctly in batch; a flow that depends on strategyOverrides still falls back to priority-based ranking for batch runs, same as before this fix. If the profile fails to load (deleted, wrong tenant), the run logs a warning and falls back to priority-based ranking rather than failing.Run Results
After a campaign run completes, results are available at three levels:Overall Statistics
Summary Breakdown
offerBreakdown and channelBreakdown are arrays of {name, count} — not a flat {name: count} object.Integration-mode delivery is gated behind a per-tenant opt-in:
TenantSettings.liveDelivery defaults to false. While it is off, integration channels are never transmitted and every such item counts toward simulatedCount instead of deliveredCount. See Channels -> Delivery Modes for the full behavior.Per-Customer Results
Each customer’s result shows the selected offers with scores:Offer IDs in the summary and per-customer results are automatically resolved to human-readable names in the API response.
API Reference
Create Campaign
File output is configured on the campaign (
Run.fileConfig v2) and executeBatchRun reads it for every file-mode channel in the run — see File Output Configuration above. Any fileConfig still stored on a Channel row is ignored by the batch path.Update Campaign
Trigger Run
pending (201). The trigger is idempotent under concurrency: it reserves the run under a FOR UPDATE lock on the campaign, so two racing triggers (a double-clicked Run Now, a client retry, or a manual trigger coinciding with a scheduled tick) can’t each start a full-segment execution and double-contact every customer. If a run for the campaign is already pending or running, that in-flight run is returned with 200 instead of a second being started. A trigger issued after the previous run reaches a terminal status starts a fresh run with the next run number.
List Campaign Runs
Get Campaign Detail
List Campaigns
UI Walkthrough
1
Navigate to Campaigns
Go to Campaigns in the sidebar (was “Runs”).
2
Create campaign
Click + New Campaign. Configure the decision flow, segment, schedule, frequency caps, batch channels, and file output settings.
3
Trigger a run
Select a campaign and click Run Now to trigger manual execution.
4
Monitor run history
Expand individual runs in the history to see stats, offer breakdown, and file outputs.
Next Steps
Dashboards
Monitor campaign performance and batch results.
Decision Flows
Configure the decisioning pipeline that campaigns execute.