Running Python with Flights
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.
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. 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
- SQL
Connect the MotherDuck MCP Server to your AI assistant, then ask it to create a Flight:
"Create a Flight named
heartbeatthat connects to MotherDuck, createsdocs_playground.heartbeatif 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."
Create a Flight directly with SQL:
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$
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()
$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:
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
);
SELECT *
FROM MD_RUN_FLIGHT(
flight_id := getvariable('heartbeat_flight_id')
);
Inspect the row the Flight wrote:
SELECT
*
FROM
docs_playground.heartbeat
ORDER BY
ts DESC
LIMIT
5;
The SQL above is the same control surface shown in the reference:
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:
- Allocates a Python runtime for the Flight.
- Injects
MOTHERDUCK_TOKENand yourconfigkeys into the environment. - Installs the packages in
requirements.txt. - Executes
main(), capturing stdout and stderr. - 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 — the mental model and where Flights fit alongside SQL, Dives, and external orchestrators.
- MotherDuck MCP Server —
create_flight,run_flight, and the rest of the Flight tool surface for AI agents.