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 FreePBX and VitalPBX.

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):

FieldValue
caller_number+40 745 123 456
caller_nameAndrew Pope (from CRM)
did0376443322
route_labelSales Queue
extensions["101", "102", "103", "104"]
missed_count6
first_missed_at14:02:11
last_missed_at14:11:48
statusopen

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’s the “Mark Resolved” button in Filament (visible only when admin enabled smart_missed_calls_allow_manual_resolve for the client).

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

SystemVersionsStatus
FreePBX14, 15, 16, 17✅ Full support, dual fallback (lastapp='missed_calls' + CDR aggregation)
VitalPBX4.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/register

# 2. On the PBX, install command (one line):
curl -fsSL https://portal.pbxtools.ro/agent/install \
  | bash -s -- --api-key <YOUR_KEY>

# 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 [email protected].