from pyspark.sql import functions as F
dbutils.widgets.text("run_date", "2025-01-01")
run_date = dbutils.widgets.get("run_date")
catalog = spark.sql("SELECT current_catalog()").first()[0]
df = (spark.table(f"{catalog}.dbx_course.orders")
.filter(F.to_date("order_ts") == run_date))
df.write.mode("overwrite").saveAsTable(f"{catalog}.dbx_course.job_orders_stage")
n = df.count()
dbutils.jobs.taskValues.set(key="row_count", value=n)
print(f"{run_date}: {n} rows")
Module 10: Workflow Orchestration
Schedule, chain and monitor work with Lakeflow Jobs.
Topics in this module
- Introduction to Lakeflow Jobs
- Jobs and Tasks
- Task Dependencies and DAGs
- Scheduling and Triggers
- Notebook and Python Tasks
- Pipeline Tasks
- Job Parameters and Dynamic Values
- Conditional Execution
- Retries, Timeouts, and Notifications
- Parallel Task Execution
- Job Monitoring
- Failure Recovery
- Backfills and Reruns
- Airflow Integration
Run your work on a schedule, in order, and know when it breaks. Lakeflow Jobs is the built-in orchestrator: a job is a set of tasks, run by a trigger.
- Product name: Lakeflow Jobs (earlier "Databricks Workflows" / "Jobs")
- Free Edition: serverless compute only, and a cap of 5 concurrent job tasks per account. Keep demo jobs small
- Create the three small notebooks below once. Topics 2–13 reuse them as tasks
ingest notebook: loads a batch for a given date:
transform notebook: merges the staged rows into a target:
catalog = spark.sql("SELECT current_catalog()").first()[0]
spark.sql(f"CREATE TABLE IF NOT EXISTS {catalog}.dbx_course.job_orders_final AS "
f"SELECT * FROM {catalog}.dbx_course.job_orders_stage WHERE 1 = 0")
spark.sql(f"""
MERGE INTO {catalog}.dbx_course.job_orders_final t
USING {catalog}.dbx_course.job_orders_stage s ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
report notebook: a last step that prints a summary:
catalog = spark.sql("SELECT current_catalog()").first()[0]
print(spark.table(f"{catalog}.dbx_course.job_orders_final").count())
Introduction to Lakeflow Jobs
By the end: say what Lakeflow Jobs is and how it relates to pipelines.
- Job = tasks + dependencies + trigger + parameters + notifications
- Task = one unit of work: notebook, Python script, wheel, SQL, pipeline, dbt, and more
- Trigger = what starts the job: a schedule, a file, a table update, or manual
- Jobs orchestrate. A pipeline (Module 9) is something a job can run as one task
| Lakeflow Jobs | Pipeline | |
|---|---|---|
| Does | Runs and orders tasks | Maintains datasets declaratively |
| Fits | Any workload and mixed steps | Layered ETL tables |
Remember
Jobs decide when and in what order. Tasks do the actual work.
Jobs and Tasks
By the end: create a job with a single notebook task and run it.
Create it:
- Jobs & Pipelines → Create → Job
- Task name
ingest. Type Notebook. Pick theingestnotebook - Compute: Serverless (the only option on Free Edition)
- Run now, then open the run
| Part | Where |
|---|---|
| Task graph | Tasks tab |
| Job settings (trigger, parameters, notifications) | Job details pane |
| Run history | Runs tab |
- Each run has an ID, a state and a duration. A task's output and logs are one click away
- Name tasks for what they do (
ingest,transform): names are used in references like{{tasks.ingest.values.row_count}} - Develop in the notebook, test interactively, then run it as a job. Behaviour should match
Interactive for building, jobs for running
Anything that must run regularly runs as a job, not as a notebook you start by hand.
Task Dependencies and DAGs
By the end: chain tasks into a DAG with dependencies.
Add tasks: Add task, then Depends on → pick the upstream task.
| Dependency choice | Result |
|---|---|
| One upstream | Runs after it |
| Several upstream | Runs after all of them (default) |
| No dependency | Starts immediately, in parallel |
- The tasks form a DAG: directed, no cycles
- Tasks with no link between them run in parallel (within the compute and the 5-task cap)
- Write tasks so each one can run again safely:
MERGEand overwrite, not blind appends
Scheduling and Triggers
By the end: start a job automatically with schedules and event triggers.
Add a trigger: Job details → Add trigger.
| Trigger | Starts a run when |
|---|---|
| Scheduled | A time is reached (simple or cron) |
| File arrival | New files land in a monitored Unity Catalog location |
| Table update | A source table changes |
| Continuous | The last run ends: the job keeps running |
| None | You press Run now, or an API or Airflow calls it |
- Cron uses Quartz syntax:
0 0 6 * * ?= every day at 06:00 - Pick a time zone. In a daylight-saving zone, hourly jobs can skip or double at the change. Use UTC for strict hourly timing
- Scheduling is not low-latency: runs can start a few minutes late
- Pausing a trigger stops new runs. Active runs finish
- Default max concurrent runs is 1: a run is skipped (or queued, if queueing is on) while another is active
Free Edition
Schedules and manual runs are fine. File arrival and table triggers depend on Unity Catalog storage locations, which Free Edition restricts. Check Add trigger to see what is offered.
Notebook and Python Tasks
By the end: choose the right task type for notebooks and Python scripts.
| Task type | Use for | Parameters arrive as |
|---|---|---|
| Notebook | Exploration-friendly steps | Widgets: dbutils.widgets.get("name") |
| Python script | A .py file in a workspace or Git folder |
Command-line arguments |
| Python wheel | Packaged code with an entry point | Keyword arguments |
| SQL | Queries, dashboards, alerts | Named parameters :name |
| Pipeline | Run a Lakeflow pipeline | Pipeline parameters |
# Python script task: read arguments from the command line
import sys
run_date = sys.argv[1]
- Notebook tasks are quickest to start. Script and wheel tasks are better for tested, versioned code (Module 26)
- Source can be the workspace or a Git repository and branch/tag (Module 15, 26)
- Dependencies for serverless tasks go in the task's environment
- A notebook run as a job needs defaults only for interactive use: the default can hide a missing job parameter
Pipeline Tasks
By the end: run a pipeline as a task in a job.
- Add task → Pipeline → choose your pipeline (Module 9)
- A pipeline task starts an update and waits for it. It succeeds or fails with the update
- Put it between other tasks: ingest files → pipeline → report
- Pass values with the task's Parameters box using dynamic references (next topic)
- Full refresh is an option on the task. Leave it off for routine runs
- Job-run pipelines are retried and restarted on recoverable errors, unlike interactive UI runs
- Pipeline tasks are not parameterised per backfill slice: for history use a one-time append flow (Module 9.7)
Schedule the job, not the pipeline
Trigger the job and let the pipeline be one task. You get ordering, alerts and retries in one place.
Job Parameters and Dynamic Values
By the end: pass parameters into tasks and between tasks with dynamic values.
Job parameters, set under Job details → Parameters:
| Key | Default |
|---|---|
run_date |
{{job.start_time.iso_date}} |
Reference them in a task's parameters:
| Syntax | Gives |
|---|---|
{{job.parameters.run_date}} |
A job parameter |
{{job.id}}, {{job.run_id}}, {{job.name}} |
Job and run identity |
{{job.start_time.iso_date}} |
Run start date (UTC) |
{{job.trigger.type}} |
periodic, one_time, file_arrival, table, continuous... |
{{task.name}}, {{task.run_id}} |
The current task |
{{tasks.ingest.values.row_count}} |
A value set by another task |
{{tasks.ingest.result_state}} |
success, failed, ... |
Task values: set in one task, read by another:
# in ingest
dbutils.jobs.taskValues.set(key="row_count", value=n)
- Give the
transformtask the parameterrows={{tasks.ingest.values.row_count}}, then read it withdbutils.widgets.get("rows") - Databricks recommends the reference syntax over
taskValues.get: renaming a task won't break it - A task value must be JSON and at most 48 KiB
- Parameters are strings in widgets: cast them
- A job parameter wins over a task parameter with the same key
The date is a parameter
Pass the run date in, don't compute it inside the notebook. Then a rerun or backfill is just another value.
Conditional Execution
By the end: branch a job with if/else and run-if conditions.
If/else task: runs one branch based on a condition:
- Add task → If/else condition named
has_rows - Condition:
{{tasks.ingest.values.row_count}}>0 - Set
transformto depend onhas_rows(true)
- Try it: today's date has no orders, so the true branch is skipped. Run with
run_date= a 2025 date and it runs - Operators:
==,!=,>,>=,<,<= ==and!=compare strings (12.0 == 12is false). The others compare numbers- Values must be number, string or boolean. A boolean
truebecomes the string"true"
Run if (set per task) reacts to upstream status:
| Run if | Runs when |
|---|---|
| All succeeded (default) | Every upstream task succeeded |
| At least one succeeded | Some upstream task succeeded |
| None failed | No upstream failure |
| All done | Upstream finished, whatever the outcome |
| At least one failed | Something upstream failed |
| All failed | Everything upstream failed |
- Use At least one failed or All done for cleanup and alert tasks
- A skipped (
Excluded) upstream counts as success for run-if
Retries, Timeouts, and Notifications
By the end: make jobs resilient with retries and timeouts, and tell people when they fail.
Per task: Retries (how many, wait between attempts, retry on timeout) and Timeout.
Per job: Timeout under run duration, an expected-duration warning, and Notifications.
| Setting | Use |
|---|---|
| Task retries | Transient failures: network, quota, a busy cluster |
| Task timeout | Stop a task that hangs |
| Job timeout | Cap a whole run: status becomes Timed Out |
| Duration warning | Alert when slow, without stopping |
| Notifications | Start, success, failure, duration warning |
- Notify by email. Slack, Teams, PagerDuty and webhooks need an admin-set destination
- Job-level notifications are not sent while a failed task is retrying. Use task-level ones for every failure, or mute until the last retry
- Set max concurrent runs deliberately. Extra runs are skipped, or queued (up to 48 hours) when queueing is on
Retries re-run the whole task
A retried task starts from the beginning. If it wrote partial output, it can duplicate it. Idempotent writes (MERGE, overwrite) make retries safe.
Free Edition
Email works. System destinations for Slack or webhooks may not be available.
Parallel Task Execution
By the end: run independent tasks and loops in parallel.
- Several tasks that depend on the same upstream task start together
- A downstream task with several dependencies waits for all (default) or follows its Run if rule
- For each repeats a task over a list:
[{"table": "orders"}, {"table": "customers"}, {"table": "products"}]
- Nested task parameter:
{{input.table}}. The list can come from a task value or a job parameter - Concurrency controls how many iterations run at once (default 1)
- A For each cannot contain another For each
- Free Edition limit: 5 concurrent tasks in the account. More wait
Parallel where it's independent
Fan out per table or per date, then join. Don't chain tasks that don't depend on each other.
Job Monitoring
By the end: watch runs, find the failing task and read its output.
- Jobs & Pipelines → Runs: all recent runs, filter by status, job, time
- Matrix view on a job: one column per run, one cell per task. Green success, red failure, pink skipped, yellow waiting to retry
- Run details: Graph, Timeline and List views. Click a task for output, logs, code and metrics
- The Timeline shows slow tasks and where branches overlap
- A run's result depends on its leaf tasks (those with nothing after them)
-- account-wide run records, if you can read the lakeflow system schema
SHOW TABLES IN system.lakeflow;
- Run history is kept for up to 60 days: export what you need to keep
- Add your own logging with
print/logging, and write audit rows (run id, counts) to a Delta table
System tables
system.lakeflow access depends on your workspace. It is not confirmed on Free Edition.
Failure Recovery
By the end: repair a failed run without starting over.
- Open the failed run → Repair run
- Only failed tasks and the ones downstream of them run again. Successful tasks are kept
- You can change parameters in the repair dialog. Settings changes you made first also apply
- Works for jobs with two or more tasks. For a single task, just Run now
- The run keeps one record, with a column for each repair in the matrix view
- Practice: make
transformfail (rename the staging table), run, fix it, Repair run - Repair reruns each task from its start: tasks must be idempotent
Disabled tasks
Repair decides by each task's run state, not by whether it is disabled.
Backfills and Reruns
By the end: rerun past dates with a backfill and keep reruns safe.
Run backfill: next to Run now → Run backfill.
| Step | Value |
|---|---|
| Date range | 2025-01-01 to 2025-01-10 |
| Interval | 1 day |
| Parameter override | run_date = {{backfill.iso_datetime}} |
- Databricks creates one run per interval, labelled Backfill
- Max 100 runs per backfill: use a bigger interval or several backfills
- Always runs the whole job: you can't pick tasks
- Raise max concurrent runs to run slices in parallel. The default is 1
- The job must read the date parameter and process only that slice
# use the date in the task, never "today"
run_date = dbutils.widgets.get("run_date")[:10] # keep YYYY-MM-DD
- Use
MERGEor overwrite-by-range (Module 8.13) so that the same slice twice gives the same table
Pipelines
Pipeline tasks aren't parameterised per slice and can't run concurrently. Backfill pipelines with a one-time append flow.
Airflow Integration
By the end: decide how Airflow fits with Lakeflow Jobs.
- Two ways to combine them: Airflow triggers an existing Databricks job, or Airflow submits one-time runs
- If everything lives in Databricks, Lakeflow Jobs alone is simpler
- Use Airflow when a company standard or cross-system dependencies need it
- Jobs appear in the Databricks Runs tab even when Airflow started them
- Keep the schedule and the retries in one layer
- The hands-on version is Module 28
Remember
One scheduler per job. Airflow or Lakeflow's own trigger: not both.
What you can do now
- Jobs, tasks, DAGs, triggers and task types
- Parameters, task values, dynamic references
- If/else, run-if, retries, timeouts, notifications
- Monitoring, repair runs and backfills
- Where Airflow fits