← writing
17 June 2025

dbt-fabric called from a Fabric notebook

An integrated platform does not always offer a place to put a dbt project. dbt’s Python entry point provides one, provided you accept what is unusual about the arrangement.

On an integrated data platform, the warehouse exposes a SQL endpoint and the matching dbt adapter exists. One dull, blocking question remains: where to run the command. There is no machine to drop a project on and run a command at five in the morning; there are notebooks and a scheduler able to trigger them. The answer is therefore a library rather than an infrastructure.

01dbt as a library

It is easily forgotten that dbt can be called from Python. `dbtRunner` runs the same commands as the command line and returns an object rather than an exit code: the list of models, their status, their duration. In a notebook that changes everything: there is no text output left to parse to find out what failed.

python
from dbt.cli.main import dbtRunner, dbtRunnerResult

# The project is mounted from the lake: it stays versioned elsewhere, and the
# notebook holds no copy of it that could drift.
PROJECT = "/lakehouse/default/Files/dbt/warehouse"

runner = dbtRunner()
result: dbtRunnerResult = runner.invoke([
    "build",                       # run + test, in graph order
    "--project-dir", PROJECT,
    "--profiles-dir", PROJECT,
    "--target", "prod",
])

if not result.success:
    # No log to re-read: each node carries its own status and message.
    failures = [
        f"{r.node.name} — {r.message}"
        for r in result.result
        if r.status in ("error", "fail")
    ]
    raise RuntimeError("dbt failed:
" + "
".join(failures))
02The profile, written rather than deployed

A notebook has no stable home directory to drop a profile file in. It is therefore written at the start of the run, from secrets read out of the platform’s vault, which has the advantage of guaranteeing no credential sleeps in the repository, and the drawback of making the run silent if the vault is misconfigured. An acceptable trade-off, provided it is said in the code rather than left to be guessed.

yaml
warehouse:
  target: prod
  outputs:
    prod:
      type: fabric
      driver: "ODBC Driver 18 for SQL Server"
      server: "<sql-endpoint>.datawarehouse.fabric.microsoft.com"
      database: warehouse
      schema: marts
      # Service principal authentication: the notebook does not borrow the
      # identity of whoever started it, otherwise the scheduled run would
      # depend on one person.
      authentication: ServicePrincipal
      tenant_id: "{{ env_var('TENANT_ID') }}"
      client_id: "{{ env_var('CLIENT_ID') }}"
      client_secret: "{{ env_var('CLIENT_SECRET') }}"
      threads: 4
03What this arrangement does not replace

The platform’s scheduler triggers the notebook, and that is all it can do: it sees one task where there are two hundred models. It is exactly the flaw described in another piece, except that here there is no alternative: the platform exposes no mechanism for making the graph visible. What can be done is to make it legible afterwards: the results returned by the entry point are written to a table, and a report built on it says which model failed and since when.

04What I leave out

I do not compare this with the platform’s native pipelines, which do part of the same thing without dbt. The choice turns on a criterion technique does not supply: if the team already knows dbt and practises it elsewhere, keeping it beats learning one more tool; if it does not, introducing it for a single warehouse is a debt taken on without saying so. And I say nothing about running dbt tests in production, which we did at the same time as the build: convenient and arguable, since a wrong table is then published before being declared wrong.