Skip to content

DND (Do Not Disturb) Contact List ​

Automatically stops calling people who ask not to be contacted, and lets customers manage a per-customer block list so those numbers are never dialed by campaigns or test calls.

Glossary

  • DND: "Do Not Disturb" a phone number that must not be called for a given customer.
  • Contact analysis: the post-call step where an LLM reads the transcript and extracts structured info. (overview)
  • Normalized phone: the digits-only form of a number (e.g. +91 98765-43210 → 919876543210) used for matching so different formats match the same entry.
  • Source: how a DND entry was added analysis (auto), manual (customer), or admin.

Architecture Overview ​

How a contact ends up on the list and how calls are blocked:

How It Works (Step-by-Step) ​

1. Detection (Analysis service, Python)

  • Triggered when a call's analysis runs. In the combined analysis prompt there is a dedicated "TASK 3 Do-Not-Contact Detection" that returns dnd_detection: { wants_no_further_contact, dnd_reason }.
  • File: backend/Analysis/src/pipeline/conversation_analysis.py (_build_dnd_detection_schema, _build_combined_system_prompt).
  • backend/Analysis/src/pipeline/run.py merges the result into the features written to Contact.analysis as wants_no_further_contact (+ dnd_reason).
  • Runs on every analyzed call regardless of the customer's chosen schema. Defaults to false when the AI doesn't run (too few messages / failure).
  • It only flags a clear opt-out ("don't call me", "remove me", "stop calling"). Plain "not interested" is NOT enough.

2. Auto-add (Client backend)

  • Triggered by the Analysis service calling POST /analysis-ready.
  • File: backend/client_backend/src/controllers/webhook.controller.ts (analysisReady). If analysis.wants_no_further_contact === true, it calls dndContactService.autoAddFromAnalysis(...) asynchronously (a DND write failure never fails the hook).
  • Result: a DndContact with source: "analysis", scoped to the contact's customerUID.

3. Manual add / view (Customer)

  • UI: app/src/pages/customer/DndContactsPage.tsx → dndService → POST /dnd-contacts.
  • Service: dndContactService.manualAdd(...) validates + normalizes the phone and creates a source: "manual" entry owned by the customer.

4. Enforcement campaign calls

  • File: backend/client_backend/src/services/calling.service.ts (fillCallSlots). Before dialing each contact it calls dndContactService.isOnDndList(customerUID, phone).
  • On match: contact status → skipped, lastError → "Skipped: contact on DND list", and the loop continues (no retry, no billing, no dispatch).
  • Fail-safe: if the DND lookup throws, the contact is skipped for that cycle rather than risk calling a DND number.

5. Enforcement test calls

  • File: backend/client_backend/src/controllers/agent.controller.ts (testAgent, testAgentFromHeader). Checks DND before creating a Contact.
  • On match: 403 { code: "CONTACT_ON_DND" }. UI surfaces this in the test-call dialog (AgentsPage.tsx).

File Reference ​

FileRoleKey Functions
Analysis/src/pipeline/conversation_analysis.pyAdds DND detection to the analysis LLM call_build_dnd_detection_schema, _build_combined_system_prompt
Analysis/src/pipeline/run.pyMerges detection into Contact.analysisrun_analysis
client_backend/src/models/DndContact.tsMongoose schema + IDndContact
client_backend/src/services/dndContact.service.tsBusiness logic + matchingisOnDndList, autoAddFromAnalysis, manualAdd, list, updateManual, removeManual
client_backend/src/controllers/dndContact.controller.tsAPI layer for customer CRUDlistDndContacts, addDndContact, updateDndContact, deleteDndContact
client_backend/src/routes/dndContact.routes.tsRoutes, gated by dnd_list feature
client_backend/src/controllers/webhook.controller.tsAuto-add on analysis-readyanalysisReady
client_backend/src/services/calling.service.tsCampaign skipfillCallSlots
client_backend/src/controllers/agent.controller.tsTest-call blocktestAgent, testAgentFromHeader
app/src/pages/customer/DndContactsPage.tsxCustomer DND list UI
app/src/services/dnd.service.tsFrontend API clientlist, add, update, remove

Data Model ​

Collection: DndContact. Scoped per customer (customerUID).

Key index: unique compound { customerUID: 1, normalizedPhone: 1 } prevents duplicates per customer and makes auto-add idempotent.

API Endpoints ​

All under verifyJwt + requireFeatureAccess("dnd_list"). All scoped to the calling customer (req.userId).

List DND contacts ​

  • Method: GET Path: /dnd-contacts
  • Query: page (default 1), limit (default 20, max 100), search (phone)
  • Response: { success, items: [...], total, page, limit }. Each item includes server-resolved campaignName, agentName, and canModify (true only for the caller's own manual entries).

Add DND contact (manual) ​

  • Method: POST Path: /dnd-contacts
  • Body: { "phone": "9876543210", "name": "optional", "reason": "optional" }
  • Response: 201 with the created record. 400 for invalid phone, 409 if already on the list.

Edit DND contact (own manual only) ​

  • Method: PATCH Path: /dnd-contacts/:id
  • Body: { "reason": "...", "name": "..." } (phone/source/timestamps are immutable)
  • Response: 200. 403 if the entry isn't a manual entry created by the caller.

Remove DND contact (own manual only) ​

  • Method: DELETE Path: /dnd-contacts/:id
  • Response: 200. 403 if not the caller's own manual entry.

There is intentionally no endpoint to edit/remove analysis or admin entries they are tamper-resistant by design.

Access Control (tiered) ​

The feature is gated by the dnd_list feature string through the standard two-layer model:

  1. Superadmin adds dnd_list to an Organization's plan (shows as "Do Not Disturb List" in OrgManagementPage).
  2. Admin assigns dnd_list to a customer (CustomerManagementPage).
  3. Customer then sees the "Do Not Disturb" nav item and pages.

Superadmin bypasses all checks; Admin passes after the org-plan check. See AUTH-SYSTEM.md.

How It Decides (examples) ​

wants_no_further_contact is set true only for a clear opt-out:

Caller saysDetected?Why
"Please don't call me again."✅ trueExplicit opt-out
"Remove my number from your list."✅ trueExplicit opt-out
"Stop calling me / put me on DND."✅ trueExplicit opt-out
"I'm not interested right now."❌ falseNo request to stop contact
"Call me next week instead."❌ falseWants contact, just later
"Who gave you my number?" (annoyed)❌ falseComplaint, not an opt-out request

How to Test (End-to-End) ​

Prerequisites: dev stack running (Docker), customer has the dnd_list feature, an agent + a verified test number.

  1. Auto-add via call: place a test call, and during the call say "don't call me again". After analysis completes, open Do Not Disturb the number appears with source Auto (call) and a reason.
  2. Skip on campaign: create a 1-contact campaign for that number, run it. The contact shows skipped with reason "On DND list" (and a DND badge in History).
  3. Block on test call: try a test call to that number → blocked with "This contact is on your Do Not Disturb list and cannot be called."
  4. Manual add: click Add Contact, enter a number → appears with source Manual and is editable/removable (only by you).
  5. Verify in DB: db.DndContact.find({ customerUID: "<uid>" }) one document per number, unique on {customerUID, normalizedPhone}.

Troubleshooting ​

SymptomCauseFix
Contact not auto-added after a clear opt-outAnalysis didn't run (too few messages) or intent wasn't explicitConfirm Contact.analysis.wants_no_further_contact; detection only fires on clear opt-outs
Campaign called a DND number anywayNumber format mismatchBoth storage and matching normalize via normalizePhone; confirm the stored normalizedPhone matches the dialed number
Restart picker shows nothing / "Restart 0 selected"DND-skipped contacts have status: skipped + callStatus: nullFixed: campaign.controller.ts includes them in the skipped bucket and restart filter
Test call blocked but should be allowedNumber is on the customer's DND listRemove it (manual entries only) or check why it was auto-added
Invalid feature: "dnd_list" on saveBackend running stale build without the featureEnsure the backend is on a branch/build where dnd_list is in AVAILABLE_FEATURES

Design Notes ("why") ​

  • Per-customer scope: one customer's DND never blocks another. orgUID is stored only for admin visibility.
  • Detection is built-in, not schema-driven: it works even if a customer never added a DND field to their analysis schema, and adds no extra LLM call (folded into the existing combined analysis call).
  • Tamper-resistant: customers cannot remove auto/admin entries, so people who opted out stay protected. Only self-created manual entries are editable/removable.
  • Idempotent auto-add: the unique index + upsert means repeat detections update origin metadata but never duplicate an entry or lose the earliest addedAt.

Last updated: