Back Blog
smart-missed-calls freepbx vitalpbx launch missed-calls

Smart Missed Calls is live: the missed-call list that cleans itself

We're launching Smart Missed Calls — the only module that distinguishes between a call missed on one extension (trivial) and a call NO ONE on the team picked up (the real problem). The list auto-clears when someone calls back or the caller returns. For ScitPBX, FreePBX, VitalPBX, BlureonPBX and FusionPBX.

PBXTools Team ·
Smart Missed Calls is live: the missed-call list that cleans itself

TL;DR

Smart Missed Calls is now live on the PBXTools portal. The module solves two problems the standard CDR report on FreePBX/VitalPBX consistently misses:

  1. “Missed on one extension” ≠ “missed by the whole team”. If the call was answered by anyone in the queue/IVR/ring group, it doesn’t appear on the list.
  2. The list cleans itself. When a colleague calls back or the caller returns and someone answers, the entry transitions to closed automatically, with reason and audit trail.

Works on FreePBX (14-17) and VitalPBX (4.x multi-tenant). Three install steps. No dialplan changes.

👉 Activate now

The problem with the “missed calls” report nobody talks about

Log into FreePBX → Reports → CDR Reports. Filter by disposition = NO ANSWER. You get 47 rows. You screenshot it to the team chat with “guys, are we even picking up the phone?”.

The data is correct. The conclusion is wrong.

A call routed through the “Sales” queue:

  • Rings ext 101 for 8s → NO ANSWER (row 1)
  • Rings ext 102 for 8s → NO ANSWER (row 2)
  • Rings ext 103 for 8s → NO ANSWER (row 3)
  • Rings ext 104 for 8s → NO ANSWER (row 4)
  • Maria on ext 105 picks up → ANSWERED (row 5)

The report shows 4 “missed” rows. In reality, the call was served. No customer lost, no callback needed. Maria spoke for 4 minutes, closed the order, ticked the ticket.

If you look at the first 4 rows and act on them, you do two bad things: (1) you create a complaint meeting for a problem that doesn’t exist, (2) you distract ext 101-104 from real calls coming in right now.

How Smart Missed Calls sees the same call

The actual algorithm, simplified to its essence. For each inbound call, our agent runs a query on the PBX that aggregates all CDR rows sharing the same linkedid (= all events of one logical call) and verifies simultaneously:

-- Condition 1: NO operator definitively answered
SUM(disposition = 'ANSWERED'
    AND lastapp = 'Dial'
    AND dst REGEXP '^[0-9]+$'
    AND CAST(dst AS UNSIGNED) BETWEEN 10 AND 999) = 0

-- Condition 2: there's evidence of unanswered ringing
AND (
    SUM(disposition = 'NO ANSWER') >= 1
    OR SUM(lastapp = 'missed_calls') >= 1  -- FreePBX native marker
)

For the example above, condition 1 FAILS (Maria answered, ext 105 between 10 and 999). The call is not missed. Period. It doesn’t appear in Smart Missed Calls.

For another call that rang at 4 extensions and nobody picked up — condition 1 passes (zero ANSWERED), condition 2 passes (4 NO ANSWER rows). That’s a real miss.

Aggregation: the same insistent caller appears once

Another critical detail. Andrew calls 6 times in 10 minutes, nobody catches it. The raw report gives you 6 entries “Andrew called and got no answer”. You actually want to see: “Andrew is calling insistently, prioritize”, not search through 6 identical rows.

Smart Missed Calls aggregates automatically by the key (client, caller_number, did):

Field Value
caller_number +40 745 123 456
caller_name Andrew Pope (from CRM)
did 0376443322
route_label Sales Queue
extensions ["101", "102", "103", "104"]
missed_count 6
first_missed_at 14:02:11
last_missed_at 14:11:48
status open

One entry. With clear history. With a big “×6” badge next to the name. Manager sees the priority instantly.

The list cleans itself — three automatic paths

This is the part I, honestly, haven’t seen implemented in any other CDR-based system I’ve evaluated. The dashboard list is dynamic. Entries disappear ON THEIR OWN when the problem is resolved, no checkbox required.

Path 1: cleared_by=returned

Maria, on ext. 105, dials Andrew’s number. The outbound call goes through, Andrew answers, they talk for 8 minutes. On the next CDR cycle (within 5 minutes max), the agent detects the row:

src=105, dst=+40745123456, disposition=ANSWERED, lastapp=Dial

It recognizes that +40745123456 is a caller_number with an open entry for this client, and marks the entry:

status=cleared
cleared_by=returned
cleared_by_extension=105
cleared_call_at=14:23:09

Audit log: action=cleared_returned, actor_type=agent, note="Returned by extension 105".

The entry leaves the “open” list automatically. Maria did no special clicking.

Path 2: cleared_by=answered_later

Or Andrew, frustrated, dials again 30 minutes later. This time someone answers (anyone, anywhere in queue/IVR). Inbound ANSWERED with the same caller_number and DID. Entry closes:

status=cleared
cleared_by=answered_later
cleared_by_extension=<extension that answered>

Audit log: cleared_answered. Disappears from the list on its own.

Path 3: cleared_by=manual

The case where someone handled the customer outside the phone — WhatsApp, email, in-person visit. For that there is the “Mark Resolved” button in the client portal. Who gets to see it is decided per company, in three steps: nobody (entries close only automatically), only the Coordinators of the app, or anyone with access to it. A new company starts on “nobody” — marking an entry resolved by hand is the claim “I handled this case away from the phone”, which the system cannot verify, so it is never switched on by itself.

The permitted user clicks, optionally adds a note, confirms. The entry:

status=cleared
cleared_by=manual
cleared_by_user_id=<the user's id>
cleared_note="Called him on WhatsApp, took the order"

Full audit: cleared_manual with actor_type=user and actor_user_id.

Plus: retention and reopen

  • cleared_by=expired — an hourly cron auto-closes entries older than N days (per-client setting). No retention = “forever” mode, entries stay open until truly closed.
  • reopened — if the caller dials again AFTER the entry was closed, the system reopens it automatically and increments missed_count. Audit shows reopened with actor_type=agent.

Audit log — for when blind trust isn’t enough

One of the features managers use most. The smart_missed_calls_audit table is append-only. Every event writes a row with:

  • action (created / incremented / notified / cleared_* / reopened)
  • actor_type (agent / user / cron)
  • actor_user_id (when applicable)
  • note (optional, for manual)
  • metadata (JSON — extension that called back, time, etc.)

This lets you, as admin, verify whether an agent is marking entries “manually resolved” without an actual callback. You’ll see: cleared_manual by user 12 with no subsequent cleared_returned audit. Clear signal for a constructive conversation.

And if you suspect a manual resolve was forced, there’s the admin-only reopen action that brings the entry back and preserves the prior closure history in audit.

Notifications — 5 cadences + dedupe

Five ways to be alerted, per-client configurable:

  • off — nothing, dashboard only
  • realtime — email per new entry (with (caller, did) dedupe, 5 min default, plus 50/h hard rate limit so the inbox doesn’t drown)
  • hourly — one email at the top of each hour with the past hour’s list
  • daily — at notify_daily_at (default 08:00) with yesterday’s report
  • weekly — on notify_weekly_day at notify_daily_at
  • monthly — on notify_monthly_day at notify_daily_at

Plus: unlimited recipients per portal, optional “use extension email” (sends to the user-of-the-extension’s email, not a generic inbox).

Privacy and non-interference

Two technical promises that matter:

1. Your data stays with you. Only metadata leaves to the portal: caller_number, caller_name (when present in CDR), did, route_label, extensions[], missed_count, first/last_missed_at, status. Never audio, never recordings, never conversation content.

2. We don’t touch anything in FreePBX/VitalPBX. No dialplan changes. No new AMI. No open ports. No deletion of native recording files. We read asteriskcdrdb (FreePBX) or per-tenant DBs (VitalPBX) read-only. UCP, Sonata, Call Reports native — all keep working as you know them. We are strictly observers.

Communication with the PBXTools portal goes through a WebSocket over HTTPS on port 443. No port forwarding, no VPN. Works through any corporate firewall.

Enterprise customers can opt for self-hosted deployment of the entire portal in their own VPC. In that case not even metadata leaves the customer’s infrastructure.

Compatibility at launch

System Versions Status
FreePBX 14, 15, 16, 17 ✅ Full support, dual fallback (lastapp='missed_calls' + CDR aggregation)
VitalPBX 4.x multi-tenant ✅ Full support, strict per-tenant isolation
FusionPBX — 🛣️ Q2 2026
Issabel — 🛣️ Q2 2026

How to activate

# 1. Free account on portal + add the PBX
https://portal.pbxtools.ro/client/register

# 2. Grab an installer token from the portal, then on the PBX (one line):
curl -sL https://cool.pbxtools.ro/api/v1/agent/installer/<TOKEN> | sudo bash

# 3. In the portal: enable the module on the Client
#    - smart_missed_calls_enabled = true
#    - retention_days (or leave null = forever)
#    - allow_manual_resolve (recommended: true)
#    - notify_frequency, notify_daily_at, recipients

On the next keepalive cycle (5 minutes) you receive a backfill of calls from the past 24 hours.

Roadmap

  • FusionPBX + Issabel — Q2 2026
  • Push mode sub-30s via AMI listening (Enterprise)
  • Mobile push for VIP callers (CRM-flagged)
  • Voicemail-to-text integrated into the digest

Activate Smart Missed Calls. For technical questions — contact page or call 0376 44 33 22.