DatabricksZero to Advanced
Course/Level 9 · Small Project: Run It with Airflow

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.

9 with caveats1 concept10 topics
Topics in this module
  1. Airflow or Lakeflow Jobs: Choosing
  2. Connecting Airflow to Databricks
  3. Installing the Databricks Provider
  4. Triggering the Job with DatabricksRunNowOperator
  5. One-Time Runs with DatabricksSubmitRunOperator
  6. Databricks Workflows in an Airflow DAG
  7. Parameters and Reading Results
  8. Retries, Timeouts and Failure Handling
  9. Monitoring from Both UIs
  10. 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.

28.01Concept

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
Airflow DAGDatabricksRunNowOperatorDatabricks job
Airflow decides when. Databricks decides how.
  • 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
28.02Hands-on with caveats

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)
Shell
export AIRFLOW_CONN_DATABRICKS_DEFAULT='databricks://@dbc-xxxx.cloud.databricks.com?token=<url-encoded-token>'

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)
28.03Hands-on with caveats

Installing the Databricks Provider

By the end: install the provider and check the connection works.

Shell
pip install apache-airflow-providers-databricks
# optional: Azure identity support
pip install "apache-airflow-providers-databricks[azure-identity]"
Python
# 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. If list_jobs is 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
28.04Hands-on with caveats

Triggering the Job with DatabricksRunNowOperator

By the end: trigger the existing job with `DatabricksRunNowOperator`.

Python
# 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's run_date parameter, so backfills in Airflow rerun the right day
  • job_parameters can't be combined with notebook_params, python_params and similar: use one style
  • job_id is shown in the job's URL and Job details panel
  • The operator pushes run_id and run_page_url to XCom
28.05Hands-on with caveats

One-Time Runs with DatabricksSubmitRunOperator

By the end: submit a one-off run with `DatabricksSubmitRunOperator`.

Python
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 as tasks. If both are given, named arguments win
  • The task shape is the Jobs API runs/submit format: check its reference for the current fields, including serverless environments
  • Don't copy a recurring job into DAG code (topic 10)
28.06Hands-on with caveats

Databricks Workflows in an Airflow DAG

By the end: run several Databricks tasks as one workflow from a DAG.

Python
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.

28.07Hands-on with caveats

Parameters and Reading Results

By the end: pass parameters in and read results back.

In: Airflow templates go into job_parameters:

Python
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
Python
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_log and check it with a SQL task (DatabricksSqlOperator needs a SQL warehouse http_path in the connection extras)
  • Keep the contract small: date and a few settings. Not whole datasets
28.08Hands-on with caveats

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"
  • RunNow has repair_run: on a retry, repair the failed run instead of starting a new one. Pair it with idempotent tasks (Module 27.7)
  • cancel_previous_runs stops a still-running run before a new one: use it only if you want the old run killed
  • Set execution_timeout on 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
Python
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,
)
28.09Hands-on with caveats

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_id and run_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_log as the cross-tool record (Module 16)
  • Mismatched dates are the classic bug: compare Airflow's {{ ds }} with the job's run_date
28.10Hands-on with caveats

Keeping the Job Definition in One Place

By the end: keep the job definition in one place.

Git repobundle deployDatabricks jobAirflow triggers it
The bundle defines the job. Airflow only starts it.
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
YAML
# 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 schedule block 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 dev against orders-pipeline-dev, then promote
  • Rollback: redeploy the previous job version, no DAG change needed
Recap

What you can do now

  • Airflow decides when. The Databricks job decides how
  • Connection: workspace URL + token, or a service principal with OAuth
  • RunNow for recurring jobs. SubmitRun for one-offs. Workflow group only when the DAG should own the job definition
  • Pass {{ ds }} as run_date. Retry in one layer
  • One owner for the job definition and for the schedule