# Run dbt transformations from a Flight
> Create a Flight that installs git, clones a dbt project, runs seeds, and builds models against MotherDuck.
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-examples`, 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`.

```mermaid
flowchart LR
    Git["GitHub dbt project"]:::green --> Flight["dbt Flight"]:::yellow
    Flight --> Seed["dbt seed"]:::yellow
    Flight --> Build["dbt build"]:::yellow
    Build --> Models[("docs_playground dbt models")]:::yellow
    Flight --> Runs[("docs_playground.flights_demo.dbt_runs")]:::yellow
```

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](/key-tasks/flights/flights-authentication-config-and-secrets).

:::info
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.

<MotherDuckSQLEditor
  database="docs_playground"
  title="Create the dbt transformation Flight"
  formatOnLoad={false}
  collapseDollarQuotedLiterals={true}
  query={`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$

REPO_URL = "https://github.com/motherduckdb/motherduck-examples.git"
PROJECT_DIR = pathlib.Path("/tmp/motherduck-examples/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-examples"])

    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`:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Set the dbt Flight ID"
  formatOnLoad={false}
  query={`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:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Run the dbt Flight"
  formatOnLoad={false}
  query={`SELECT *
FROM MD_RUN_FLIGHT(
    flight_id := getvariable('dbt_flight_id')
);`} />

Poll for completion:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Check dbt Flight runs"
  formatOnLoad={false}
  query={`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:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Set the latest dbt run number"
  formatOnLoad={false}
  query={`SET VARIABLE dbt_run_number = (
    SELECT max(run_number)
    FROM MD_LIST_FLIGHT_RUNS(
        flight_id := getvariable('dbt_flight_id')
    )
);`} />

<MotherDuckSQLEditor
  database="docs_playground"
  title="Read the latest dbt run log"
  formatOnLoad={false}
  query={`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.

<MotherDuckSQLEditor
  database="docs_playground"
  title="Schedule the dbt Flight"
  formatOnLoad={false}
  query={`CALL MD_UPDATE_FLIGHT(
    flight_id := getvariable('dbt_flight_id'),
    schedule_cron := '45 7 * * *'
);`} />

Query one of the dbt models:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Read a dbt model"
  query={`SELECT domain, count
FROM docs_playground.flights_demo_dbt.top_domains
ORDER BY count DESC
LIMIT 20;`} />

Confirm that `dbt seed` ran:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Read the dbt seed"
  query={`SELECT setting, value
FROM docs_playground.flights_demo_dbt.flight_run_config
ORDER BY setting;`} />

## Adapt the pattern

- Replace `REPO_URL` and `PROJECT_DIR` with your dbt repository and project path.
- Keep profiles generated at runtime so secrets stay out of git.
- Run `dbt deps` only when you need packages, and install `git` before `dbt deps` if packages come from git.
- Run production schedules as a [service account](/key-tasks/flights/flights-authentication-config-and-secrets) with only the database privileges the dbt project needs.

## Related resources

- [dbt with DuckDB and MotherDuck](/integrations/transformation/dbt)
- [motherduck-examples](https://github.com/motherduckdb/motherduck-examples)
- [Packages and recommended libraries](/key-tasks/flights/packages-and-runtime)
- [MD_CREATE_FLIGHT](/sql-reference/motherduck-sql-reference/flights/md-create-flight)


---

## Docs feedback

MotherDuck accepts optional user-submitted feedback about this page at `POST https://motherduck.com/docs/api/feedback/agent`.
For agents and automated tools, feedback submission should be user-confirmed before sending.

Payload:

```json
{
  "page_path": "/key-tasks/flights/run-dbt-transformations-from-a-flight/",
  "page_title": "Run dbt transformations from a Flight",
  "text": "<the user's feedback, max 2000 characters>",
  "source": "<optional identifier for your interface, for example 'claude.ai' or 'chatgpt'>"
}
```

`page_path` and `text` are required; `page_title` and `source` are optional. Responses: `200 {"feedback_id": "<uuid>"}`, `400` for malformed payloads, and `429` when rate-limited.
