VICIdial's built-in reporting is accurate. It covers the fundamentals and has served operations of all sizes for years. But if you've tried putting VICIdial's default HTML reports on a wallboard, handing them to an executive, or using them to make split-second decisions during a live campaign — you already know the limitations.
Static HTML tables. One campaign at a time. Manual page refreshes. No alerting. No threshold coloring. No time-series charts showing trends instead of snapshots. The data is all there in MySQL — every dial attempt, every agent state change, every second of talk time, every disposition. The problem is presentation.
Grafana turns that data into auto-refreshing dashboards with configurable panels, color-coded thresholds, and alerts that fire before problems cost you money. Metabase gives your managers a self-service analytics layer where they can ask questions about VICIdial data without writing SQL.
This guide walks through setup, the specific SQL queries for every panel a call center needs, alerting configuration, and wallboard deployment.
Why External Dashboards
VICIdial's default reports render as static HTML that requires manual refreshes, show one campaign at a time in most views, don't support time-series charts or gauges or heatmaps, have no built-in alerting, require admin-level VICIdial access, and can't be customized without modifying PHP source code.
What Grafana adds: auto-refreshing panels (configurable down to 5 seconds), multi-campaign views on a single screen, color-coded green/yellow/red thresholds at a glance, time-series trending, alerting via email, Slack, PagerDuty, or webhooks, role-based access without VICIdial admin credentials, and TV/wallboard display mode for the operations floor.
You shouldn't abandon VICIdial's built-in reports — they're still useful for detailed drill-downs and ad-hoc lookups. External dashboards are for the metrics you watch continuously.
Setup: 15 Minutes to First Panel
Install Grafana
On AlmaLinux/Rocky Linux (ViciBox base):
cat <<'EOF' | sudo tee /etc/yum.repos.d/grafana.repo
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF
sudo dnf install -y grafana
sudo systemctl enable --now grafana-server
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --reload
Grafana runs at http://your-server:3000. Default login: admin/admin — change it immediately.
Where to install: On your web server or a dedicated monitoring server — not a dialer server. Dialer servers need every CPU cycle for Asterisk.
Create a Read-Only MySQL User
Never point Grafana at VICIdial with write access:
CREATE USER 'grafana_ro'@'%' IDENTIFIED BY 'strong_random_password';
GRANT SELECT ON asterisk.* TO 'grafana_ro'@'%';
FLUSH PRIVILEGES;
If Grafana runs on the same server, replace '%' with 'localhost'. If on a separate server, replace with the Grafana server's IP.
For tighter security, grant SELECT only on specific operational tables instead of asterisk.*:
GRANT SELECT ON asterisk.vicidial_log TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_closer_log TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_agent_log TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_live_agents TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_auto_calls TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_hopper TO 'grafana_ro'@'%';
GRANT SELECT ON asterisk.vicidial_campaigns TO 'grafana_ro'@'%';
FLUSH PRIVILEGES;
Configure the Data Source
In Grafana: Connections > Data Sources > Add > MySQL. Set host, database (asterisk), user, password. Click Save & Test — "Database Connection OK."
If it fails: check MySQL bind-address in /etc/my.cnf (if Grafana is on another server, MySQL can't be listening only on 127.0.0.1), verify firewall allows 3306, verify MySQL user has correct host grants.
Set Up Dashboard Variables
Before building panels, configure dashboard variables so supervisors can filter by campaign:
Name: campaign_id
Type: Query
Data source: VICIdial MySQL
Query: SELECT DISTINCT campaign_id FROM vicidial_campaigns WHERE active = 'Y' ORDER BY campaign_id;
Multi-value: Yes
Include All option: Yes
Now every panel query can reference $campaign_id as a filter, and supervisors switch campaigns from a dropdown.
The 10 Panels That Matter
These queries are tested against VICIdial databases handling 100,000+ calls per day.
Panel 1: Drop Rate Gauge (The Most Important Panel)
The FTC's 3% abandoned call rule is measured over a 30-day campaign period. This should be the largest panel on your wallboard.
SELECT ROUND(
COUNT(CASE WHEN status = 'DROP' THEN 1 END) /
NULLIF(COUNT(CASE WHEN status NOT IN ('NA','B','DC','N','AFTHRS','XDROP') THEN 1 END), 0) * 100, 2
) as drop_rate_30d
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND campaign_id IN ($campaign_id);
Thresholds: Green < 2.0%. Yellow 2.0-2.5%. Red > 2.5%. Set red well below 3% because one bad hour can push you over.
Panel 2: Connect Rate Gauge
SELECT ROUND(
COUNT(CASE WHEN status NOT IN ('NA','B','DC','N','AFTHRS','AB','ADC','XDROP')
AND user != '' THEN 1 END) /
NULLIF(COUNT(*), 0) * 100, 2
) as connect_rate
FROM vicidial_log
WHERE call_date >= CURDATE()
AND campaign_id IN ($campaign_id);
Thresholds: Green > 15%. Yellow 10-15% (check DID health). Red < 10% (list quality, DID flagging, or time-of-day issue). Adjust by vertical — debt collection might see 8-12% as normal, warm lead follow-up should hit 25%+.
Panel 3: Calls Per Hour Time Series
The single most useful chart for understanding campaign momentum. Refresh every 30 seconds.
SELECT
$__timeGroup(call_date, '1h') as time,
COUNT(*) as Dials,
COUNT(CASE WHEN status NOT IN ('NA','B','DC','N','AFTHRS') THEN 1 END) as Answered,
COUNT(CASE WHEN status IN ('SALE','SET','XFER') THEN 1 END) as Conversions
FROM vicidial_log
WHERE $__timeFilter(call_date)
AND campaign_id IN ($campaign_id)
GROUP BY $__timeGroup(call_date, '1h')
ORDER BY time;
Use different colors: Dials (blue), Answered (green), Conversions (orange). Add a dashed line for Conversions since the scale is typically much smaller.
Panel 4: Real-Time Agent Status
Shows how many agents are in each state right now. Refresh every 5 seconds. Use Pie Chart or Bar Gauge visualization.
SELECT status as "Status", COUNT(*) as "Count"
FROM vicidial_live_agents
WHERE campaign_id IN ($campaign_id)
GROUP BY status ORDER BY COUNT(*) DESC;
Color mapping: INCALL = green (productive). READY = blue (waiting — dialer should be sending calls). PAUSED = orange (not taking calls). CLOSER = teal (handling inbound/transfer). DEAD = red (session problem).
Healthy outbound campaign: most agents INCALL, small percentage READY, minimal PAUSED outside breaks. High READY = dial level too low or hopper empty. PAUSED consistently above 15-20% = investigate pause codes.
Panel 5: Agent Leaderboard Table
Real-time ranking by production. Put this on the floor wallboard — healthy competition drives performance. Refresh every 30 seconds.
SELECT
val.user as "Agent",
COUNT(*) as "Calls",
COUNT(CASE WHEN val.status IN ('SALE','SET','XFER') THEN 1 END) as "Sales",
ROUND(AVG(val.talk_sec), 0) as "Avg Talk (s)",
ROUND(SUM(val.talk_sec) / 60, 0) as "Talk Min",
ROUND(SUM(val.pause_sec) / 60, 0) as "Pause Min",
ROUND(
SUM(val.talk_sec) /
NULLIF(SUM(val.talk_sec + val.wait_sec + val.pause_sec + val.dispo_sec), 0) * 100, 1
) as "Utilization %"
FROM vicidial_agent_log val
WHERE val.event_time >= CURDATE()
AND val.campaign_id IN ($campaign_id)
GROUP BY val.user
ORDER BY COUNT(CASE WHEN val.status IN ('SALE','SET','XFER') THEN 1 END) DESC;
Top-performing outbound agents hit 45-55% utilization. Below 35% means the dialer isn't feeding fast enough or excessive pausing. Use Grafana column overrides to add a bar gauge to "Utilization %" with color coding.
Panel 6: Conversion Rate by Campaign Bar Chart
Cross-campaign comparison for today. Lets managers spot which campaigns are producing.
SELECT
campaign_id as "Campaign",
COUNT(*) as "Dials",
COUNT(CASE WHEN user != '' AND status NOT IN ('NA','B','DC','N','DROP','AFTHRS','XDROP')
THEN 1 END) as "Contacts",
COUNT(CASE WHEN status IN ('SALE','SET','XFER') THEN 1 END) as "Conversions",
ROUND(
COUNT(CASE WHEN status IN ('SALE','SET','XFER') THEN 1 END) /
NULLIF(COUNT(CASE WHEN user != '' AND status NOT IN ('NA','B','DC','N','DROP','AFTHRS','XDROP')
THEN 1 END), 0) * 100, 2
) as "Contact-to-Conv %"
FROM vicidial_log
WHERE call_date >= CURDATE()
GROUP BY campaign_id
HAVING COUNT(*) > 10;
High contact rates + low contact-to-conversion = scripting/training problem. Low contact rates + decent conversion = dialing/list/DID problem.
Panel 7: Hopper Status
Early warning for list exhaustion. Refresh every 10 seconds.
SELECT campaign_id as "Campaign", COUNT(*) as "Leads in Hopper"
FROM vicidial_hopper
WHERE campaign_id IN ($campaign_id)
GROUP BY campaign_id;
Thresholds: Green > 200. Yellow 50-200. Red < 50. A 100-agent campaign at dial level 3.0 burns ~300 leads per minute. At 50 leads, you have ~10 seconds before the dialer stalls.
Panel 8: Trunk Utilization Gauge
Monitor SIP trunk capacity to prevent call failures. Refresh every 10 seconds.
SELECT COUNT(*) as active_channels FROM vicidial_auto_calls;
Set gauge maximum to your total trunk capacity (e.g., 300 simultaneous channels). Green < 70% of capacity. Yellow 70-85%. Red > 85% (risk of SIP 503 errors on new calls). If you're consistently hitting yellow, it's time to add trunk capacity or reduce dial levels.
For operations running multiple carriers, break this down by server_ip from vicidial_carrier_log to see which trunk groups are saturated. A single overloaded carrier creates call failures even when total capacity across all carriers is fine.
Panel 8b: DID Health Summary (Stat Panel)
Track the number of active DIDs in rotation and flag any with declining performance. Refresh every 300 seconds.
SELECT
COUNT(DISTINCT phone_number) as active_dids,
ROUND(AVG(CASE WHEN status NOT IN ('NA','B','DC','N','AFTHRS','DROP','XDROP')
AND user != '' THEN 1.0 ELSE 0 END) * 100, 1) as avg_connect_rate
FROM vicidial_log
WHERE call_date >= CURDATE()
AND campaign_id IN ($campaign_id);
This gives you a quick read on whether your DID pool is healthy overall. If the average connect rate drops sharply compared to previous days with similar lists, DID flagging is the most likely culprit.
Panel 9: Average Handle Time Trend
Track AHT trends throughout the day. Refresh every 60 seconds.
SELECT
$__timeGroup(event_time, '30m') as time,
ROUND(AVG(talk_sec + dispo_sec), 1) as "Avg Handle Time (s)",
ROUND(AVG(talk_sec), 1) as "Avg Talk Time (s)",
ROUND(AVG(dispo_sec), 1) as "Avg Dispo Time (s)"
FROM vicidial_agent_log
WHERE $__timeFilter(event_time)
AND campaign_id IN ($campaign_id)
AND talk_sec > 0
GROUP BY $__timeGroup(event_time, '30m')
ORDER BY time;
If dispo time is climbing, agents may be sitting in wrap-up to avoid calls. If talk time spikes, check for script or product changes.
Panel 10: Hourly Conversion Heatmap
Shows which hours and days produce the best conversion rates across 30 days. Refresh every 5 minutes.
SELECT
DAYNAME(call_date) as day_name,
HOUR(call_date) as hour,
ROUND(
COUNT(CASE WHEN status IN ('SALE','SET','XFER') THEN 1 END) /
NULLIF(COUNT(CASE WHEN user != '' AND status NOT IN ('NA','B','DC','N','DROP','AFTHRS','XDROP')
THEN 1 END), 0) * 100, 2
) as conversion_rate
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND campaign_id IN ($campaign_id) AND user != ''
GROUP BY DAYNAME(call_date), HOUR(call_date)
ORDER BY DAYOFWEEK(call_date), hour;
If Tuesday 10-11 AM converts at 8% while Friday 4-5 PM converts at 2%, you know where to put your best agents and where to schedule training.
Set Up Alerting
Panels are for watching. Alerting is for when you're not watching. This is where Grafana moves from "nice dashboard" to "operational necessity."
Critical Alerts (Immediate Action Required)
Drop rate > 2.5% — Email + Slack immediately. This is compliance critical. The FTC's 3% rule on abandoned calls is measured over a 30-day campaign period. By the time you hit 2.5%, one bad hour pushes you over. Configure this as a Grafana alert rule against the 30-day drop rate query:
SELECT ROUND(
COUNT(CASE WHEN status = 'DROP' THEN 1 END) /
NULLIF(COUNT(CASE WHEN status NOT IN ('NA','B','DC','N','AFTHRS','XDROP') THEN 1 END), 0) * 100, 2
) as drop_rate
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND campaign_id IN ($campaign_id);
Set the condition to "is above 2.5" and the evaluation interval to every 5 minutes.
Zero agents INCALL for > 5 minutes — Something is broken. Either the dialer stopped, Asterisk crashed, the hopper is empty, or there's a carrier outage. This should page the on-call person.
Trunk utilization > 85% — SIP 503 errors incoming. Add capacity or reduce dial levels immediately. Calls are about to start failing.
Warning Alerts (Investigate Within 30 Minutes)
Hopper < 50 leads — Dialer is about to stall. Either add more lists to the campaign or notify the operations lead that the current list is exhausted.
Connect rate dropped > 5 percentage points vs. yesterday — Could indicate DID flagging, list quality issues, or a carrier problem. Check which DIDs are in rotation and test-call them.
Agent utilization below 30% for > 1 hour — Agents are sitting idle. Either the dialer isn't feeding them (check hopper and dial level), or they're pausing excessively (check pause code breakdown).
Alert Configuration
Grafana supports email, Slack, PagerDuty, Microsoft Teams, webhooks, and dozens of other notification channels. At minimum, set up email for everything and add Slack (or equivalent) for critical alerts. PagerDuty or OpsGenie for after-hours critical alerts if you run multi-shift operations.
Create a notification policy that routes alerts to the right people: compliance alerts go to the compliance officer and operations lead, technical alerts (trunks, hopper) go to the sysadmin, performance alerts (utilization, connect rate) go to the campaign managers.
Complete Dashboard JSON Template
Instead of building every panel manually, Grafana supports JSON import. Go to Dashboards > New > Import and paste a template. A complete VICIdial operations dashboard template should include all 10 panels with pre-configured variables, sensible refresh intervals, and threshold coloring.
The template structure:
{
"dashboard": {
"title": "VICIdial Operations Dashboard",
"tags": ["vicidial", "call-center"],
"timezone": "browser",
"refresh": "30s",
"time": { "from": "now-12h", "to": "now" },
"templating": {
"list": [{
"name": "campaign_id",
"type": "query",
"datasource": "VICIdial MySQL",
"query": "SELECT DISTINCT campaign_id FROM vicidial_campaigns WHERE active = 'Y'",
"multi": true,
"includeAll": true
}]
}
}
}
Each panel definition includes the SQL query as rawSql, the visualization type (gauge, time-series, stat, table, piechart), grid position, and threshold configuration. The drop rate gauge should occupy the top-left with "gridPos": {"h": 6, "w": 6, "x": 0, "y": 0}, connect rate gauge next to it, hopper status and trunk utilization completing the top row.
Place the agent status pie chart and agent leaderboard table in the middle row. Calls per hour time series and conversion bar chart in the bottom half. The hourly conversion heatmap and AHT trend can go on a second dashboard tab for deeper analysis.
Wallboard Best Practices
For TV/monitor display on the operations floor:
- Use Grafana's kiosk mode — append
?kioskto the dashboard URL - Set browser to full-screen mode
- Configure dashboard refresh to 30 seconds
- Use dark theme for better visibility on TVs
- If you have multiple dashboards, set up a playlist for auto-rotation between them (5-minute intervals work well)
- Dedicate a cheap thin client or Raspberry Pi to each display — don't tie up a workstation
The drop rate gauge and agent status panel should be visible from anywhere on the floor. The agent leaderboard drives competition — agents will ask to have it up where everyone can see it.
Dashboard Security
Grafana's role-based access lets you give managers dashboard access without VICIdial admin credentials. Create a "Viewer" role for supervisors (view dashboards only), an "Editor" role for operations leads (modify panels), and reserve "Admin" for your sysadmin.
If Grafana is accessible outside your local network, configure HTTPS. Use a reverse proxy (nginx) with a Let's Encrypt certificate. Never expose port 3000 directly to the internet with default admin credentials.
The read-only MySQL user provides the database-level security layer. Even if someone compromises the Grafana login, they can only run SELECT queries against VICIdial data — they can't modify campaigns, leads, or system configuration.
Performance Considerations
Heavy Grafana queries against a production VICIdial database can slow down live dialing. Recommendations:
- Run Grafana against a read replica if possible — this isolates reporting queries from production writes entirely
- Avoid queries that scan the entire
vicidial_logtable without a date filter. Always include$__timeFilter(call_date)orcall_date >= CURDATE() - For historical analysis panels (like the 30-day conversion heatmap), cache results with a longer refresh interval (5 minutes) rather than refreshing every 30 seconds
- Monitor Grafana's query execution time in the panel inspector. If a query takes more than 2 seconds, optimize it with proper indexes or use pre-computed summary tables
- Check
SHOW PROCESSLISTon your MySQL server periodically — if Grafana queries are showing up with long execution times, they need attention
Metabase for Non-Technical Users
Grafana is built for operations monitoring — real-time panels, auto-refresh, threshold alerts. Metabase is built for business users who want to explore data without writing SQL. Most VICIdial operations benefit from running both.
docker run -d -p 3000:3000 --name metabase metabase/metabase
Add the asterisk database as a data source (use the same read-only user you created for Grafana). Metabase auto-detects tables and lets managers build questions using point-and-click. The MySQL views you create for Grafana work even better in Metabase because they hide join complexity and give clean, readable column names.
Metabase's strength is self-service exploration: a campaign manager can ask "what was the conversion rate by hour for campaign SOLAR last week?" without knowing SQL. They click through a visual query builder, get the answer as a chart, and can save it as a dashboard. This eliminates the "hey, can you pull a report for me?" requests that eat your sysadmin's time.
For VICIdial, create the MySQL views described earlier (v_agent_daily_summary, v_campaign_hourly, v_list_penetration, etc.) and tell Metabase about them. Views become first-class tables in Metabase's interface — managers see clean names like "Agent Daily Summary" instead of raw table names like "vicidial_agent_log."
Pro tip: Metabase's email subscriptions let managers receive daily or weekly dashboard snapshots in their inbox. Set up a weekly campaign performance summary that auto-delivers Monday at 7 AM. Your managers start the week with data instead of waiting for someone to run a report.
Wallboard Deployment
Grafana has a built-in kiosk mode for TV displays. Add ?kiosk to the dashboard URL, set the browser to full-screen, and you have a live wallboard. Set the dashboard refresh to 30 seconds and configure auto-rotation between dashboards if you have multiple.
Disposition Breakdown Panel (Bonus)
Understanding exactly how calls are being dispositioned tells you where to focus coaching:
SELECT
status,
COUNT(*) AS call_count,
ROUND(COUNT(*) / SUM(COUNT(*)) OVER () * 100, 2) AS pct_of_total
FROM vicidial_log
WHERE call_date >= CURDATE()
AND campaign_id IN ($campaign_id)
GROUP BY status
ORDER BY call_count DESC;
If NI (Not Interested) is 60% of contacts but SALE is only 3%, your scripting or offer needs work. If DNC is spiking, check which lists are generating opt-out requests — they may be stale data.
Carrier Performance Panel (For Multi-Carrier Setups)
Track which SIP carriers are delivering the best results:
SELECT
server_ip,
dialstatus,
COUNT(*) AS calls,
ROUND(AVG(answered_time), 1) AS avg_answer_sec,
ROUND(COUNT(*) / SUM(COUNT(*)) OVER () * 100, 2) AS pct_of_total
FROM vicidial_carrier_log
WHERE call_date >= CURDATE()
GROUP BY server_ip, dialstatus
ORDER BY server_ip, calls DESC;
If one carrier shows a disproportionate number of CHANUNAVAIL or CONGESTION statuses, it's having capacity or routing issues. Time to route traffic elsewhere.
Read Replicas: The Right Way to Run Reporting at Scale
For operations with 50+ agents generating significant query volume, the ideal architecture puts Grafana and Metabase on a MySQL read replica rather than the production database. This completely isolates reporting queries from production writes — no matter how many Grafana panels are refreshing every 5 seconds, your hopper refill and call processing queries run at full speed on the master.
Setting up a read replica follows the same master-slave replication pattern described in our disaster recovery guide. Point your Grafana data source at the replica's IP instead of the master. The replica stays within seconds of the master, so your dashboards show near-real-time data without any risk to production performance.
If a read replica isn't in your budget, at minimum ensure your Grafana queries include date filters (call_date >= CURDATE() or $__timeFilter) so they don't scan the entire history of vicidial_log every refresh cycle. A query without a date filter on a 50-million-row table will pin a CPU core and slow down everything.
VICIdial's data is excellent. Its presentation isn't. Grafana closes that gap and gives floor supervisors real-time visibility to make decisions in seconds rather than minutes. If building dashboards from scratch isn't your thing, ViciStack deploys pre-built Grafana dashboards with every managed VICIdial deployment — configured, tested, and ready for your wallboard on day one.
Originally published at https://vicistack.com/blog/vicidial-grafana-dashboards/
Top comments (0)