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 / conditionStored status
DialStatus = ANSWER (talked)Received
Customer left on hold / connectingAbandoned
Cancel, DONTCALL, INVALIDARGS, causes 1, 21, 22, 27–29Rejected
NOANSWER, BUSY, CONGESTION, CHANUNAVAIL, causes 16–20Not 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 ConnectedAbandoned
Waitretry_seconds (currently 3600) same day while the current pattern bucket is open, else next work_startKeep existing next_retry_at, else retry_seconds (work-hours clamped). No pattern / max_tries
Patternretry_pattern buckets (currently [4,4,3,2,1,1,1,1,1,1,1]); leftover carries to the next working dayIgnored
max_triesDisposes when call_count hits the cap (currently 20)Ignored
call_countIncrements on this settleUnchanged
Auto-claimWhen next_retry_at is null or dueOnly 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:

FlagOutbound auto-dialInboundSocket redialRetry scheduling
0 (default)Any time a ready, matching agent existsAfter-hours rules still applyNever gated by hoursnext_retry_at still clamped to the window
1Only inside the work window (old behavior)After-hours rules still applyNever gated by hoursnext_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:

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.

WhereWhat the agent / supervisor sees
Socket dialeroutbound_blocked and missed_activities: [{ oli_id, due_at }] (overdue only, max 20)
Agent pageBanner 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=
DashboardDialer 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

SettingUsed?Meaning
dialer_typeYesProgressive or Predictive
buffer_timeYesWrap seconds
predictive_ratioYesCustomer legs per idle agent
decline_cooldown_secondsYesDecline block on the same hold
max_linesYesCap ringing + hold + connecting
max_tries / retry_patternYesNot Connected only (live: 20 tries, pattern [4,4,3,2,1,…])
retry_secondsYesNC same-day gap and Abandoned wait (live: 3600)
disposed_priority_idYesFinal Disposal — never auto-claimed
isMaintainanceYes1 pauses new floor calls; live calls stay up
MISSED_ACTIVITY_ENABLED (.env)Yes0 disables the missed-activity outbound block (default 1)
MISSED_ACTIVITY_GRACE_MINUTES (.env)YesMinutes after a bridged call before it blocks outbound (default 10)
hold_timeout_secondsNoUnused leftover
max_abandon_rateNoUnused leftover