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.

No external provider. This is a guardrail and a customer reference you already have; it composes with any channel playbook. There are no third-party calls to verify.

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().

in your gateway's dispatch
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.

policies/contact.yaml
post:
  - block_contact_frequency:
      max_contacts: 3
      window_hours: 168        # 7 days
      ledger: contacts.sqlite  # the SAME path for every agent = a shared cap
agents/collections.yaml
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.

policies/contact.yaml
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_hours ago, so the allowance refills continuously rather than resetting on a calendar boundary.
  • Only counts what it can attribute: a turn with no customer_ref is 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.

← All playbooks