Kommo + Tableau: Sales BI Dashboard via ETL Without Manual Data Exports

Tableau is the enterprise BI visualization leader (Gartner Magic Quadrant Leader 2024): drag-and-drop analytics, Tableau Server / Tableau Cloud, 80+ native connectors. Widely used in enterprise environments where sales, finance, and operations analytics live in a single tool. There is no direct Tableau connector for Kommo — Tableau does not have a marketplace of CRM connectors at the native API level. The correct architecture: export Kommo data into a relational database via Python ETL, then Tableau connects to that database as a datasource. Unlike Grafana or Metabase, Tableau is a tool for analysts without SQL, with drag-and-drop visualization building.

Tableau vs Grafana vs Metabase for CRM Analytics

ParameterTableauGrafanaMetabase
Target audienceBusiness analysts (no SQL)DevOps + technical teamsBusiness (no SQL), self-hosted
InterfaceDrag-and-dropSQL queries + panelsDrag-and-drop + SQL
Time-series focusNo (general BI)YesNo
Native connectors80+ (Salesforce, Google Sheets)50+ datasource pluginsSQL databases, MongoDB
Self-hostedTableau Server (paid)Yes, freeYes, free (Community)
Price$75–$115/user/monthFreeFree (Community)
Best forEnterprise BI with BI analystsDevOps + business metricsSMB self-hosted BI

Tableau is chosen by enterprise companies where BI analysts build dashboards for business users without SQL, and where a corporate license already exists.

Architecture: Kommo -> Tableau

Kommo REST API  ->  Python ETL (cron)  ->  Postgres  ->  Tableau (Published Datasource)

Two options for publishing data in Tableau:

  1. PostgreSQL datasource — Tableau connects directly to Postgres. Simple, data is always fresh. Requires network access from Tableau Server -> Postgres.
  2. Tableau Hyper API — Python publishes a .hyper file (in-memory columnar format) to Tableau Server/Cloud. Faster queries, no persistent DB connection required. Optimal for Tableau Cloud where Postgres is not directly accessible.

ETL: Kommo -> Postgres

The base ETL is similar to other BI integrations. Key tables for a Tableau sales dashboard:

import requests
import psycopg2
from datetime import datetime, timezone, timedelta

KOMMO_BASE  = "https://youraccount.kommo.com/api/v4"
KOMMO_HDRS  = {"Authorization": "Bearer your_token"}
DB_DSN      = "postgresql://user:pass@localhost:5432/analytics"

# Schema: kommo_leads, kommo_pipelines, kommo_statuses, kommo_users
CREATE_TABLES = [
    (
        "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, is_deleted BOOLEAN DEFAULT FALSE"
    ),
    (
        "kommo_statuses",
        "id INT PRIMARY KEY, pipeline_id INT, name TEXT, sort INT, type TEXT"
    ),
    (
        "kommo_users",
        "id BIGINT PRIMARY KEY, name TEXT, email TEXT, role TEXT"
    ),
]

def init_schema(conn):
    cur = conn.cursor()
    for table, cols in CREATE_TABLES:
        cur.execute(
            f"CREATE TABLE IF NOT EXISTS {table} ({cols})"
        )
    conn.commit()
    cur.close()

def fetch_leads_incremental(updated_after: datetime) -> list:
    ts, leads, page = int(updated_after.timestamp()), [], 1
    while True:
        resp = requests.get(f"{KOMMO_BASE}/leads",
                            headers=KOMMO_HDRS,
                            params={"updated_at[from]": ts, "page": page, "limit": 250})
        if resp.status_code == 204:
            break
        resp.raise_for_status()
        batch = resp.json().get("_embedded", {}).get("leads", [])
        if not batch:
            break
        leads.extend(batch)
        page += 1
    return leads

def upsert_leads(conn, leads: list):
    cur = conn.cursor()
    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, is_deleted=FALSE"
    )
    for lead in leads:
        cur.execute(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()

Publishing to Tableau Cloud via Hyper API

With Tableau Cloud — the Hyper API allows publishing a .hyper file directly without a persistent DB connection:

# pip install tableauserverclient pantab
import tableauserverclient as TSC
import pantab

def publish_to_tableau_cloud(df, datasource_name: str):
    # df - pandas DataFrame with data from Postgres
    server = TSC.Server("https://prod-apnortheast-a.online.tableau.com")
    tableau_auth = TSC.PersonalAccessTokenAuth(
        token_name  = "your_pat_name",
        token_value = "your_pat_value",
        site_id     = "your_site_id",
    )
    with server.auth.sign_in(tableau_auth):
        project_id = get_project_id(server, "Sales Analytics")
        hyper_path = f"/tmp/{datasource_name}.hyper"

        # pantab converts DataFrame -> .hyper file
        pantab.frame_to_hyper(df, hyper_path, table="kommo_leads")

        ds      = TSC.DatasourceItem(project_id, name=datasource_name)
        server.datasources.publish(
            ds, hyper_path,
            mode=TSC.Server.PublishMode.Overwrite,
        )
        print(f"Published: {datasource_name}")

def get_project_id(server, project_name: str) -> str:
    all_projects, _ = server.projects.get()
    for p in all_projects:
        if p.name == project_name:
            return p.id
    raise ValueError(f"Project '{project_name}' not found")

Key Metrics for a Tableau Sales Dashboard

After publishing the datasource in Tableau, analysts build dashboards via drag-and-drop. Core metrics from kommo_leads:

  • Win RateCOUNTD(IF status_id = [won_id] THEN id END) / COUNTD(id)
  • Avg Deal SizeAVG(IF status_id = [won_id] THEN price END)
  • Sales Cycle LengthAVG(DATEDIFF('day', created_at, closed_at))
  • Pipeline by Stage — bar chart by status_id, grouped by pipeline_id
  • Revenue by Manager — join with kommo_users, SUM(price) per responsible_user_id
  • Lead VelocityCOUNTD(id) per week with a time filter

Calculated fields in Tableau are created through the UI — no SQL required for the analyst.

Real-World Case

Enterprise distributor (US, 300 employees, Tableau Server + Kommo):

  • Before: an analyst exported a CSV from Kommo weekly (manual export) -> loaded into Tableau. Data was 3–7 days old. 2 hours of work per week.
  • After: Python ETL once per hour -> Postgres -> Tableau PostgreSQL datasource (live connection). Data is less than 1 hour old. 0 manual work.
  • Additionally: Tableau Data-driven Alerts on win rate dropping below 25% -> automatic email to the VP of Sales.

Who This Is Relevant For

  • Enterprise companies with a corporate Tableau license where the BI team builds dashboards for the business
  • Companies where sales analysts do not write SQL and need a drag-and-drop tool
  • Organizations where Kommo data must be combined with other sources (ERP, finance) in a single Tableau dashboard
  • Teams that already have Tableau Server/Cloud and adding CRM data is an extension of the existing ecosystem

Frequently Asked Questions

Tableau live connection vs extract — which to choose for Kommo data?

Live connection: Tableau queries Postgres every time a dashboard is opened. Data is current, slower at large volumes. Extract: Tableau takes a data snapshot on a schedule (hourly/daily), stored in .tde/.hyper format. Faster, data has a delay. For Kommo (typically up to 100k leads) — live connection + materialized views in Postgres for performance.

Tableau Prep Builder vs Python ETL — which is better?

Tableau Prep Builder is Tableau’s visual ETL tool (drag-and-drop transformations). It does not natively support the Kommo API — only SQL sources and files. For Kommo data: Python ETL -> Postgres (a required step), after which Tableau Prep Builder can additionally transform data if complex joins are needed. For simple sales dashboards, Python ETL -> Postgres is sufficient without Prep.

How to configure Tableau Server (not Cloud) with Postgres?

Tableau Server -> Data Sources -> Connect to Data -> PostgreSQL -> enter host/port/db/user/password. If Postgres is on the same server — localhost. If on a separate server — network access is required (TCP 5432). The ODBC PostgreSQL driver is installed on Tableau Server automatically during installation.

Can the Tableau Extract be updated automatically after each ETL run?

Yes. Tableau REST API: POST /api/{version}/sites/{siteId}/datasources/{id}/refreshes initiates an extract refresh. Add to the end of the Python ETL script:

# trigger_tableau_refresh(datasource_id)
resp = requests.post(
    f"{tableau_server}/api/3.15/sites/{site_id}/datasources/{ds_id}/refreshes",
    headers={"x-tableau-auth": token},
    json={},
)

Summary

  • Architecture: Kommo API -> Python ETL (incremental, cron) -> Postgres -> Tableau
  • Two paths into Tableau: PostgreSQL live connection (simpler) or Hyper API (Tableau Cloud)
  • Tableau Cloud -> pantab + tableauserverclient for publishing .hyper files
  • Key tables: kommo_leads, kommo_statuses, kommo_users, kommo_pipelines
  • Tableau Data-driven Alerts — automatic notifications when metrics deviate

If you use Tableau and want to connect Kommo data without weekly CSV exports — describe your data volume and whether you use Tableau Server or Cloud. Exceltic.dev will configure the ETL and published datasource.

More articles

All →