Shared behaviour
Settle, retry, wrap, pause, decline, redial, missed activities.
These rules are the same in Progressive and Predictive. Hangup writes master + sales_dialer_call_attempts (snapshot of master data_id at settle, for outgoing / incoming / redial). Only Not Connected increments call_count and uses retry_pattern + max_tries. Abandoned keeps an existing next_retry_at, or schedules retry_seconds when null — it is not on the NC retry cycle.
Hangup map
| AMI / condition | Stored status |
|---|---|
DialStatus = ANSWER (talked) | Received |
| Customer left on hold / connecting | Abandoned |
Cancel, DONTCALL, INVALIDARGS, causes 1, 21, 22, 27–29 | Rejected |
NOANSWER, BUSY, CONGESTION, CHANUNAVAIL, causes 16–20 | Not Connected |
Settle only updates rows still Calling. Agent, duration, hold seconds, and data_id are on the attempt row, not master. Attempt dial_status is this call’s outcome; previous_dial_status is master at claim.
Retry
| Not Connected | Abandoned | |
|---|---|---|
| Wait | retry_seconds (currently 3600) same day while the current pattern bucket is open, else next work_start | Keep existing next_retry_at, else retry_seconds (work-hours clamped). No pattern / max_tries |
| Pattern | retry_pattern buckets (currently [4,4,3,2,1,1,1,1,1,1,1]); leftover carries to the next working day | Ignored |
max_tries | Disposes when call_count hits the cap (currently 20) | Ignored |
call_count | Increments on this settle | Unchanged |
| Auto-claim | When next_retry_at is null or due | Only when next_retry_at is set and due |
Claim order is status tier first (Callback → Pending → Abandoned → Not Connected). Within Callback / Pending / Abandoned: priority_no, then oldest attempt. Within Not Connected: sooner next_retry_at, then Fresh Assign first, then lowest call_count. Abandoned is not on the NC pattern. Dispose exclusion uses only this module’s sales_dialer_config (sales-dialer). Live knobs: retry_pattern = [4,4,3,2,1,1,1,1,1,1,1], retry_seconds = 3600, max_tries = 20, work_days = [1,2,3,4,5,6,7], window 09:30–18:30.
Claim only reserves the row (Calling, isReserved = 1, reserved_at). first_attempt_at / last_attempt_at are written right before a real outbound originate, or on an inbound claim. A claim released without dialing restores the previous status and waits RELEASE_RETRY_MS (default 60s; invalid numbers keep retry_seconds). Predictive claims only leads that one idle agent can take (skill and priority on the same agent). /api/health reports metrics.released, metrics.releasedNoAgent, leads.phantomAttempts (target 0) and leads.uncoveredLeads.
Wrap and working hours
buffer_time is per-agent wrap after a connected call. A miss with no lead retries claim every 10 seconds. Originate is refused when isMaintainance = 1. Auto-dial also needs an idle ready agent whose skills match the lead form_type.
sales_dialer_config.outbound_obey_work_hours (default 0) controls whether outbound auto-dial (progressive and predictive) follows work_start–work_end on work_days:
| Flag | Outbound auto-dial | Inbound | Socket redial | Retry scheduling |
|---|---|---|---|---|
0 (default) | Any time a ready, matching agent exists | After-hours rules still apply | Never gated by hours | next_retry_at still clamped to the window |
1 | Only inside the work window (old behavior) | After-hours rules still apply | Never gated by hours | next_retry_at still clamped to the window |
Because retry scheduling stays clamped, after hours only leads whose next_retry_at is already due (or null) are claimed. A Not Connected at 18:45 still waits for the next work_start. Turn the old behavior back on with UPDATE sales_dialer_config SET outbound_obey_work_hours = 1 WHERE module_id = 'sales-dialer';
Pause and decline
Panel pause writes sales_dialer_employee.panel_status = pause, queue-pauses the extension, and opens a row on sales_dialer_agent_pauses (source agent or auto on leave). Agent pause requires { reason } from public GET /api/pause-reasons. Pick ondemand for an on-demand OLI call. other requires a short note (12 words). Invalid or missing reason → server_error invalid_pause_reason and no pause. Ready closes that row and needs no reason. status / panel include pause_reason, pause_reason_note, and paused_at while paused. The dashboard Work column shows a live elapsed clock from that start time.
Daily day reset at PAUSE_DAY_RESET_AT (default 09:30 IST / DB_TIMEZONE) closes open pauses that started before today (resume_reason = day_reset) and sets panel_status = yet_to_login. The dialer ignores those desks until the agent goes Active. Same-morning pauses stay open. Kill switch: PAUSE_DAY_RESET_ENABLED=0.
SIP deny on a live predictive/inbound hold records sales_dialer_agent_declines and blocks that extension for the same caller for decline_cooldown_seconds (default 30). The customer stays on hold. The same or another skilled agent is offered when the window ends.
Maintenance
sales_dialer_config.isMaintainance (column spelling as stored) is the floor switch. 0 / null is fail-open (calls run). 1 stops new predictive/progressive originates, socket redial, and inbound park/offer (PSTN is left to FreePBX). Live talks are not hung. Dashboard and agent show a MAINTENANCE banner; the CRM sales-dialer page shows a "Maintenance mode" banner. Reloaded with the rest of config every 30 seconds.
Redial
Socket redial with oli_id from a joined agent. Skips wrap and queue. Skill must match (not_owned otherwise). Blocked in maintenance (error: maintenance). While the agent is outbound-blocked by missed activities, only their own missed OLIs can be redialed (error: missed_activity otherwise). ondemand: true allows redial from pause if SIP is idle (and unpauses on success). Reopens any active master row, including Received / Rejected / Disposed. Auto-dial attempts stay outgoing; inbound stays incoming; this attempt (and a decline on that predictive offer) is call_type = redial on sales_dialer_call_attempts / sales_dialer_agent_declines. Live dialer and call_connected also send that attempt call_type so the agent Type slot can show Outgoing / Incoming / Redial. status.call_type is still engine mode. A redial that ends Not Connected does not increment master call_count (pattern / Final Disposal stay based on auto-dial NC only). Payload is in the Socket guide.
Missed activities
Every bridged outgoing or redial call (customer and agent both answered) opens a row in sales_dialer_missed_activities for that agent and OLI. Incoming calls are not tracked. Settle stamps ended_at and due_at = ended_at + MISSED_ACTIVITY_GRACE_MINUTES (default 10).
A 15 second refresh collects uncleared rows past due_at. An agent with any such row is outbound blocked:
- Predictive and progressive stop offering or originating outbound to that desk (
missed_activity). - Socket
redial(including on-demand) fails witherror: missed_activity, unless the OLI is one of that agent's own missed leads, so they can call the customer back. - Inbound offers are unaffected. Pause / Active work as before.
The block clears when the CRM stores a pending callback or pending disposition for the OLI. That clears every open row for the OLI, whoever submits, and the agent is re-evaluated immediately. Rows that never got a hangup (crash or lost AMI event) are closed by the stale sweep after 120 minutes when the lead is not on a call, then follow the same grace rule.
| Where | What the agent / supervisor sees |
|---|---|
Socket dialer | outbound_blocked and missed_activities: [{ oli_id, due_at }] (overdue only, max 20) |
| Agent page | Banner with the OLIs; status line "Outbound disabled — complete missed activities" |
| CRM sales-dialer page | "Outbound paused" banner (on demand disabled) and a compact Missed activities panel beside the lead card: name, due countdown / Overdue, OLI, with Call (redial) and Details (load the card to dispose or schedule). Data from GET /api/missed-activities?extension= |
| Dashboard | Dialer column shows Outbound blocked (hover lists OLIs). Outgoing blocked panel under Waiting idle lists blocked agents with time since the oldest missed lead went overdue. Blocked agents are not counted as waiting idle |
Config: MISSED_ACTIVITY_ENABLED=0 turns tracking and blocking off; MISSED_ACTIVITY_GRACE_MINUTES sets the grace window. Table from migration 042_missed_activities.sql.
Priorities
Only three rows in sales_dialer_priority: 1 Callback Schedule (is_callback = 1), 2 Fresh Assign, 3 Final Disposal (disposed_priority_id, never claimed, rejected on push). Disposed leads only come back through socket redial, a new CRM push, or a CRM pending callback. A CRM pending disposition also sets Final Disposal with disposeRemark = CRM: <reason>.
Config that actually moves the needle
| Setting | Used? | Meaning |
|---|---|---|
dialer_type | Yes | Progressive or Predictive |
buffer_time | Yes | Wrap seconds |
predictive_ratio | Yes | Customer legs per idle agent |
decline_cooldown_seconds | Yes | Decline block on the same hold |
max_lines | Yes | Cap ringing + hold + connecting |
max_tries / retry_pattern | Yes | Not Connected only (live: 20 tries, pattern [4,4,3,2,1,…]) |
retry_seconds | Yes | NC same-day gap and Abandoned wait (live: 3600) |
disposed_priority_id | Yes | Final Disposal — never auto-claimed |
isMaintainance | Yes | 1 pauses new floor calls; live calls stay up |
MISSED_ACTIVITY_ENABLED (.env) | Yes | 0 disables the missed-activity outbound block (default 1) |
MISSED_ACTIVITY_GRACE_MINUTES (.env) | Yes | Minutes after a bridged call before it blocks outbound (default 10) |
hold_timeout_seconds | No | Unused leftover |
max_abandon_rate | No | Unused leftover |