Campaign Events and Hooks
Automate campaigns around your own systems. The recipient lifecycle, the Campaign Pre-Call hook (skip a recipient or replace its context before every attempt; fails closed; one active per business), Campaign Call Completed and Campaign Recipient Unreachable events with their args, webhooks, setting the success result, and recipes for common campaign setups.
Campaigns connect to your systems through event handlers (Python that runs on an event) and webhooks (an HTTP request to your server). This page shows when each campaign event fires and how to combine them.
A recipient's lifecycle
added -> waiting -> Pre-Call hook (before every attempt)
|
+-- skip -------------> Skipped (never called)
|
+-- call -> connected? --yes-> conversation -> Call Completed
|
no (no answer, busy, failed)
|
+-- attempts left -> wait 5 min, try again
+-- 3 attempts used -> Recipient Unreachable| Event | Event type | When it fires | Conversation | Webhook |
|---|---|---|---|---|
| Campaign Pre-Call | campaign_pre_call | Before every attempt, retries and test calls included. The call waits for the handler's answer. | No | No |
| Campaign Call Completed | campaign_call_completed | Once per recipient who was reached, after the call ends and its transcript is saved. | Yes | Yes |
| Campaign Recipient Unreachable | campaign_recipient_unreachable | Once per recipient who couldn't be reached, when the call won't be tried again. | No | Yes |
All three are handled per business, not per campaign: a handler runs for every campaign of the businesses in its scope. To act on one campaign only, check args["campaign"]["id"] or ["name"] in your code.
What handlers receive
Each event passes the campaign and recipient in context["args"]["campaign"]:
| Field | Value |
|---|---|
id, name | The campaign |
recipient["phone"] | The number called |
recipient["name"] | The recipient's name, or None |
recipient["context"] | The recipient's context as stored now, including any Pre-Call replacement |
recipient["metadata"] | Your metadata, never shown to the AI |
recipient["isTest"] | True for test recipients |
context["args"]["event_type"] names the event. context["conversation"] is None when there is no conversation, so guard before using it.
Pre-Call
Campaign Pre-Call runs right before each attempt. Use it to re-check whether the call should still happen, and to give the AI fresh facts. Its return value decides:
| Return value | What happens |
|---|---|
None or {} | The call goes ahead as is. |
{"skip": True, "reason": "Order already paid"} | The recipient is Skipped for good and never called. The reason shows in the results, up to 500 characters. |
{"context": "..."} | Replaces the recipient's context, then calls. Up to 2,000 characters. The new context is saved, so later attempts and other events see it. |
def handle_event(context):
campaign = context["args"]["campaign"]
recipient = campaign["recipient"]
if campaign["name"] != "Unpaid orders":
return None
order = get_order(recipient["metadata"]["orderId"]) # your own lookup
if order["paid"]:
return {"skip": True, "reason": "Order already paid"}
return {"context": f"Order {order['number']} for {order['total']}, unpaid since {order['date']}."}It fails closed. If the handler raises an error, returns anything else, or more than one active Pre-Call handler covers the business, the recipient isn't called. It's tried again 5 minutes later, without using one of its attempts. The campaign visibly stalls instead of calling people it shouldn't. To call anyway when your own lookup fails, catch the error in your code and return None.
One active Pre-Call handler per business. Creating or activating a second one that covers the same business is refused. Put the logic for all campaigns in one handler and branch on the campaign.
Keep it fast. The call is placed only after the handler returns.
Call Completed
Campaign Call Completed fires once a reached recipient's call has ended and the transcript is in the conversation. context["conversation"] has the full transcript, so this is the place to:
-
Decide whether the call achieved its goal and set the success result:
def handle_event(context): messages = context["conversation"]["messages"] agreed = llm_classify_binary( "Did the customer agree to complete their order?", messages, fallback=False, ) set_data_collection_result(context, "success", "true" if agreed else "false") -
Write the outcome back to your CRM, using the IDs in
recipient["metadata"].
A call that connected counts as reached, whatever was said. It isn't called again.
Recipient Unreachable
Campaign Recipient Unreachable fires once when a recipient's call won't be tried again. context["args"]["reason"] says why:
| Reason | Meaning |
|---|---|
no-answer | The last attempt wasn't answered |
busy | The line was busy |
failed | The call couldn't be connected |
Use it to fall back to another channel, such as an email, or to flag the contact in your CRM. To find out why calls fail, see Call failures.
Other events on campaign calls
Campaign calls are conversations, so conversation events fire for them too. For example, Conversation Closed fires after a call that wasn't handed off to your team.
Webhooks
Campaign Call Completed and Campaign Recipient Unreachable are available as webhooks. The payload's args holds campaign, and reason for unreachable recipients. conversation is null when there is none. Campaign Pre-Call isn't available as a webhook, because the call needs its answer.
Recipes
- Call a list once: A one-shot campaign with a CSV upload. Add a Call Completed handler to set
success. - Call people as events happen in your store: A continuous campaign. A recurring job finds new cases, such as abandoned carts, and adds them with
add_campaign_recipientsand adedupeKeyper case. Or your system calls the REST endpoint. - Don't call people who no longer need it: A Pre-Call handler that checks your system and returns
skip, for example when the cart was bought in the meantime. - Respect your own do-not-call list: A Pre-Call handler that looks the number up and skips it.
- Give the AI up-to-date facts: A Pre-Call handler that returns fresh
context, such as the order status and today's date. - Keep your CRM in sync: A Call Completed handler or webhook for reached recipients, and Recipient Unreachable for the rest.
- Follow up when a call didn't reach someone: A Recipient Unreachable handler that sends an email or creates a task.
Creating handlers
In the dashboard, open Settings -> Automation -> Event Handlers and pick the campaign event. With Copilot or MCP, use create_event_handler with eventType set to campaign_pre_call, campaign_call_completed or campaign_recipient_unreachable, and test_event_handler to try it. See event handlers for writing and testing handlers.