> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lehar.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaign lifecycle

> How an outbound voice campaign executes: durable orchestration, business-hours scheduling, retries, and pause/resume.

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](/concepts/campaigns).

## 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.

```mermaid theme={null}
flowchart TD
  START["POST /campaigns/{id}/start"] --> SCHED[status: scheduled]
  SCHED --> BOOT["bootstrap: load recipients + policy, status: running"]
  BOOT --> FAN{for each recipient}
  FAN --> RW[Recipient workflow]
  subgraph Attempt loop (up to max_attempts)
    RW --> PLAN["plan attempt: next business-hours slot + retry delay"]
    PLAN --> SLEEP[wait until slot]
    SLEEP --> CREATE["create attempt: place a real outbound session"]
    CREATE --> WAIT{completion signal or call_timeout}
    WAIT --> CHECK["classify outcome from SIP + transcript"]
    CHECK -->|retryable + attempts left| PLAN
    CHECK -->|done / non-retryable / exhausted| DONE[finalize recipient]
  end
  DONE --> JOIN[parent awaits all]
  JOIN --> COMP["complete: status completed / failed"]
```

## Step by step

<Steps>
  <Step title="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`.
  </Step>

  <Step title="Bootstrap">
    Loads the campaign's recipients and execution policy, sets status `running`, and emits `campaign.workflow_bootstrapped`.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="Wait for completion">
    The recipient workflow blocks until the [session-update webhook](/guides/webhooks) signals the call ended, or until `call_timeout_seconds` (default 600, bounds 30–3600). A timed-out active session is marked `expired`.
  </Step>

  <Step title="Classify the outcome">
    The call is classified from its SIP result and transcript:

    | Outcome             | Cause                                                 | Retryable |
    | ------------------- | ----------------------------------------------------- | --------- |
    | `completed`         | Transcript present                                    | —         |
    | `no_answer`         | SIP 408/480 (not answered)                            | Yes       |
    | `busy`              | SIP 486                                               | Yes       |
    | `user_disconnected` | Answered, then the callee hung up before a transcript | Yes       |
    | `declined`          | SIP 603 (rejected)                                    | No        |
    | `failed`            | 5xx, session-create failure, or attempts exhausted    | Yes\*     |

    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`.
  </Step>

  <Step title="Complete">
    Once every recipient is final, the parent completes the campaign — `completed` if there were zero failures, otherwise `failed` — and emits `campaign.completed`.
  </Step>
</Steps>

## 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](/guides/webhooks#campaign-event-log).

## 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`.

<Note>
  `whatsapp_voice` campaigns reuse this exact voice attempt loop for the call-escalation step. See the [WhatsApp guide](/guides/whatsapp).
</Note>
