Custom Integrations

When the Built-In Connectors Are Not Enough

  • DataHub ships with 80+ ingestion connectors: Snowflake, BigQuery, dbt, Airflow, Looker, and many more
  • But most organizations also have internal systems that are not covered
  • Examples: a homegrown key-value config store, a proprietary ETL framework, an internal ML feature store, a CI/CD pipeline that creates derived datasets
  • Without a custom integration, those systems are invisible to your data catalog
  • This module teaches you how to make any system visible to DataHub in 30 lines of Python
DataHub 70+ built-in connectors Snowflake, BigQuery, dbt, Airflow... Config store ETL framework ML feature store CI/CD pipeline

Four Reasons to Build a Custom Source

  • Internal system not covered by built-in connectors — homegrown config store, proprietary ETL engine
  • Emit metadata from a CI/CD pipeline step — mark a dataset "built from branch X" at deploy time
  • Build an internal tool that creates DataHub assets programmatically — a self-service table registration UI for data engineers
  • Emit lineage from a custom pipeline that is not Airflow or dbt — a Spark job that reads from S3 and writes to Redshift
  • You do not need to build a full structured connector for all of these
  • For scripts and one-off jobs: the DataHub REST emitter is enough
  • For reusable connectors: the Source base class is the right structure
Internal system CI/CD metadata Programmatic asset creation Custom lineage Script + REST emitter quick, one-off Source base class reusable, recipe YAML

The DataHub Python SDK: Two Ways to Emit

  • Install: pip install acryl-datahub
  • Option 1: DatahubRestEmitter (production) — connects to DataHub's REST API, emits MetadataChangeProposals (MCPs) directly to GMS
  • Use this for scripts, pipelines, and tools that run against a live DataHub instance
  • Option 2: SynchronizedFileEmitter (debugging) — writes MCPs to a local JSON file instead of a live server; inspect the file, then switch to the REST emitter once the output looks right
DataHub Python SDK DatahubRestEmitter → GMS (production) SynchronizedFileEmitter → local file (debugging)

MCPs: The Unit of Metadata Change

  • MCP = MetadataChangeProposal
  • An MCP says exactly one thing: "for this entity URN, set this aspect to this value"
  • entityUrn — the unique identifier for the entity being updated
  • entityType — the type of entity: dataset, dashboard, dataJob, etc.
  • aspectName — which aspect you are updating: schemaMetadata, datasetProperties, ownership, etc.
  • aspect — the new value of that aspect
  • changeType — the operation: UPSERT (create or update) or DELETE
MCP entityUrn entityType aspectName aspect changeType UPSERT / DELETE

URNs: How DataHub Identifies Every Entity

  • Recall from Core Concepts: every entity has exactly one URN and it never changes
  • As a developer, you will construct and read URNs directly
  • Dataset: urn:li:dataset:(urn:li:dataPlatform:PLATFORM,TABLE_NAME,ENV)
  • Dashboard: urn:li:dashboard:(PLATFORM,DASHBOARD_ID)
  • User: urn:li:corpuser:email@company.com
  • Real examples: urn:li:dataset:(urn:li:dataPlatform:snowflake,PROD.FINANCE.FACT_REVENUE_DAILY,PROD), urn:li:dataset:(urn:li:dataPlatform:internaldb,mydb.users,PROD)
  • Tip: copy URNs directly from the browser URL bar on any entity page
entity type platform name env urn:li:dataset:(urn:li:dataPlatform:PLATFORM,TABLE_NAME,ENV) Browser address bar Copy URN here

Map Your Entities Before Writing Any Code

  • "Before you write a single line of Python, map your source system's objects to DataHub entity types"
  • Table, view, file → dataset
  • BI report or chart → chart or dashboard
  • ETL pipeline or job → dataJob
  • Feature store feature group → mlFeatureTable
  • Common mistake: using the wrong entity type (e.g. dashboard for a table) creates an entity that looks broken in the UI and is hard to fix after 500 entities have been ingested
Source object DataHub entity type Table, view, file dataset BI report or chart chart or dashboard ETL pipeline or job dataJob Feature store group mlFeatureTable

Emitting Lineage: Connecting Your Assets to the Broader Graph

UpstreamLineage declares that one dataset is derived from another.

from datahub.emitter.mcp import MetadataChangeProposalWrapper
from datahub.emitter.rest_emitter import DatahubRestEmitter
from datahub.metadata.schema_classes import (
    UpstreamLineageClass, UpstreamClass, DatasetLineageTypeClass
)

upstream_urn = "urn:li:dataset:(urn:li:dataPlatform:snowflake,PROD.CORE.raw_events,PROD)"
downstream_urn = "urn:li:dataset:(urn:li:dataPlatform:internaldb,mydb.aggregated_metrics,PROD)"

lineage_mcp = MetadataChangeProposalWrapper(
    entityUrn=downstream_urn,
    aspect=UpstreamLineageClass(
        upstreams=[UpstreamClass(dataset=upstream_urn, type=DatasetLineageTypeClass.TRANSFORMED)]
    ),
)

emitter = DatahubRestEmitter("http://localhost:8080")
emitter.emit(lineage_mcp)
snowflake PROD.CORE.raw_events internaldb mydb.aggregated_metrics UpstreamLineageClass type: TRANSFORMED

MCEs Are the Old Format. Use MCPs.

  • "Always use MetadataChangeProposals (MCPs) for new integrations. Never use MCEs."
  • MCP (MetadataChangeProposal): current standard
  • MCE (MetadataChangeEvent): legacy
  • Why MCEs still exist: old connectors and documentation may still reference them; they still work for backward compatibility; some blog posts and StackOverflow answers show MCE examples from 2021–2022
✔ MCP (current) New features: fine-grained lineage, structured properties ⚠ MCE (legacy) Backward compatibility only

Validate Your Connection Before Emitting Anything

  • "Run datahub check server-config before starting any ingestion script"
  • Uses the host and token already configured via datahub init (or the DATAHUB_GMS_URL / DATAHUB_GMS_TOKEN env vars)
  • Validates that the DataHub GMS endpoint is reachable
  • Confirms your authentication token is valid
  • Fails fast before you emit a single entity
Brief: Screenshot of a terminal window running datahub check server-config with the server config printed to output, confirming GMS is reachable and the token is valid.

Structured Ingestion Sources: When to Go Further

  • Two levels of custom integration: a script with DatahubRestEmitter (one-off jobs, CI/CD steps, quick scripts), or the Source base class (a reusable connector teams configure with a recipe YAML)
  • When to use the Source base class: multiple teams or projects will run this connector
  • You want configuration validation at recipe parse time
  • You need state management (e.g. only ingest entities modified since the last run)
Script + DatahubRestEmitter one-off jobs, CI/CD steps, quick scripts Source base class reusable, recipe YAML, state management

Build a Custom Ingestion Script for InternalDB

  • Scenario: InternalDB is a mock internal system with 5 tables. DataHub has no connector for it. You will build the ingestion script.
  • internaldb.users (user_id, email, created_at)
  • internaldb.sessions (session_id, user_id, started_at, duration_seconds)
  • internaldb.events (event_id, session_id, event_type, occurred_at)
  • internaldb.aggregated_metrics (metric_name, metric_value, computed_at) — downstream of snowflake.PROD.CORE.raw_events
  • internaldb.revenue_summary (period, revenue, region) — downstream of snowflake.PROD.FINANCE.raw_revenue
Brief: Screenshot of the lab handout card listing the 5 InternalDB tables and their columns, as distributed to participants at their table.

Check for Understanding

  • You want to emit metadata from a nightly batch job that runs in your internal ETL framework. The job reads from an S3 bucket and writes to a Redshift table. Which DataHub entity types would you create, and which aspect would you use to connect them?
  • Your colleague finds a blog post from 2021 showing how to emit metadata using MetadataChangeEvent. Should they follow that example? Why or why not?
  • Your script runs without any Python errors, but when you search DataHub the tables are not there. What is the first thing you check?
1 Entity types + lineage aspect 2 MCE vs. MCP 3 Silent failure first check

Summary + What's Next

  • When to build a custom integration: internal systems, CI/CD pipelines, custom lineage, programmatic asset creation
  • The DataHub Python SDK: DatahubRestEmitter for production, file emitter for debugging
  • MCPs: the unit of metadata change, with fields entityUrn, entityType, aspectName, and aspect
  • URN format for datasets: urn:li:dataset:(urn:li:dataPlatform:PLATFORM,TABLE_NAME,ENV)
  • Emitting lineage with UpstreamLineageClass
  • When to use a plain script vs. the Source base class
✔ When to build a custom integration ✔ Python SDK: REST emitter vs. file emitter ✔ MCPs: the unit of metadata change ✔ URN format for datasets ✔ Emitting lineage with UpstreamLineageClass ✔ Script vs. the Source base class

Thank you!