import { CodeBlock } from '../views/docs/CodeBlock';
import { Callout, NextLink, Prose, PropTable } from '../views/docs/prose';
import { GUAVADIALER_EX, VOICEMAIL_DETECTION_EX } from './guides-constants';

## Campaign

Guavadialer is the SDK for creating and running high-volume outbound calling campaigns programmatically. Define campaigns, upload contacts, write per-call logic via `Agent` callbacks, and serve campaigns — all from Python.
Note that you need outbound permission approval to use outbound dialing; please contact support@goguava.ai.

### Campaign lifecycle

A campaign is created with `create_or_update_campaign()`, which upserts by name and returns an `Campaign` object. From there you upload contacts, serve the campaign, and monitor status.

### Campaign configuration

<PropTable rows={[
  { name: "campaign_name", type: "str", desc: "Unique campaign name per organization." },
  { name: "origin_phone_numbers", type: "list[str]", desc: "E.164 numbers owned by you. Calls are round-robin distributed across them." },
  { name: "calling_windows", type: "list[dict]", desc: 'Each dict: day (0–6 or name like "monday"), start_time ("HH:MM"), end_time ("HH:MM"). Times are in campaign timezone.' },
  { name: "start_date", type: "str", desc: '"YYYY-MM-DD" — when the campaign begins.' },
  { name: "end_date", type: "str | None", default: "None", desc: '"YYYY-MM-DD" — optional end date.' },
  { name: "max_concurrency", type: "int", default: "1", desc: "Maximum simultaneous active calls." },
  { name: "max_attempts", type: "int", default: "1", desc: "Maximum call attempts per contact before marking failed." },
  { name: "timezone", type: "str", default: '"America/Los_Angeles"', desc: "IANA timezone for calling windows." },
  { name: "description", type: "str", default: '""', desc: "Campaign description. Required when using Agentic Tenacity." },
]} />

### SDK methods

<PropTable rows={[
  { name: "create_or_update_campaign()", type: "function", desc: "Upserts a campaign by name. Returns an Campaign object. If the campaign already existed, updates with any provided fields" },
  { name: "list_campaigns()", type: "function", desc: "Returns all campaigns for the authenticated user." },
  { name: "campaign.upload_contacts()", type: "method", desc: 'Uploads contacts. Each contact is a Contact(phone_number="+1...", data={...}) object. accepted_terms_of_service must be True. Pass outreach_modalities to enable multi-channel outreach (see Agentic Tenacity). allow_duplicates is also available.' },
  { name: "campaign.get_status()", type: "method", desc: "Returns contact status histogram (trying, completed, partially_completed, failed)." },
  { name: "campaign.update()", type: "method", desc: "Updates mutable campaign fields (concurrency, calling windows, etc.)." },
  { name: "campaign.delete()", type: "method", desc: "Soft-deletes the campaign." },
]} />

### Contact statuses

<div className="my-4 overflow-x-auto rounded-lg border border-border">
  <table className="w-full text-sm font-mono">
    <thead>
      <tr className="border-b border-border bg-card/50">
        <th className="text-left px-4 py-3 text-muted-foreground font-semibold">Status</th>
        <th className="text-left px-4 py-3 text-muted-foreground font-semibold">Meaning</th>
      </tr>
    </thead>
    <tbody>
      <tr className="bg-transparent">
        <td className="px-4 py-3 text-primary">trying</td>
        <td className="px-4 py-3 text-muted-foreground">Eligible for dispatch</td>
      </tr>
      <tr className="bg-card/20">
        <td className="px-4 py-3 text-primary">completed</td>
        <td className="px-4 py-3 text-muted-foreground">Call finished successfully</td>
      </tr>
      <tr className="bg-transparent">
        <td className="px-4 py-3 text-primary">partially_completed</td>
        <td className="px-4 py-3 text-muted-foreground">Call connected but not all tasks done</td>
      </tr>
      <tr className="bg-card/20">
        <td className="px-4 py-3 text-primary">failed</td>
        <td className="px-4 py-3 text-muted-foreground">Permanent error or max attempts exhausted</td>
      </tr>
      <tr className="bg-transparent">
        <td className="px-4 py-3 text-primary">do_not_call</td>
        <td className="px-4 py-3 text-muted-foreground">Contact has asked not to be called</td>
      </tr>
    </tbody>
  </table>
</div>

### Agent callbacks

Define an `Agent` and attach callbacks using decorators. Per-call data from the contact's `data` dict is available via `call.get_variable()`. Use `on_call_start` to initiate contact, `on_reach_person` to branch on availability, and `on_task_complete` to wrap up.

### Serving a campaign

Use `agent.attach_campaign(campaign=campaign)` to start processing calls. This blocks and handles calls as they are dispatched by the autodialer. Multiple processes can run in parallel for horizontal scaling.

The autodialer dispatches calls every 2 minutes within configured calling windows. Calls that fail with retryable SIP errors are automatically rescheduled up to `max_attempts`.

`attach_campaign` polls `v1/campaigns/{id}/has-callable-contacts` every 5 seconds. When no callable contacts remain and no active calls exist, the socket closes cleanly and `attach_campaign` returns with a log: `INFO: Campaign '<name>' has no more callable contacts and no active calls. Closing.` (Added in v0.21; previously `attach_campaign` blocked indefinitely until the process was killed.)

### Complete example

<CodeBlock code={GUAVADIALER_EX} filename="campaign.py" language="python" />

<Callout><span className="text-primary font-semibold">Auth:</span> All SDK calls require the `GUAVA_API_KEY` env var set to a valid API key. The base URL defaults to production; override with `GUAVA_BASE_URL`.</Callout>

### Voicemail detection and handling

Not every outbound call reaches a live person, and that's fine. Rather than using `reach_person()` (which simply succeeds or fails), you can build custom routing logic to handle voicemail, wrong numbers, and do-not-contact requests, turning every call outcome into a useful action.

The pattern: use a `Field` with `field_type="multiple_choice"` to classify who answered — the target contact, someone else, voicemail, or a wrong number. Then route to the appropriate handler based on the result.

<Prose><span className="text-foreground">read_script()</span> — delivers a scripted message verbatim (no LLM improvisation). Ideal for voicemail messages where you need precise, compliant wording.</Prose>

<Prose><span className="text-foreground">Multi-phase tasks.</span> Call `set_task()` again from `on_complete` to chain tasks. The first task identifies who answered; the second task handles the actual conversation or voicemail.</Prose>

<Prose><span className="text-foreground">Graceful exits.</span> Handle every outcome — confirmed identity, unavailable, wrong number, do-not-contact — so no call ends awkwardly or without logging.</Prose>

<CodeBlock code={VOICEMAIL_DETECTION_EX} filename="voicemail_detection.py" language="python" />

<NextLink section="agentic-tenacity" label="Agentic Tenacity" />
