# Running Python with Flights
> Build scheduled Python workflows in MotherDuck for ingest, transformation, sharing, and operational tasks.
A **Flight** is a Python program that runs on MotherDuck, on demand or on a recurring schedule. Use Flights to pull data in from external sources, refresh aggregates, run dbt, scrape a page, or post a scheduled summary to Slack.

Flights complement SQL: where SQL handles transformation against your tables, Flights add everything Python can do (HTTP calls, the full PyPI ecosystem, file processing, custom logic) right next to your data.

:::info
For scheduled or production-like Flights, test with an on-demand run first, use a service account token instead of a personal token, and keep the Flight's database permissions as narrow as the workload allows.
:::

## Anatomy of a Flight

| Field | What it is |
|---|---|
| **Name** | Human-readable identifier shown in the UI and logs. |
| **Source code** | A single-file Python program. The runtime executes it as a plain script, so end it with `if __name__ == "__main__": main()` to invoke your entrypoint. |
| **Requirements** | A `requirements.txt`-style list of pip packages, one per line. |
| **MotherDuck token** | The name of an [access token](/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/). MotherDuck injects the token value into the Flight as the `MOTHERDUCK_TOKEN` environment variable. |
| **Config** | A map of non-sensitive key/value pairs surfaced to the Flight as environment variables (for example, a region or a batch size). |
| **Schedule** | A standard 5-field cron expression in UTC. Omit to make the Flight on-demand only. |

Each edit to source code, requirements, config, or the token produces a fresh version. Renaming a Flight or changing its schedule is a metadata-only update and does not produce one.

## Create your first Flight

### MCP / AI agent

Connect the [MotherDuck MCP Server](/sql-reference/mcp/) to your AI assistant, then ask it to create a Flight:

> "Create a Flight named `heartbeat` that connects to MotherDuck, creates `docs_playground.heartbeat` if it doesn't exist, inserts one row, and prints how many rows it wrote. Use the latest MotherDuck-supported DuckDB version in the requirements."

The agent will call `create_flight` with the right `source_code` and `requirements_txt`. It can then call `run_flight` to trigger an immediate run and `get_flight_run_logs` to read the output back into the conversation.

You can iterate with the agent the same way you would on a script: "add a config value for the table name and use it instead of hard-coding `heartbeat`."

### SQL

Create a Flight directly with SQL:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Create the heartbeat Flight"
  formatOnLoad={false}
  collapseDollarQuotedLiterals={true}
  query={`SELECT flight_id, flight_name, current_version
FROM MD_CREATE_FLIGHT(
    name := 'heartbeat_docs_demo',
    requirements_txt := array_to_string([
        'duckdb==1.5.2'
    ], chr(10)),
    source_code := $flight$

def main():
    con = duckdb.connect("md:")
    con.execute("""
        CREATE TABLE IF NOT EXISTS docs_playground.heartbeat (
            ts TIMESTAMP DEFAULT current_timestamp,
            message VARCHAR
        )
    """)
    con.execute("INSERT INTO docs_playground.heartbeat (message) VALUES ('hello from a flight')")
    print("wrote 1 row to docs_playground.heartbeat")

if __name__ == "__main__":
    main()
$flight$
);`} />

Trigger an on-demand run. The `MD_*` Flight table functions only accept literal parameters, not subqueries, so store the Flight ID in a SQL variable first:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Set the heartbeat Flight ID"
  formatOnLoad={false}
  query={`SET VARIABLE heartbeat_flight_id = (
    SELECT flight_id
    FROM MD_LIST_FLIGHTS()
    WHERE flight_name = 'heartbeat_docs_demo'
    ORDER BY created_at DESC
    LIMIT 1
);`} />

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

Inspect the row the Flight wrote:

<MotherDuckSQLEditor
  database="docs_playground"
  title="Read the heartbeat table"
  query={`SELECT *
FROM docs_playground.heartbeat
ORDER BY ts DESC
LIMIT 5;`} />

The SQL above is the same control surface shown in the reference:

```sql
SELECT flight_id, flight_name, current_version
FROM MD_CREATE_FLIGHT(
    name := 'heartbeat',
    source_code := $$
import duckdb

def main():
    con = duckdb.connect("md:")
    con.execute("""
        CREATE TABLE IF NOT EXISTS docs_playground.heartbeat (
            ts TIMESTAMP DEFAULT current_timestamp,
            message VARCHAR
        )
    """)
    con.execute("INSERT INTO docs_playground.heartbeat (message) VALUES ('hello from a flight')")
    print("wrote 1 row to docs_playground.heartbeat")

if __name__ == "__main__":
    main()
$$,
    requirements_txt := array_to_string([
        'duckdb==1.5.2'
    ], chr(10))
);
```

## What happens when a Flight runs

When you trigger a run (manually or on schedule), MotherDuck:

1. Allocates a Python runtime for the Flight.
2. Injects `MOTHERDUCK_TOKEN` and your `config` keys into the environment.
3. Installs the packages in `requirements.txt`.
4. Executes `main()`, capturing stdout and stderr.
5. Records the run's status and logs.

Runs are asynchronous. A new run starts in `RUN_STATUS_PENDING`, moves to `RUN_STATUS_RUNNING`, and ends in one of `RUN_STATUS_SUCCEEDED`, `RUN_STATUS_FAILED`, or `RUN_STATUS_CANCELLED`. Poll for completion with `list_flight_runs` (MCP) or `MD_LIST_FLIGHT_RUNS` (SQL).

## Versioning

Every edit to a Flight's content fields (`source_code`, `requirements_txt`, `config`, or the access token) produces a fresh version. Renames and schedule changes do not.

When a run starts, it locks to the version that was current at that moment. If you update the Flight while a run is in progress, that run finishes against the version it started with; only the next run picks up the updated source.

You can browse versions in the MotherDuck UI or read them programmatically with `list_flight_versions` and `get_flight` (passing a specific version number).

## Related resources

- [Flights concept](/concepts/flights) — the mental model and where Flights fit alongside SQL, Dives, and external orchestrators.
- [MotherDuck MCP Server](/sql-reference/mcp/) — `create_flight`, `run_flight`, and the rest of the Flight tool surface for AI agents.


---

## 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/",
  "page_title": "Running Python with Flights",
  "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.
