Automations and background jobs
An automation is a campaign of type AUTOMATION or TRANSACTIONAL with a workflow graph: a trigger, then steps joined by edges. This page explains how a contact moves through it and how the work is scheduled. For the create/update endpoints see Campaigns.
Triggers
| Trigger | Starts when | Notes |
|---|---|---|
EVENT | An event of the configured eventType is logged for a contact | Fires every time the event arrives. Optional filter on an event field. |
SEGMENT_ENTRY | A contact enters the configured segment | A contact already in the segment does not enter it again. includeExisting: true on the trigger enrols current members once, when the automation is activated. |
SEGMENT_EXIT | A contact leaves a dynamic segment | |
SCHEDULED | A cron expression fires | Enrols the members of the configured segment at that moment. |
MANUAL | POST …/campaigns/{id}/enroll or the Enroll panel |
Static segments change through the API, the ingest endpoint, forms and bulk actions. Dynamic segments are re-evaluated for a contact whenever one of their events arrives, and recomputed in full every hour; a contact that newly matches “enters” the segment either way.
Steps
| Step | What it does |
|---|---|
SEND_EMAIL, SEND_PUSH, SEND_TELEGRAM, SEND_SMS, SEND_WHATSAPP | Sends a template through that channel. The template’s language fallback and A/B variants apply. |
WAIT | Pauses for a fixed time. There is no upper limit: the wait is stored on the enrollment and resumed by the scheduler. |
CONDITION | Branches on rules over the contact (conditionMode: "rules", the same rule language as segments, including daysSince.<date field>) or on membership of a segment (conditionMode: "segment"). Edges labelled true/false (shown as Yes/No) pick the branch; a branch with no edge ends the path. |
DECISION | Waits for something to happen, up to a time limit: an event (eventType, optional filter) or a previous step’s outcome such as “opened” or “clicked” (stepOutcome). Takes the true edge as soon as it happens and the false edge when timeoutMs passes. |
AB_SPLIT | Assigns the contact to one of several branches by weight; see A/B tests. |
ADD_TAG, REMOVE_TAG, SET_FIELD, UPDATE_SCORE, ADD_TO_SEGMENT, REMOVE_FROM_SEGMENT, ADD_DNC, REMOVE_DNC, CHANGE_STAGE, UPDATE_COMPANY | Change the contact. |
SEND_WEBHOOK | Calls your URL with the contact. |
SHOW_WIDGET | Shows an on-site widget on the next visit. |
NOTIFY_INBOX, CREATE_TASK, GOTO | Create follow-up work, or jump to another step. |
A common shape for a “did they do it in time?” automation:
EVENT PAYMENT_OPEN → DECISION (wait for PAYMENT_COMPLETED, timeoutMs 1 800 000)
├─ true → (end)
└─ false → SEND_EMAIL "Payment reminder"Enrollment rules
- A contact has at most one enrollment per automation. With
reEntryAllowed: false(the default) a contact who completed it never enters again; withtruethey can re-enter after completing, no sooner thanreEntryCooldownMsafter the previous completion. - A contact whose automation is paused (
automationPausedon the contact) is skipped by every automation. startAt/endAtandentryLimiton the campaign gate new enrollments.- Only an active automation enrols contacts or resumes waiting ones. Pausing it freezes every enrollment where it is; activating it again resumes them.
Pausing and resetting the queue
Pausing a campaign parks every contact where they are: waits do not resume and no new contacts enter. Resume picks them up again from the same step.
While a campaign is paused, Reset queue (next to Resume) removes every contact still in the flow instead. Their enrollment ends as EXITED with the reason Removed by a queue reset, and enrollment work still queued for the campaign is dropped. After you resume, only contacts that hit the trigger from then on enter; removed contacts are not re-enrolled from the trigger’s segment on resume, and come back only if they fire the trigger again and the campaign allows re-entry. The API is GET (counts) and DELETE (reset) on /api/workspaces/{workspaceId}/campaigns/{campaignId}/queue; the reset is refused while the campaign is active.
Marketing policies and transactional sends
Before a message is sent, the sender checks the contact’s consent for the channel, the campaign’s frequency cap and quiet hours, and the workspace frequency cap. Transactional sends (a password reset, a receipt) bypass these checks: they go to anyone with an address, at any hour, regardless of opt-outs. Suppression lists apply to both.
For an email sent from a template, the template’s email type decides: a marketing template is also sent at most once to each contact, while a transactional one can be sent again every time the step runs. The Send Email step shows this type: with a template you change it on the template (it applies to every step that sends it); without a template (inline subject and body) you set it on the step, or leave it on Same as the campaign type. On other channels, a campaign of type TRANSACTIONAL makes the send transactional.
When a marketing email is not sent because of consent, the step is logged as Skipped with the reason under the contact in the campaign’s execution logs, for example Not sent: contact opted out of marketing emails.
How work is scheduled
Nothing in CampaignOS depends on an external scheduler. Work that cannot finish inside a request is written to the Job table and drained by a cron endpoint:
GET /api/cron/tickis called every minute by the platform cron (vercel.json), authenticated withAuthorization: Bearer <CRON_SECRET>. Each tick runs for up to 12 minutes within a function limit of 800 seconds; ticks overlap safely because every job is claimed atomically and holds a 15-minute lease.- Each tick first resumes enrollments whose wait has ended, then takes jobs in order of creation. A job that fails is retried up to 5 times with backoff; a job that outlives its lease is reclaimed.
- Bulk work is split into chunks of 200 contacts: enrolling a segment, a broadcast, a scheduled trigger.
| Job type | Scheduled by | Does |
|---|---|---|
campaign.enroll | Broadcasts, activation with includeExisting | Enrols a chunk of contacts |
campaign.trigger | Segment entry of more than ten contacts | Starts the automation for a chunk |
campaign.scheduled-run | A SCHEDULED trigger firing | Enrols the segment’s members |
campaign.broadcast-summary | The last enroll chunk of a broadcast | Notifies that the broadcast is out |
segment.recompute | Hourly, and after a segment is saved | Recomputes a dynamic segment and starts entry/exit automations for contacts that changed |
score.decay | Daily | Applies score decay rules |
report.run-schedules | Hourly | Runs due report schedules |
task.notify-due | Hourly | Notifies assignees of due tasks |
periodic.hourly, periodic.daily | The tick itself | Enqueue the jobs above |
GET /api/workspaces/{workspaceId}/jobs lists the workspace’s recent jobs with their status, attempts and errors; Settings → Background jobs shows the same, with the time of the last tick. A tick that has not run for several minutes means the cron is not reaching the endpoint or CRON_SECRET is wrong.
Testing an automation without sending
POST /api/workspaces/{workspaceId}/campaigns/{campaignId}/dry-runwith{ "contactId": "…" }walks the graph for that contact and reports the branch every condition would take and every message that would be sent, without sending or enrolling.POST …/templates/{templateId}/send-testsends one template to an address of your choice.