docs/Campaign

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

ParameterTypeDefaultDescription
campaign_namestrUnique campaign name per organization.
origin_phone_numberslist[str]E.164 numbers owned by you. Calls are round-robin distributed across them.
calling_windowslist[dict]Each dict: day (0–6 or name like "monday"), start_time ("HH:MM"), end_time ("HH:MM"). Times are in campaign timezone.
start_datestr"YYYY-MM-DD" — when the campaign begins.
end_datestr | NoneNone"YYYY-MM-DD" — optional end date.
max_concurrencyint1Maximum simultaneous active calls.
max_attemptsint1Maximum call attempts per contact before marking failed.
timezonestr"America/Los_Angeles"IANA timezone for calling windows.
descriptionstr""Campaign description. Required when using Agentic Tenacity.

SDK methods

ParameterTypeDefaultDescription
create_or_update_campaign()functionUpserts a campaign by name. Returns an Campaign object. If the campaign already existed, updates with any provided fields
list_campaigns()functionReturns all campaigns for the authenticated user.
campaign.upload_contacts()methodUploads 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.
campaign.get_status()methodReturns contact status histogram (trying, completed, partially_completed, failed).
campaign.update()methodUpdates mutable campaign fields (concurrency, calling windows, etc.).
campaign.delete()methodSoft-deletes the campaign.

Contact statuses

StatusMeaning
tryingEligible for dispatch
completedCall finished successfully
partially_completedCall connected but not all tasks done
failedPermanent error or max attempts exhausted
do_not_callContact has asked not to be called

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

campaign.py
import os
import guava
from guava import Agent, Field
from guava.campaigns import get_or_create_campaign, Contact

# 1. Create or retrieve a campaign
campaign = get_or_create_campaign(
    "political-poll-q2-2026",
    origin_phone_numbers=[os.environ["GUAVA_AGENT_NUMBER"]],
    calling_windows=[
        {"day": day, "start_time": "09:00", "end_time": "17:00"}
        for day in ["monday", "tuesday", "wednesday", "thursday", "friday"]
    ],
    start_date="2026-04-01",
    max_concurrency=3,
    max_attempts=2,
)

# 2. Upload contacts
campaign.upload_contacts(
    [
        Contact(phone_number="+15551234567", data={"first_name": "Alice", "district": "District 5"}),
        Contact(phone_number="+15559876543", data={"first_name": "Bob", "district": "District 12"}),
    ],
    accepted_terms_of_service=True,
)

# 3. Define call logic
agent = Agent(
    name="Jordan",
    organization="National Opinion Research Center",
    purpose="Conduct a non-partisan political opinion poll.",
)

@agent.on_call_start
def on_call_start(call: guava.Call):
    first_name = call.get_variable("first_name")
    call.reach_person(
        contact_full_name=first_name,
        greeting=(
            f"Hi, is this {first_name}? I'm calling from the National Opinion Research Center "
            "about issues affecting your district. Would you have two minutes to participate?"
        ),
    )

@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str):
    if outcome == "unavailable":
        call.hangup()
    elif outcome == "available":
        first_name = call.get_variable("first_name")
        district = call.get_variable("district")
        call.set_task(
            "political_poll",
            objective=(
                f"Conduct a brief political opinion poll with {first_name} "
                f"in {district}. Be polite and non-partisan."
            ),
            checklist=[
                Field(
                    key="willing_to_participate",
                    description="Whether the respondent agrees to take the poll",
                    field_type="multiple_choice",
                    choices=["yes", "no"],
                ),
                "If they said no, thank them and end the call.",
                Field(
                    key="top_issue",
                    description="Most important issue to the respondent",
                    question=f"What is the most important issue facing {district}?",
                    field_type="multiple_choice",
                    choices=["economy", "healthcare", "education", "housing",
                             "public_safety", "environment", "other"],
                ),
                Field(
                    key="likely_to_vote",
                    description="How likely they are to vote",
                    question="How likely are you to vote in the upcoming election?",
                    field_type="multiple_choice",
                    choices=["very_likely", "likely", "unlikely", "very_unlikely"],
                ),
            ],
        )

@agent.on_task_complete("political_poll")
def on_poll_complete(call: guava.Call):
    call.hangup("Thank them for participating.")

# 4. Serve the campaign (blocks until campaign completes or process is stopped)
agent.attach_campaign(campaign=campaign)
Auth: 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.

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.

read_script() — delivers a scripted message verbatim (no LLM improvisation). Ideal for voicemail messages where you need precise, compliant wording.

Multi-phase tasks. 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.

Graceful exits. Handle every outcome — confirmed identity, unavailable, wrong number, do-not-contact — so no call ends awkwardly or without logging.

voicemail_detection.py
import os
import logging

import guava
from guava import Agent, Field, Say
from guava.campaigns import create_or_update_campaign, Contact

logging.basicConfig(level=logging.INFO)

agent = Agent(
    name="Sarah",
    organization="Valley Health",
    purpose="Remind patients about upcoming appointments",
)


@agent.on_call_start
def on_call_start(call: guava.Call):
    patient_name = call.get_variable("patient_name")
    call.reach_person(
        contact_full_name=patient_name,
        greeting=(
            f"Hello, this is Sarah calling from Valley Health. "
            f"May I please speak with {patient_name}?"
        ),
    )


@agent.on_reach_person
def on_reach_person(call: guava.Call, outcome: str):
    patient_name = call.get_variable("patient_name")
    appointment_date = call.get_variable("appointment_date")
    appointment_time = call.get_variable("appointment_time")
    office_number = call.get_variable("office_number")

    if outcome == "unavailable":
        call.read_script(
            f"Hello, this is a message for {patient_name}. "
            f"This is Sarah calling from Valley Health. "
            f"This is a friendly reminder about your appointment on "
            f"{appointment_date} at {appointment_time}. "
            f"Please call us at {office_number} if you need to make any changes. "
            f"Thank you!"
        )
        call.hangup()
    elif outcome == "available":
        call.set_task(
            "confirm_appointment",
            objective=f"Confirm the appointment with {patient_name}.",
            checklist=[
                Say(
                    f"Great! I'm calling to remind you about your appointment on "
                    f"{appointment_date} at {appointment_time}."
                ),
                Field(
                    key="appointment_response",
                    description="Whether the patient confirms, reschedules, or cancels",
                    field_type="multiple_choice",
                    choices=["confirmed", "reschedule", "cancel"],
                ),
            ],
        )


@agent.on_task_complete("confirm_appointment")
def on_appointment_confirmed(call: guava.Call):
    response = call.get_field("appointment_response")
    logging.info("Appointment response: %s", response)
    call.hangup("Thank them and end the call.")


if __name__ == "__main__":
    campaign = create_or_update_campaign(
        "appointment-reminders",
        origin_phone_numbers=[os.environ["GUAVA_AGENT_NUMBER"]],
        calling_windows=[
            {"day": day, "start_time": "09:00", "end_time": "17:00"}
            for day in ["monday", "tuesday", "wednesday", "thursday", "friday"]
        ],
        start_date="2026-04-01",
        max_concurrency=3,
        max_attempts=2,
    )

    campaign.upload_contacts(
        [
            Contact(
                phone_number="+15551234567",
                data={
                    "patient_name": "Alice Johnson",
                    "appointment_date": "April 22nd",
                    "appointment_time": "2:30 PM",
                    "office_number": "(555) 123-4567",
                },
            ),
        ],
        accepted_terms_of_service=True,
    )

    agent.attach_campaign(campaign=campaign)

Questions? hi@goguava.ai