Kommo + Grafana: sales dashboard based on CRM data with no vendor lock-in
Grafana is an open-source data visualization platform: 50+ datasource plugins (Postgres, InfluxDB, Prometheus, Elasticsearch, MySQL), flexible dashboards, alerts. Widely used for infrastructure monitoring, but works excellently for business analytics when CRM data is exported to a SQL database. Unlike Metabase or Redash, Grafana is the tool for teams that already have Postgres and have no desire to pay for a BI SaaS. No vendor lock-in, no licenses — just your data and SQL.
Grafana vs Metabase vs Redash for CRM analytics
| Parameter | Grafana | Metabase | Redash |
|---|---|---|---|
| Open-source | Yes | Yes (Community) | Yes |
| Time-series focus | Yes | No | No |
| Datasources | 50+ (Postgres, InfluxDB, Prometheus…) | SQL databases, MongoDB | SQL databases |
| Self-hosted | Yes | Yes | Yes |
| Alerts | Native, multi-channel | Via Slack | Limited |
| Grafana Cloud | Yes | No | No |
| Best for | DevOps + business metrics in one | Business analytics | SQL dashboards |
Grafana is chosen by teams that already have DevOps dashboards in Grafana and want to add business metrics to the same tool.
Architecture: Kommo -> ETL -> Postgres -> Grafana
There is no direct Grafana datasource for Kommo. The correct architecture:
Kommo REST API -> Python ETL (cron) -> Postgres -> Grafana
- Python ETL (cron every 30–60 min): pulls data from the Kommo API (deals, contacts, statuses)
- Postgres (or TimescaleDB for time-series): stores normalized data
- Grafana: PostgreSQL datasource -> SQL queries -> panels and dashboards
ETL: exporting Kommo data to Postgres
import requests
import psycopg2
from datetime import datetime, timezone, timedelta
KOMMO_SUBDOMAIN = "youraccount"
KOMMO_TOKEN = "your_access_token"
KOMMO_BASE_URL = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HEADERS = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
DB_DSN = "postgresql://user:pass@localhost:5432/analytics"
def fetch_leads(updated_after: datetime) -> list:
# Paginated fetch of all deals updated after the given date
ts = int(updated_after.timestamp())
leads = []
page = 1
while True:
resp = requests.get(
f"{KOMMO_BASE_URL}/leads",
headers=KOMMO_HEADERS,
params={
"updated_at[from]": ts,
"page": page,
"limit": 250,
},
)
if resp.status_code == 204:
break
resp.raise_for_status()
data = resp.json().get("_embedded", {}).get("leads", [])
if not data:
break
leads.extend(data)
page += 1
return leads
def upsert_leads_to_postgres(leads: list):
conn = psycopg2.connect(DB_DSN)
cur = conn.cursor()
create_sql = (
"CREATE TABLE IF NOT EXISTS kommo_leads ("
"id BIGINT PRIMARY KEY, name TEXT, status_id INT, "
"pipeline_id INT, responsible_user_id BIGINT, price NUMERIC, "
"created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, "
"closed_at TIMESTAMPTZ, loss_reason_id INT)"
)
cur.execute(create_sql)
for lead in leads:
insert_sql = (
"INSERT INTO kommo_leads "
"(id, name, status_id, pipeline_id, responsible_user_id, "
"price, created_at, updated_at, closed_at, loss_reason_id) "
"VALUES (%s,%s,%s,%s,%s,%s,"
"to_timestamp(%s),to_timestamp(%s),to_timestamp(%s),%s) "
"ON CONFLICT (id) DO UPDATE SET "
"status_id=EXCLUDED.status_id, price=EXCLUDED.price, "
"updated_at=EXCLUDED.updated_at, closed_at=EXCLUDED.closed_at, "
"loss_reason_id=EXCLUDED.loss_reason_id"
)
cur.execute(insert_sql, (
lead["id"], lead.get("name"),
lead.get("status_id"), lead.get("pipeline_id"),
lead.get("responsible_user_id"), lead.get("price"),
lead.get("created_at"), lead.get("updated_at"),
lead.get("closed_at"), lead.get("loss_reason_id"),
))
conn.commit()
cur.close(); conn.close()
def run_etl():
updated_after = datetime.now(timezone.utc) - timedelta(hours=2)
leads = fetch_leads(updated_after)
if leads:
upsert_leads_to_postgres(leads)
print(f"ETL: {len(leads)} leads upserted")
if __name__ == "__main__":
run_etl()
SQL queries for Grafana panels
After configuring the Postgres datasource in Grafana — build panels via SQL.
Conversion funnel (Bar Chart):
SELECT
s.name AS stage,
COUNT(*) AS leads,
SUM(l.price) AS pipeline_value
FROM kommo_leads l
JOIN kommo_statuses s ON l.status_id = s.id
WHERE l.pipeline_id = $pipeline_id
AND l.created_at >= $__timeFrom()
GROUP BY s.name, s.sort
ORDER BY s.sort;
Lead Velocity (leads created per week):
SELECT
date_trunc('week', created_at) AS week,
COUNT(*) AS new_leads
FROM kommo_leads
WHERE created_at >= $__timeFrom()
AND created_at <= $__timeTo()
GROUP BY 1
ORDER BY 1;
Manager comparison (Won deals):
SELECT
u.name AS manager,
COUNT(*) AS won_deals,
SUM(l.price) AS total_revenue,
AVG(EXTRACT(EPOCH FROM (l.closed_at - l.created_at))/86400) AS avg_days_to_close
FROM kommo_leads l
JOIN kommo_users u ON l.responsible_user_id = u.id
WHERE l.status_id = $won_status_id
AND l.closed_at >= $__timeFrom()
GROUP BY u.name
ORDER BY total_revenue DESC;
Win Rate by month:
SELECT
date_trunc('month', closed_at) AS month,
COUNT(*) FILTER (WHERE status_id = $won_status_id) AS won,
COUNT(*) FILTER (WHERE status_id = $lost_status_id) AS lost,
ROUND(
100.0 * COUNT(*) FILTER (WHERE status_id = $won_status_id)
/ NULLIF(COUNT(*), 0), 1
) AS win_rate_pct
FROM kommo_leads
WHERE closed_at >= $__timeFrom()
GROUP BY 1
ORDER BY 1;
Alerts: Grafana -> team notification
Grafana Alert Rules allow: if new_leads_today = 0 -> Slack notification. If avg_deal_age > 30 days -> email to team lead.
Setup: Grafana -> Alerting -> Alert Rules -> create rule based on any SQL query.
Real-world case
SaaS startup (EU, 30 people, Kommo + Grafana + Postgres):
- Before: sales analytics — Excel once a week. Conversion rate, lead velocity — manual. The DevOps team was already using Grafana for infrastructure monitoring.
- After: added business metrics to the same Grafana. Python ETL every hour -> Postgres -> Grafana. One dashboard with infrastructure and sales on the same screen. No new tool — just a datasource added.
- Key insight: lead velocity dropped 40% in one week -> Grafana alert -> retrospective held. Turned out: one lead generation channel had been switched off. Discovered in 2 days, not 2 weeks.
Who this is relevant for
- Technical teams with Grafana for DevOps — easier to add business metrics than adopt a new BI tool
- Startups with Postgres in the stack — no additional BI SaaS costs
- Companies with self-hosted analytics requirements (finance, healthcare)
- Teams that need analytics at the custom SQL level without BI tool constraints
Frequently asked questions
Grafana Cloud vs self-hosted — which to choose for CRM analytics?
Grafana Cloud Free: 3 users, 10k metrics, sufficient for a small team. Self-hosted: full control, no limits, server needed (Docker or Kubernetes). For small team CRM analytics — Grafana Cloud Free + Postgres on the same server as the ETL.
Kommo API rate limits — how to avoid exceeding them during ETL?
Kommo: 7 requests per second, 5,000 requests per hour. With incremental ETL (only updated in the last N hours) — typically 1–5 requests per run. With full sync (all deals) — pagination by 250, with a 0.2 sec delay between requests. Full sync is only needed on the first run.
Grafana Variables — how to parameterize the dashboard?
Grafana -> Dashboard Settings -> Variables. Type Query: SQL query from Postgres. For example, SELECT DISTINCT pipeline_id FROM kommo_leads -> dropdown in the dashboard. Used as $pipeline_id in SQL queries for panels. This allows switching pipeline/manager without editing SQL.
How do I add Kommo custom fields to Postgres?
Custom fields in Kommo are stored in _embedded.custom_fields_values as an array. During ETL: parse the array by field_id -> write to separate table columns or to a JSONB column. JSONB allows queries: WHERE custom_fields->>'plan' = 'growth'.
Summary
- Architecture: Kommo API -> Python ETL (cron) -> Postgres -> Grafana PostgreSQL datasource
- ETL: incremental by
updated_at[from], upsert byid, pagination 250 leads per request - Key panels: funnel, lead velocity, win rate, manager comparison — all via SQL
- Grafana Variables: parameterize by pipeline_id, manager, period
- Alerts: Lead velocity = 0, avg deal age > threshold — Slack/email notification
If you use Kommo and want to build a Grafana dashboard — describe which metrics are critical and whether Postgres already exists in your infrastructure. Exceltic.dev will configure the ETL and a baseline set of panels.