Playbook 12 · Compliance
Cap contacts across every channel at once
Goal: a customer who is on WhatsApp, SMS, and RCS is still one person, and a "no more than three contacts a week" rule has to count all three together. This playbook enforces a rolling per-customer contact cap in one shared ledger, so the limit holds across channels and across sessions, not per silo, and the cap is policy the deploy validates, not a line in a prompt.
The shape: you pass a stable customer_ref, a hashed phone or a core-banking
id, alongside every message on every channel. The block_contact_frequency
guardrail counts allowed agent responses for that ref in a rolling window, in one SQLite
ledger shared by every agent and channel, and blocks the next contact once the cap is
reached.
Prerequisites #
- A stable identifier per customer that is the same across channels, for example a hash of the verified phone number or the core-banking customer id. Do not use the raw channel handle; those differ per channel.
- Agents already reachable on your channels (see the WhatsApp, SMS, and RCS playbooks).
- Python ≥ 3.11 and
pip install zolva.
Step 1 · Thread a customer reference through every channel #
The cap can only govern traffic it can attribute to a person, so pass
customer_ref with each turn. Include it in the channel payload your gateway
dispatches, or pass it directly to run().
await hub.dispatch(
"whatsapp", "collections-agent",
{"session_id": wa_number, "text": text, "customer_ref": customer_ref},
)
# or, calling the agent directly:
reply = await app.run(
"collections-agent", session_id, user_msg, customer_ref=customer_ref,
)
Use the same customer_ref for that customer on every channel; that
shared value is what makes the count cross-channel rather than per-silo.
Step 2 · One shared cap in policy #
Add block_contact_frequency to a policy every channel-facing agent shares,
and point every agent at the same ledger file. Because the ledger is keyed
on customer_ref, one file counts contacts across all of them.
post:
- block_contact_frequency:
max_contacts: 3
window_hours: 168 # 7 days
ledger: contacts.sqlite # the SAME path for every agent = a shared cap
name: collections-agent
instructions: collections.md
model: { provider: openai, name: gpt-5.6-sol }
tools: [get_dues, get_repayment_options, send_payment_link]
handoffs: [human-escalation]
guardrails: policies/contact.yaml # every channel-facing agent references this
When the fourth contact in the window is attempted, the guardrail blocks it and escalates to a human with the reason attached, rather than sending yet another message. The block is on the audit chain, which is the evidence a regulator asks for.
Step 3 · Add the contact window #
Frequency and hours are separate rules; combine them. block_outside_window
refuses contact outside permitted hours before the model runs, and works alongside the
shared cap.
pre:
- block_outside_window: { hours: "08:00-19:00", tz: Asia/Kolkata } # RBI contact norms
post:
- block_contact_frequency: { max_contacts: 3, window_hours: 168, ledger: contacts.sqlite }
Policies are validated at startup, so a malformed cap fails the deploy, not a live customer conversation.
How the cap counts #
- Keyed on the customer, not the channel: the ledger counts by
customer_ref, so WhatsApp, SMS, and RCS draw down the same allowance. - Rolling window: it counts contacts newer than
window_hoursago, so the allowance refills continuously rather than resetting on a calendar boundary. - Only counts what it can attribute: a turn with no
customer_refis skipped by the cap, so always pass the ref for traffic you intend to govern. - Blocks escalate: a capped contact is not dropped silently; it becomes a human escalation on the audit chain.