export AIRFLOW_CONN_DATABRICKS_DEFAULT='databricks://@dbc-xxxx.cloud.databricks.com?token=<url-encoded-token>'
Module 28: Run the Project with Airflow
Trigger the same job from Airflow. Assumes you know Airflow. Needs API access to the workspace, still to be verified on Free Edition.
Topics in this module
- Airflow or Lakeflow Jobs: Choosing
- Connecting Airflow to Databricks
- Installing the Databricks Provider
- Triggering the Job with DatabricksRunNowOperator
- One-Time Runs with DatabricksSubmitRunOperator
- Databricks Workflows in an Airflow DAG
- Parameters and Reading Results
- Retries, Timeouts and Failure Handling
- Monitoring from Both UIs
- Keeping the Job Definition in One Place
Trigger the orders-pipeline job from Airflow. You know Airflow: this module is only the Databricks side. The job itself (Module 27) doesn't change.
- Needs the deployed job from Module 27 (UI-built or from the bundle)
- Airflow runs outside Databricks: on your machine, in Docker, or in your company's Airflow
- Provider facts below come from the Airflow Databricks provider docs (provider 7.x needs Airflow 2.11+). Import paths are shown for Airflow 2.11: Airflow 3 moved some, so follow your version's docs
- Free Edition: Airflow must call your workspace's REST API with a token or service principal. That isn't documented for Free Edition (Module 15). Check Settings → Developer → Access tokens. If you can't create one, read this module as reference
Check this first
No token, no Airflow connection. In that case, Module 27 (schedules and triggers in Databricks) is your orchestration on Free Edition.
Airflow or Lakeflow Jobs: Choosing
By the end: decide whether Airflow or Lakeflow Jobs should orchestrate.
| Situation | Better choice |
|---|---|
| Everything runs on Databricks | Lakeflow Jobs: nothing extra to host |
| Your company already runs Airflow, and pipelines span several systems | Airflow triggers the Databricks job |
| Need file-arrival or table-update triggers | Lakeflow Jobs |
| Need repair runs and backfills with one click | Lakeflow Jobs (Airflow can also backfill) |
| Dependencies on non-Databricks work (warehouse load, API call, email) | Airflow |
- Best split: Airflow owns scheduling and cross-system order. Databricks owns the task graph and compute inside the job
- Avoid orchestrating the same job from two places: pick one (topic 10)
- Taught in Module 10 and 27
Connecting Airflow to Databricks
By the end: connect Airflow to your Databricks workspace.
| Field | Value |
|---|---|
| Connection id | databricks_default (the operators' default) |
| Connection type | Databricks |
| Host | Your workspace URL, e.g. https://dbc-xxxx.cloud.databricks.com |
| Password | Personal access token |
| Login | Leave empty (token sent as a Bearer header) |
Production: a Databricks-managed service principal with OAuth
| Field | Value |
|---|---|
| Login | Service principal id |
| Password | Its OAuth secret |
| Extra | {"service_principal_oauth": true} |
- URL-encode every part of the connection URI
- Keep the token in Airflow's secrets backend, not in the DAG repo
- A personal token is the same as you: when you leave, the DAGs fail. Use a service principal for anything shared
- Give the principal only what it needs: Can manage run on this job
- Create tokens under Settings → Developer (Free Edition: may be unavailable)
Installing the Databricks Provider
By the end: install the provider and check the connection works.
pip install apache-airflow-providers-databricks
# optional: Azure identity support
pip install "apache-airflow-providers-databricks[azure-identity]"
# dags/check_databricks.py
from datetime import datetime
from airflow import DAG
from airflow.decorators import task
from airflow.providers.databricks.hooks.databricks import DatabricksHook
with DAG("check_databricks", start_date=datetime(2025, 1, 1), schedule=None, catchup=False):
@task
def ping():
hook = DatabricksHook(databricks_conn_id="databricks_default")
for job in hook.list_jobs(limit=5): # a harmless read-only call
print(job.get("settings", {}).get("name"))
ping()
- Install it in the same environment as the scheduler and workers, then restart them
- Run it once (
airflow dags test check_databricks) to prove host and token are right. Iflist_jobsis missing in your provider version, any read-only hook call works - A
401/403= token or permission. A connection timeout = network or host URL - If your Airflow runs in a corporate network, the workspace URL must be reachable: allow-list it
- Don't put the token in DAG code: use the connection
Triggering the Job with DatabricksRunNowOperator
By the end: trigger the existing job with `DatabricksRunNowOperator`.
# dags/orders_pipeline.py
from datetime import datetime, timedelta
from airflow import DAG
from airflow.providers.databricks.operators.databricks import DatabricksRunNowOperator
with DAG(
dag_id="orders_pipeline",
start_date=datetime(2025, 1, 1),
schedule="30 6 * * *",
catchup=False,
default_args={"retries": 1, "retry_delay": timedelta(minutes=5)},
) as dag:
run_job = DatabricksRunNowOperator(
task_id="run_orders_job",
databricks_conn_id="databricks_default",
job_name="orders-pipeline-prod", # or job_id=123456789
job_parameters={"run_date": "{{ ds }}"},
)
| Parameter | Does |
|---|---|
job_id or job_name |
Which job. A name fails if none or several jobs match |
job_parameters |
A plain dict[str, str] that overrides the job's parameters for this run |
wait_for_termination |
Default True: the Airflow task waits and succeeds or fails with the run |
polling_period_seconds |
How often to check the run (default 30) |
deferrable |
Free the worker slot while waiting (needs a triggerer) |
{{ ds }}is the Airflow logical date,YYYY-MM-DD: matches the job'srun_dateparameter, so backfills in Airflow rerun the right dayjob_parameterscan't be combined withnotebook_params,python_paramsand similar: use one stylejob_idis shown in the job's URL and Job details panel- The operator pushes
run_idandrun_page_urlto XCom
One-Time Runs with DatabricksSubmitRunOperator
By the end: submit a one-off run with `DatabricksSubmitRunOperator`.
from airflow.providers.databricks.operators.databricks import DatabricksSubmitRunOperator
one_off = DatabricksSubmitRunOperator(
task_id="adhoc_gold_rebuild",
run_name="orders-gold-rebuild-{{ ds }}",
tasks=[{
"task_key": "build_gold",
"python_wheel_task": {
"package_name": "orders_pipeline",
"entry_point": "build_gold",
"named_parameters": {"catalog": "<catalog>", "run_date": "{{ ds }}", "days": "30"},
},
"environment_key": "default",
}],
environments=[{
"environment_key": "default",
"spec": {"environment_version": "3",
"dependencies": ["/Volumes/<catalog>/dbx_project/artifacts/orders_pipeline-0.1.0-py3-none-any.whl"]},
}],
)
RunNow |
SubmitRun |
|
|---|---|---|
| Needs a saved job | Yes | No |
| Appears in Jobs UI as a job | Yes | Only as a one-time run |
| Where the definition lives | Databricks (or your bundle) | The DAG file |
| Good for | Recurring pipelines | Ad hoc or one-off work |
- You can pass a payload as
json, as named arguments, or astasks. If both are given, named arguments win - The task shape is the Jobs API
runs/submitformat: check its reference for the current fields, including serverlessenvironments - Don't copy a recurring job into DAG code (topic 10)
Databricks Workflows in an Airflow DAG
By the end: run several Databricks tasks as one workflow from a DAG.
from airflow.providers.databricks.operators.databricks_workflow import DatabricksWorkflowTaskGroup
from airflow.providers.databricks.operators.databricks import (
DatabricksNotebookOperator, DatabricksTaskOperator,
)
with DAG("orders_workflow", start_date=datetime(2025, 1, 1), schedule=None, catchup=False) as dag:
group = DatabricksWorkflowTaskGroup(
group_id="orders",
databricks_conn_id="databricks_default",
job_clusters=job_cluster_spec, # shared cluster definition
notebook_params={"run_date": "{{ ds }}"}, # put parameters here, not on each task
)
with group:
ingest = DatabricksNotebookOperator(
task_id="ingest", databricks_conn_id="databricks_default",
notebook_path="/Shared/ingest", source="WORKSPACE",
job_cluster_key="Shared_job_cluster",
)
report = DatabricksTaskOperator(
task_id="report", databricks_conn_id="databricks_default",
task_config={"sql_task": {"query": {"query_id": "<QUERY_ID>"},
"warehouse_id": "<WAREHOUSE_ID>"}},
)
ingest >> report
- The group creates or updates a Databricks job named
<dag_name>.<group_id>from your DAG, then runs it - Gives you one Databricks run for several Airflow tasks. Task groups, cross-DAG triggers and workflow-level parameters are available through Airflow
- Parameters are passed at trigger time and don't change the job definition
- Notebook, SQL and similar tasks are supported by these operators: check whether wheel tasks and serverless work in your provider version before relying on them
- For this project, prefer
RunNow: the job already exists in your bundle
Free Edition
The docs' example uses job clusters. Free Edition is serverless-only, so treat this as a paid-workspace pattern.
Parameters and Reading Results
By the end: pass parameters in and read results back.
In: Airflow templates go into job_parameters:
run_job = DatabricksRunNowOperator(
task_id="run_orders_job",
job_name="orders-pipeline-prod",
job_parameters={
"run_date": "{{ ds }}",
"days": "{{ var.value.get('orders_days', '7') }}",
},
)
Out: three ways to get something back.
| Need | How |
|---|---|
| Link to the run | XCom run_page_url from the operator |
| Run id for logs | XCom run_id |
| The data result (row counts, status) | Read it from a table with a SQL check task after the run |
from airflow.decorators import task
@task
def log_run(ti=None):
run_id = ti.xcom_pull(task_ids="run_orders_job", key="run_id")
url = ti.xcom_pull(task_ids="run_orders_job", key="run_page_url")
print(f"databricks run_id={run_id} url={url}")
run_job >> log_run()
- Values are strings. Convert in the Databricks entry point (
argparse type=int, Module 26.4) - Wheel tasks don't return values. Write results to a table like
ops_run_logand check it with a SQL task (DatabricksSqlOperatorneeds a SQL warehousehttp_pathin the connection extras) - Keep the contract small: date and a few settings. Not whole datasets
Retries, Timeouts and Failure Handling
By the end: combine Airflow retries with Databricks retries safely.
| Layer | Setting | Covers |
|---|---|---|
| Databricks task | max_retries, timeout |
A flaky task inside the job |
| Operator | databricks_retry_limit, databricks_retry_delay |
Failed API calls to Databricks (not a failed run) |
| Airflow task | retries, retry_delay, execution_timeout |
The whole RunNow step |
| Airflow DAG | on_failure_callback, email, SLA tools |
Telling someone |
- Retries multiply. 2 job retries × 2 Airflow retries = up to 9 attempts of a task. Retry in one layer: Databricks for task-level flakiness, Airflow for "run the whole job again"
RunNowhasrepair_run: on a retry, repair the failed run instead of starting a new one. Pair it with idempotent tasks (Module 27.7)cancel_previous_runsstops a still-running run before a new one: use it only if you want the old run killed- Set
execution_timeouton the Airflow task so a stuck run doesn't hold a slot forever - When an Airflow task is cancelled or times out, the operator tries to cancel the Databricks run: check on the Databricks side that it stopped
run_job = DatabricksRunNowOperator(
task_id="run_orders_job", job_name="orders-pipeline-prod",
job_parameters={"run_date": "{{ ds }}"},
execution_timeout=timedelta(hours=1),
retries=1, repair_run=True,
)
Monitoring from Both UIs
By the end: follow one run through both UIs.
| Question | Where |
|---|---|
| Did the DAG run? | Airflow Grid / Graph |
| Why did the Databricks task fail? | The Databricks run page (URL printed in the Airflow task log) |
| What was the input? | Both: the run_date appears in Airflow's rendered template and in the job's run parameters |
| How long did each task take? | Databricks run page, per task |
| Which tasks reran? | Databricks matrix view (repairs show as extra columns) |
- Start at Airflow to see whether something failed. Go to Databricks to see why
- Log
run_idandrun_page_url(topic 7) so the link is always one click away - Put one alert in one place: Airflow for "the pipeline is late", Databricks for "a task failed". Both is double paging
- Keep
ops_run_logas the cross-tool record (Module 16) - Mismatched dates are the classic bug: compare Airflow's
{{ ds }}with the job'srun_date
Keeping the Job Definition in One Place
By the end: keep the job definition in one place.
| Rule | Why |
|---|---|
| Job defined in the bundle | Reviewed, versioned, deployed by CI (Module 27.8) |
Airflow calls it by name (job_name from an Airflow Variable) |
No job ids in code. Dev and prod use different names |
| Remove the Databricks schedule in prod when Airflow triggers | Otherwise it runs twice |
No copy of tasks in the DAG (SubmitRun, WorkflowTaskGroup) for recurring pipelines |
Two definitions drift apart |
| Deploy the job before the DAG | The name must exist when Airflow runs |
# databricks.yml, prod target: Airflow owns the trigger, so no schedule block
resources:
jobs:
orders_pipeline:
name: orders-pipeline-${bundle.target}
# schedule: removed in prod
- Keep the
scheduleblock only for the Databricks-scheduled setup (Module 27) - Version the DAG file and the bundle together if they change together (new parameter = job change + DAG change)
- Test the DAG in
devagainstorders-pipeline-dev, then promote - Rollback: redeploy the previous job version, no DAG change needed
What you can do now
- Airflow decides when. The Databricks job decides how
- Connection: workspace URL + token, or a service principal with OAuth
RunNowfor recurring jobs.SubmitRunfor one-offs. Workflow group only when the DAG should own the job definition- Pass
{{ ds }}asrun_date. Retry in one layer - One owner for the job definition and for the schedule