Skip to main content
This is the deep dive on how a voice campaign actually runs after you call start. Campaigns are orchestrated by the Campaign Engine — a durable workflow engine — so a run survives worker restarts, respects calling windows, and retries without double-dialing. For the API surface and states, see the Campaigns concept.

Orchestration model

A run is a parent workflow that fans out one child per recipient:
  • Campaign workflow (parent) — bootstraps the run, starts one child per recipient, waits for all of them, then completes.
  • Recipient workflow (child) — retries attempts for a single recipient until success or exhaustion.
  • Activities — the side effects (planning a slot, creating a call, classifying the result) that run on the campaign worker.

Step by step

1

Start

POST /campaigns/{id}/start starts the workflow, persists its temporal_workflow_id / temporal_run_id, and sets status scheduled. If the engine isn’t configured, you get 503.
2

Bootstrap

Loads the campaign’s recipients and execution policy, sets status running, and emits campaign.workflow_bootstrapped.
3

Plan the attempt

Computes the next allowed call time from the campaign timezone + business hours (default Mon–Fri 09:00–18:00), applying the retry delay for attempts after the first. The recipient goes scheduled.
4

Create the attempt

Records an attempt row (in_progress), marks the recipient calling, and places a real outbound voice session — minting a short-lived agent token, dispatching the agent, and dialing over SIP. On failure the attempt and recipient go failed.
5

Wait for completion

The recipient workflow blocks until the session-update webhook signals the call ended, or until call_timeout_seconds (default 600, bounds 30–3600). A timed-out active session is marked expired.
6

Classify the outcome

The call is classified from its SIP result and transcript:Retryable outcomes loop back to plan with a business-hours-aware delay, bounded by max_attempts (1–10). no_answer on the final attempt becomes failed.
7

Complete

Once every recipient is final, the parent completes the campaign — completed if there were zero failures, otherwise failed — and emits campaign.completed.

Pause & resume

POST /campaigns/{id}/pause signals the workflow to stop starting new attempts (in-flight calls finish); status goes paused. POST /campaigns/{id}/start on a paused campaign resumes it. Deleting a campaign terminates the workflow.

Reliability & idempotency

  • At-least-once safe. Attempt creation is de-duplicated so a redelivered signal or retried activity can’t place a second call for the same attempt.
  • Durable. Workflow state is persisted; a worker restart resumes mid-run.
  • Bounded. max_attempts (1–10), call_timeout_seconds (30–3600), retry_delay_minutes (1–10080), and max_recipients_per_campaign (default 10,000, max 50,000) cap the blast radius of any run.
  • Auditable. Every transition is written to the campaign event log — see Reading events.

Running again

A finished campaign is terminal and read-only — re-running in place is disabled so a previous run’s recipients and results can’t be destroyed by accident. To run it again, clone it (POST /campaigns/{id}/clone): the clone copies the config into a fresh draft (no recipients), records its source in cloned_from_campaign_id, and leaves the original untouched. Publish the clone, upload a recipient list, then start.
whatsapp_voice campaigns reuse this exact voice attempt loop for the call-escalation step. See the WhatsApp guide.