Predictive engine
Customers ring first. Agents join only on answer.
Same sales_dialer_master_data and sales_dialer_priority pool as Progressive. Originate is AMI for both engines. Lead ingest lives in the API reference.
End-to-end flow
A 1 second pacer counts idle skilled agents on this module’s sales_dialer_employee roster, claims skill- and priority-matched leads from the master table, and originates the customer on a Local channel. Skills come from sales_dialer_employee.skills (JSON list matched to lead form_type); priority_ids (empty = all) gate lead priority_id. Master has no agent pin. AMI ignores non-roster extensions. The agent panel stays on waiting until a customer answers and is bridged. PSTN inbound on a desk blocks outbound for that agent. An agent with an overdue missed activity (outbound_blocked) is not offered outbound customers, except the redial leg of their own missed OLI; inbound still reaches them.
- Ready idle skilled agents (panel + presence + SIP + wrap clear)
- Pace: serve hold first, then idle × ratio (at most ceil(ratio) per agent), capped by max_lines
- Claim a skill- and priority-matched row from master (no employee pin)
- Originate Local/{0number}@from-internal with MusicOnHold
- No answer → Not Connected. Answer → pick longest-waiting skilled idle agent
- Agent free → AMI Bridge. Else hold until a skilled agent is free or the customer hangs up (Abandoned)
- Decline keeps the caller on hold; that extension is blocked for decline_cooldown_seconds
- Talk → record attempt. Hangup → settle. Agent wrap buffer_time
Master has no agent_id. Connected agent is written on sales_dialer_call_attempts at settle, never on the lead row.
Pacing
Each tick first offers customers already on hold to a free matching agent, before anyone new is rung. Then target = min(max(idle, floor(idle × predictive_ratio)), max_lines), and new legs are target − served, where served is ringing + on-hold customers that at least one idle agent could take (skill and priority on the same agent). Customers on hold that no idle agent can take do not reduce it. The total is still capped at max_lines − (ringing + hold + connecting). Ratio is at least 1.
Each idle agent has ceil(ratio) slots. A placed customer uses one slot on the least-loaded matching agent, but does not lock that agent. At answer the slot agent is preferred if still ready and still matching (skill, priority, decline cooldown); otherwise any matching idle agent takes the call, or the customer waits on hold. Claims only include agents with a free slot, so a small skill group cannot be over-dialed. Examples: 1 agent at 2.5 places 2 calls; 2 agents at 2.5 place 5 (3 + 2); ratio 1 places one per agent. Socket redial still pins to the requesting agent.
There is no abandon-rate brake. After a connected call the agent is not idle until buffer_time wrap finishes. Dashboard Customer Waiting is answered customers still on MusicOnHold or mid agent-offer (predictive outbound plus inbound waiting). Dialing is only while the customer phone is still ringing. See also Inbound and Settle & retry.
Claim order
Same SQL as Progressive: FOR UPDATE SKIP LOCKED, then Calling and reserved. Connected agent is written on sales_dialer_call_attempts at settle, not on master. call_count does not increment on claim — only when settle maps to Not Connected.
Callback (due), then Pending, then Abandoned, then Not Connected last.
Callback / Pending / Abandoned: lowest
priority_no. Not Connected: sooner next_retry_at, then Fresh Assign, then lowest call_count.Never attempted first, then oldest attempt, then oldest push, then lowest id.
Received, Rejected, Calling, and reserved rows are skipped by the pacer. Not Connected waits on next_retry_at (retry_pattern + working hours). Abandoned waits on next_retry_at (settle uses retry_seconds when that field was empty — not the NC pattern / max_tries). Claim refuses originate when isMaintainance = 1, or outside the work window only when sales_dialer_config.outbound_obey_work_hours = 1 (default 0; see Wrap and working hours). Auto-dial also requires an idle ready agent whose skills match form_type and whose priority_ids allow the lead’s priority_id — the claim SQL checks both on the same agent (one (skills AND priorities) clause per idle agent, OR-ed), so a lead no single agent can take is never claimed. Claim only reserves the row (reserved_at); first_attempt_at / last_attempt_at are stamped right before the customer originate. A claim released without dialing retries after RELEASE_RETRY_MS (default 60s; invalid numbers keep retry_seconds). /api/health reports metrics.releasedNoAgent, leads.phantomAttempts and leads.uncoveredLeads. priority_id 1 (Callback Schedule) waits on callback_scheduled_at. Time-gated callbacks are documented in the Callback Request guide. Socket redial can reopen a settled row and stores call_type = redial — see Settle & retry.
AMI originate
Customer originate is Local/{0number}@from-internal/n with Application: MusicOnHold and OLI_ID. AMI_OUTBOUND_CHANNEL (for example PJSIP/9001) identifies the trunk in AMI events; it is not the Originate channel. 10-digit numbers get a leading 0.
When a customer answers, the engine originates PJSIP/{extension} with Application: Bridge to the longest-waiting idle agent whose skills include form_type and whose priority_ids allow the lead’s priority_id (empty allowlist = all). The agent's SIP phone shows the shared AMI_CALLERID placeholder, not the customer number.
Hold until agent
If nobody matching the skill is free, or the agent is already on another predictive call, the extra customer stays on MusicOnHold. Only one bridge originate is sent to an agent at a time. If that agent declines (SIP deny), the customer stays on hold. That extension is written to sales_dialer_agent_declines and blocked for this call for decline_cooldown_seconds (default 30). When the window ends, the engine offers again. Hold is unlimited — there is no hold timeout. The customer is settled Abandoned only if they hang up while waiting or connecting.
Hangup results
| Hangup | Stored status |
|---|---|
| Answered and talked | Received |
| Answered, customer left before an agent talked | Abandoned |
| Cancel by customer, invalid destination | Rejected |
| No answer, busy, congestion, chanunavail | Not Connected |
Not Connected uses sequential retry_pattern buckets (currently [4,4,3,2,1,1,1,1,1,1,1]) until max_tries (20). Leftover in the current bucket carries to the next working day; finishing a bucket waits until the next work_start. Same-day gap is retry_seconds (3600); after work_end snaps to next morning. call_count increments only on Not Connected. Abandoned is not on that cycle — it only uses retry_seconds when next_retry_at was empty. Received, Rejected, and Disposed are not claimed again (unless redial or a new CRM push).
Engine config
| Setting | Where | Meaning |
|---|---|---|
dialer_type | sales_dialer_config | Set to Predictive |
buffer_time | sales_dialer_config | Wrap after a connected call |
predictive_ratio | sales_dialer_config | Customer legs per idle agent (default 1.20). Total floor(idle × ratio), at most ceil(ratio) per agent |
decline_cooldown_seconds | sales_dialer_config | Seconds a declined agent stays blocked for this call (default 30) |
max_lines | sales_dialer_config | Cap on ringing + hold + connecting customer legs |
max_tries | sales_dialer_config | Hard cap on Not Connected tries (currently 20) |
retry_seconds | sales_dialer_config | Same-day gap after Not Connected (currently 3600); Abandoned uses this when next_retry_at was empty |
retry_pattern | sales_dialer_config | Sequential buckets; leftover carries (currently [4,4,3,2,1,1,1,1,1,1,1]) |
work_start / work_end | sales_dialer_config | Retry window (currently 09:30–18:30 IST); also the originate window when outbound_obey_work_hours = 1 |
work_days | sales_dialer_config | ISO weekdays (currently [1,2,3,4,5,6,7]) |
outbound_obey_work_hours | sales_dialer_config | 1 = auto-dial only inside the work window. Default 0. Inbound and redial are unaffected |
isMaintainance | sales_dialer_config | 1 stops new originates and redial |
MISSED_ACTIVITY_ENABLED / MISSED_ACTIVITY_GRACE_MINUTES | .env | Missed-activity outbound block on/off (default on) and grace minutes after a bridged call (default 10) |
AMI_OUTBOUND_CHANNEL | .env | Trunk identity, e.g. PJSIP/9001 |
AMI_AGENT_CHANNEL | .env | Default PJSIP/{extension} |
PREDICTIVE_CONNECT_TIMEOUT_MS | .env | Agent ring / answer window (default 30s) |
CALL_API_URL | .env | Not used |