Run dbt transformations from a Flight
You have a dbt project and want it to run close to your MotherDuck data without maintaining a separate scheduler. In this guide, a Flight installs git, clones the dbt-ingestion-s3 example from motherduck-cookbook, writes a runtime profile that uses the injected MotherDuck token, runs dbt seed, runs dbt build, and records the run in docs_playground.flights_demo.dbt_runs.
The demo builds models into docs_playground and keeps a small run ledger in your own MotherDuck account. The clone-and-install flow keeps the example self-contained; for production, keep per-run setup as small as possible.
Before you start
The Flight runtime authenticates to MotherDuck for you and injects the credential as MOTHERDUCK_TOKEN, which the generated dbt profile reads through env_var(). To run a scheduled Flight as a service account instead, see Authentication, config, and secrets.
This guide installs Debian git and clones a public GitHub repository at the start of each run. For a production Flight, package the project source closer to the runtime or keep the setup step narrow so most run time goes to dbt work instead of environment preparation.
The example writes a profiles.yml file at runtime with MOTHERDUCK_TOKEN referenced through env_var(). Do not paste a MotherDuck token value into dbt profiles, project files, or Flight source.
Create the Flight
Create the Flight on demand first, then add cron after the first dbt build succeeds.
SELECT flight_id, flight_name, current_version
FROM MD_CREATE_FLIGHT(
name := 'docs_dbt_transform',
requirements_txt := array_to_string([
'duckdb==1.5.3',
'dbt-duckdb==1.10.1'
], chr(10)),
source_code := $flight$
import os
import pathlib
import subprocess
import textwrap
import duckdb
REPO_URL = "https://github.com/motherduckdb/motherduck-cookbook.git"
PROJECT_DIR = pathlib.Path("/tmp/motherduck-cookbook/dbt-ingestion-s3")
def run(command, cwd=None):
print("$ " + " ".join(command))
subprocess.run(command, cwd=cwd, check=True)
def main():
os.environ.setdefault("HOME", "/tmp")
run(["apt-get", "update"])
run(["apt-get", "install", "-y", "git"])
if not PROJECT_DIR.exists():
run(["git", "clone", "--depth", "1", REPO_URL, "/tmp/motherduck-cookbook"])
profiles_yml = PROJECT_DIR / "profiles.yml"
profiles_yml.write_text(textwrap.dedent("""
dbt_ingestion_s3:
outputs:
flight:
type: duckdb
path: "md:docs_playground?motherduck_token={{ env_var('MOTHERDUCK_TOKEN') }}"
schema: flights_demo_dbt
threads: 1
target: flight
""").strip() + "\n")
seed_file = PROJECT_DIR / "seeds" / "flight_run_config.csv"
seed_file.write_text("setting,value\nrunner,flight\nwarehouse,docs_playground\n")
run(["dbt", "deps", "--profiles-dir", "."], cwd=PROJECT_DIR)
run(["dbt", "seed", "--target", "flight", "--profiles-dir", "."], cwd=PROJECT_DIR)
run(["dbt", "build", "--target", "flight", "--profiles-dir", "."], cwd=PROJECT_DIR)
con = duckdb.connect("md:")
con.execute("CREATE SCHEMA IF NOT EXISTS docs_playground.flights_demo")
con.execute("""
CREATE TABLE IF NOT EXISTS docs_playground.flights_demo.dbt_runs (
run_at TIMESTAMPTZ,
repo_url VARCHAR,
project_path VARCHAR,
target_schema VARCHAR
)
""")
con.execute(
"""
INSERT INTO docs_playground.flights_demo.dbt_runs
VALUES (current_timestamp, ?, ?, ?)
""",
[REPO_URL, str(PROJECT_DIR), "flights_demo_dbt"],
)
print("dbt build completed")
if __name__ == "__main__":
main()
$flight$
);Run and inspect it
The MD_* Flight table functions only accept literal parameters, not subqueries or lateral join columns, so store the Flight ID in a SQL variable first. The next cells reuse it through getvariable:
SET VARIABLE dbt_flight_id = ( SELECT flight_id FROM MD_LIST_FLIGHTS() WHERE flight_name = 'docs_dbt_transform' ORDER BY created_at DESC LIMIT 1 );
Trigger a manual run:
SELECT *
FROM MD_RUN_FLIGHT(
flight_id := getvariable('dbt_flight_id')
);Poll for completion:
SELECT run_number, status, flight_version, created_at
FROM MD_LIST_FLIGHT_RUNS(
flight_id := getvariable('dbt_flight_id')
)
ORDER BY run_number DESC
LIMIT 5;Read the run log if dbt fails. Store the latest run number in a variable first:
SET VARIABLE dbt_run_number = (
SELECT max(run_number)
FROM MD_LIST_FLIGHT_RUNS(
flight_id := getvariable('dbt_flight_id')
)
);
SELECT logs
FROM MD_GET_FLIGHT_LOGS(
flight_id := getvariable('dbt_flight_id'),
run_number := getvariable('dbt_run_number')
);Schedule the dbt build
After the manual run succeeds, add a daily 07:45 UTC schedule. Schedule updates are metadata-only; they do not create a new Flight version.
CALL MD_UPDATE_FLIGHT(
flight_id := getvariable('dbt_flight_id'),
schedule_cron := '45 7 * * *'
);Query one of the dbt models:
SELECT domain, count FROM docs_playground.flights_demo_dbt.top_domains ORDER BY count DESC LIMIT 20;
Confirm that dbt seed ran:
SELECT setting, value FROM docs_playground.flights_demo_dbt.flight_run_config ORDER BY setting;
Adapt the pattern
- Replace
REPO_URLandPROJECT_DIRwith your dbt repository and project path. - Keep profiles generated at runtime so secrets stay out of git.
- Run
dbt depsonly when you need packages, and installgitbeforedbt depsif packages come from git. - Run production schedules as a service account with only the database privileges the dbt project needs.