Lakeflow Jobs Tasks, Dependencies and Control Flow: Retries, Run If, If/Else and For Each (Databricks Data Engineer Associate)
Lakeflow Jobs is Databricks' orchestrator: a job is a graph of tasks (notebook, SQL, dashboard, pipeline, Run Job and more) linked by dependencies, with control-flow features that retry failed tasks, decide which tasks run after a failure, branch on values and loop over lists. This lesson covers how to configure the common task types and their compute, wire dependencies in the DAG, set retry policies and timeouts, choose Run if conditions, branch with the If/else condition task, loop with the For each task, pass values with job parameters and task values, read a run's status from its leaf tasks, and repair failed runs.
On this page10 sections
- Lakeflow Jobs: tasks arranged in a DAG
- Common task types and notebook task configuration
- SQL and dashboard tasks: running Databricks SQL objects from a job
- Pipeline and Run Job tasks: declarative pipelines and child jobs
- Retries and timeouts: recovering from transient failures
- Run if conditions: what a task does when upstream tasks fail
- Job run status and repairing failed runs
- Branching with the If/else condition task
- Looping with the For each task
- Passing values: task parameters, job parameters and task values
- Design a task graph with Depends on so independent tasks fan out and dependent tasks fan in as early as possible
- Configure notebook, SQL, dashboard, pipeline and Run Job tasks, including their source, parameters and compute
- Set a task retry policy, timeout and retry on timeout, and work out a task's worst-case duration
- Choose the Run if condition for cleanup, handler and fallback tasks, and predict Excluded and Upstream failed statuses
- Branch with the If/else condition task and loop with the For each task, including operator semantics, inputs and concurrency
- Pass values with task parameters, job parameters and task values, and know their precedence and limits
- Explain how leaf tasks decide a run's status and repair a failed run without rerunning completed tasks
Lakeflow Jobs: tasks arranged in a DAG
A Lakeflow Jobs job (formerly Databricks Workflows jobs) is a set of tasks plus the dependencies between them, and Databricks runs those tasks in the order the dependencies allow. The dependencies form a directed acyclic graph (DAG): each task lists the tasks it Depends on, and it can start only when those upstream tasks have finished and its Run if condition is met. A task with no dependencies starts as soon as the run starts.
Two shapes cover most designs:
- Fan-out: independent tasks with no dependency on each other start in parallel (compute permitting). Four extracts from separate source systems that each take about fifteen minutes finish in roughly fifteen minutes together, not an hour, when none of them depends on another.
- Fan-in: a task that needs several inputs lists every prerequisite in Depends on, so it waits for all of them. Depending on only the slowest load is a bug: the others may not be finished when it starts.
The rule that keeps a DAG fast is depend on what the task needs, nothing more. A report that needs only the orders and customers tables should depend on those two loads, not on a cleanup task that runs after all loads; the scheduler then starts it the moment its own inputs are ready. Chaining unrelated tasks, or splitting the work into separate jobs glued together with fixed time offsets, throws away both the parallelism and the dependency check.
Each run of the job records a status for every task and an overall status for the run, and failed runs can be repaired instead of rerun from scratch. The rest of this lesson covers the task types the exam names, the control-flow features (retries, Run if, If/else, For each), how values move between tasks, and how run status and repair work. Schedules and trigger types (scheduled, file arrival, table update) are covered in the sibling lesson on schedules and triggers.
Common task types and notebook task configuration
The task type decides what a step runs and on which compute. The exam guide names notebook, SQL query, dashboard and pipeline tasks; the Run Job task is the tool for reusing whole jobs.
| Task type | What it runs | Compute | Key settings |
|---|---|---|---|
| Notebook | A notebook from the workspace or from the job's Git source | Job compute, serverless, an all-purpose cluster, or a SQL warehouse (all-SQL notebooks only) | Parameters (read as widgets), source, compute |
| SQL | A saved query, a saved alert, or a .sql file | Serverless or pro SQL warehouse | Subtype (Query, Alert, File), warehouse, subscribers |
| Dashboard | A refresh of a published AI/BI dashboard | Serverless or pro SQL warehouse | Dashboard, warehouse, subscribers (emailed a PDF snapshot) |
| Pipeline | One update of a Lakeflow Spark Declarative Pipelines pipeline | The pipeline's own compute | Pipeline, full refresh |
| Run Job | Another job, as a single step | Whatever the child job uses | Job, job parameter values for the child run |
Other task types exist (Python script, Python wheel, JAR, dbt, and the control-flow tasks If/else and For each), but the four named in the guide plus Run Job carry most of the scenarios.
Notebook tasks: source, parameters and compute
A notebook task runs one notebook as a step of the job. Three settings matter:
- Source. Workspace runs the notebook at a workspace path (including a Git folder, which is a workspace copy someone must pull). Git provider runs the notebook straight from a remote repository at a branch, tag or commit, so every run uses committed code. A job has one Git source: all its Git-sourced tasks share one repository. To orchestrate notebooks that live in two repositories, give each repository its own job and chain them with a Run Job task.
- Parameters. Task parameters are key-value pairs that reach the notebook as widget values, so the notebook reads them with
dbutils.widgets.get("source_path"). They are not Spark configuration, environment variables or task values. Reading parameters through widgets keeps one notebook reusable across jobs that pass different values. - Compute. A notebook task can run on job compute (a cluster created for the run and terminated after it), serverless compute, or an existing all-purpose cluster. It can run on a SQL warehouse only when the notebook is a SQL notebook, written in SQL with SQL as its default language. A Python notebook full of
%sqlcells still does not qualify; granting permissions or switching warehouse type does not change that.
Note the difference between %run and a Run Job task: %run inlines another notebook's code into the caller's session, while a Run Job task triggers a separate job with its own tasks and settings.
SQL and dashboard tasks: running Databricks SQL objects from a job
A SQL task runs a Databricks SQL object on a serverless or pro SQL warehouse; a classic warehouse or a job cluster cannot run it. Its subtypes are Query, Alert and File (dashboard refreshes are not a SQL task subtype; they use the Dashboard task below):
- Query runs a saved query, for example to rebuild a gold summary table after an upstream notebook.
- Alert evaluates a saved Databricks SQL alert (a query plus a threshold condition) and notifies its subscribers. It reuses the threshold logic analysts already maintain instead of re-implementing it in a notebook that raises an error.
- File runs a
.sqlfile, which can hold several statements separated by semicolons, from the workspace or from the job's Git source. For a Git source the path is relative to the repository root with no leading/or./, for exampleingest/sql/load_customers.sql; a/Workspace/...path points at the workspace, not the repository.
A Dashboard task refreshes a published AI/BI dashboard and can email the result to subscribers with a PDF snapshot attached. It also needs a serverless or pro SQL warehouse. A draft dashboard cannot be selected: the author must publish it first, and the user configuring the task needs at least CAN VIEW on the published dashboard. When the job runs, the refresh runs as the job's Run as identity. Rerunning the dashboard's queries in a SQL query task does not refresh the published dashboard or send it to anyone.
Pipeline and Run Job tasks: declarative pipelines and child jobs
A Pipeline task starts one update of an existing Lakeflow Spark Declarative Pipelines pipeline and finishes when that update completes, so downstream tasks can depend on fresh streaming tables and materialized views. Running the pipeline's source notebook as a plain notebook task does not update the pipeline's tables; declarative code is executed by a pipeline update.
- Full refresh. Normally an update processes only new data. Enabling full refresh on the Pipeline task makes the update recompute the pipeline's tables from scratch, the right tool for a one-off rebuild after historical source data was corrected. Retries, repairs or a continuous job do not reprocess data already handled.
- One update at a time. A pipeline runs only one update at a time, so a job that contains a Pipeline task runs one job run at a time even if its maximum concurrent runs is set higher. Parallel backfills through the same pipeline queue behind each other.
Run Job tasks: modular orchestration with child jobs
A Run Job task triggers another job in the workspace as one step of the current job and waits for that child run to finish. It is how a team reuses a tested job (an ingestion job, say) inside several orchestrations while keeping its tasks, settings and fixes in one place.
- Parameters. The Run Job task can set values for the child job's job parameters, overriding the child's defaults for that run. Two Run Job tasks pointing at the same child job with different values (for example
region= emea and apac) run the same logic twice, in parallel if the child job allows enough concurrent runs. - Isolation. The child job is a separate run. Task values set in the parent run are not visible inside the child run; pass data to it through its job parameters.
- Crossing Git repositories. Because each job has one Git source, chaining a job per repository with Run Job tasks is how one orchestration runs code from several repositories.
- Multi-step loop bodies. A For each task has exactly one nested task. When each iteration needs several steps (validate, then load), make the nested task a Run Job task whose child job contains those steps.
Retries and timeouts: recovering from transient failures
A retry policy on a task makes Databricks rerun that task automatically when it fails, without rerunning the rest of the job and without code changes. It is the answer to transient failures such as a briefly unavailable API. The policy has:
- Retries: how many extra attempts follow the first one. Two retries means up to three attempts.
- Wait between retries: the minimum interval before the next attempt, measured from the start of the failed attempt, giving a transient error time to clear.
- Retry on timeout: off by default. A task's timeout applies to each attempt; an attempt that exceeds it ends as Timed out, and it is retried only when retry on timeout is enabled. Raising the timeout or the wait does not make a hung attempt retry.
Worst-case duration follows from those rules: with retry on timeout enabled, a task can occupy roughly (retries + 1) × timeout when every attempt hangs, because each attempt gets the full timeout and an interval shorter than an attempt has already elapsed when the attempt times out. A job-level timeout, by contrast, limits the whole run and does not cause any retry.
Related settings are often confused with retries: a duration warning threshold only notifies when a task or run is slow, and maximum concurrent runs controls how many runs of the job may overlap. Neither reruns a failed task.
Run if conditions: what a task does when upstream tasks fail
Run if is the dependency condition on a task: it decides, from the results of the task's direct dependencies, whether the task runs. The default is All succeeded. In the job definition the values appear as run_if constants.
| Condition | run_if value | The task runs when | Typical use |
|---|---|---|---|
| All succeeded (default) | ALL_SUCCESS | every dependency succeeded | normal pipeline steps |
| At least one succeeded | AT_LEAST_ONE_SUCCESS | one or more dependencies succeeded | publish if any region loaded |
| None failed | NONE_FAILED | no dependency failed and at least one dependency ran (some may be excluded) | continue when nothing broke |
| All done | ALL_DONE | every dependency finished, whatever the result | cleanup, audit, always-run steps |
| At least one failed | AT_LEAST_ONE_FAILED | one or more dependencies failed | quarantine, alert handlers |
| All failed | ALL_FAILED | every dependency failed | fallback when everything broke |
When a condition is not met the task does not run, and the status it gets matters for everything downstream:
- Excluded: the task was skipped because an At least one failed or All failed condition was not met (nothing failed, so the handler had nothing to do), or because it sits on the branch an If/else task did not take. Downstream Run if checks treat an Excluded task as successful.
- Upstream failed: the task was skipped because any other condition was not met, typically All succeeded after a dependency failed. Downstream checks treat it as failed, so the failure propagates: a task with None failed that depends on an Upstream failed task does not run either. An Upstream canceled dependency is treated as failed in the same way.
One exception to "Excluded counts as successful": if all of a task's dependencies are Excluded, the task itself is Excluded, whatever its Run if condition. Exclusion cascades, so a task whose only dependency is an Excluded handler does not run.
For example, when every extract succeeds, a page_oncall task set to All failed is Excluded; a refresh_reports task that depends on both page_oncall and a successful build_marts task still runs, because one dependency succeeded and the Excluded one counts as successful. When an extract fails instead, a conform task with All succeeded downstream of it is Upstream failed, and a task with None failed that depends on conform is skipped as well.
Job run status and repairing failed runs
The overall status of a job run is decided by its leaf tasks, the tasks no other task depends on:
| Run status | When |
|---|---|
| Succeeded | every task succeeded |
| Succeeded with failures | some tasks failed, but every leaf task succeeded |
| Failed | one or more leaf tasks did not succeed |
Two consequences are tested. First, an always-run leaf (an audit or cleanup task with All done) that succeeds turns a run with a failed ingest into Succeeded with failures, so an alerting rule that fires only on Failed runs stays silent; alert on the task, or use task-level notifications, when that matters. Second, a handler that succeeds does not rescue the run if another leaf was skipped: if enrich fails, a downstream export_feed leaf with All succeeded is Upstream failed and the run is Failed, even though a log_incident handler with At least one failed ran and succeeded. Status never depends on which task finished last. Task-level statuses such as Upstream failed and Excluded describe individual tasks, not the run.
Repairing failed runs
Repair run is for unsuccessful runs (failed or canceled): it finishes such a run by rerunning only the unsuccessful tasks and their dependents (failed, timed out, canceled, and the skipped tasks downstream of them). Tasks that already succeeded are not rerun, which matters when they are not idempotent, for example appends. The repair is recorded as part of the same run.
- Current settings. Repaired tasks run with the job's and tasks' current settings, so fixing a cluster's memory or a notebook and then repairing picks up the fix.
- Parameter overrides. The repair dialog accepts new parameter values that apply to the repaired tasks, so a wrong
run_datecan be corrected without editing the saved job definition.
Compare the alternatives: Run now with different parameters starts a brand-new run of every task, duplicating appends; retries repeat a task with the same inputs; and repair applies only to unsuccessful runs, so it is not a way to reprocess data that already loaded successfully.
Branching with the If/else condition task
The If/else condition task branches a job: it evaluates one comparison and runs the tasks attached to its true outcome or its false outcome. Downstream tasks pick an outcome when they declare the condition task as a dependency; tasks on the branch not taken are Excluded. Branching this way lets a job route a batch (quarantine versus publish) without any task failing, which is cleaner than raising an exception and catching it with an At least one failed condition.
The condition is left operand, operator, right operand. Operands can be literals, job parameters such as {{job.parameters.mode}}, task values such as {{tasks.profile_batch.values.null_pct}}, or other dynamic value references.
| Operators | Comparison | Consequence |
|---|---|---|
==, != | as strings | 3.0 == 3 is false; != is true for any other text |
>, >=, <, <= | as numbers | use these for thresholds such as a null percentage above 5 |
Run if conditions cannot do this job: they see only whether upstream tasks succeeded or failed, never a value a task produced. And job parameters cannot carry a value computed mid-run, because they are fixed when the run is triggered. To branch on something computed during the run, set it as a task value and compare it in an If/else task.
Looping with the For each task
The For each task runs one nested task once per element of an input array, and tracks each iteration separately, so a failure for one value is visible and does not hide the others. It replaces defining dozens of near-identical tasks, or a single notebook that loops internally (where every value shares one task run).
- Referencing the element. The nested task reads the current element with
{{input}}, typically passed as a task parameter. When the inputs are JSON objects,{{input.<key>}}reads one field, for example{{input.region}}and{{input.table}}. - One nested task, never another For each. Nested loops are not allowed, so loop over combinations by flattening them into one array of objects. When each iteration needs several steps, the nested task can be a Run Job task that runs a child job.
- Concurrency. Iterations run one at a time by default (concurrency 1). Raising the For each task's concurrency runs several iterations in parallel. The job's maximum concurrent runs setting is unrelated: it governs overlapping runs of the whole job, not iterations inside one run, and adding workers does not help a loop that starts iterations one by one.
The inputs can come from three places, chosen by when the list is known:
| Inputs source | Known when | Fits |
|---|---|---|
| A JSON array typed in the Inputs box | when the job is defined | a fixed list; the box is static configuration with a size limit |
A job parameter, e.g. {{job.parameters.dates}} | when the run is triggered | an operator choosing backfill dates per run |
A task value, e.g. {{tasks.list_tables.values.tables}} | during the run | a list an upstream task discovers at runtime |
Passing values: task parameters, job parameters and task values
Values reach tasks in three ways, and the exam tests which one fits a requirement.
| Mechanism | Set by | When | Scope |
|---|---|---|---|
| Task parameter | the task definition | job definition time | one task |
| Job parameter | the job definition (default) or the trigger (override) | when the run is triggered | pushed down to every task that accepts key-value parameters |
| Task value | code in an upstream task | during the run | downstream tasks of the same run |
Job parameters suit one value every task needs, chosen per run, such as env = dev or prod: define it once instead of copying a task parameter onto every task. A notebook reads it with dbutils.widgets.get. When a job parameter and a task parameter share a key, the job parameter takes precedence, whether the run is manual or scheduled. Job parameters cannot change during a run.
Task values carry small results from one task to later tasks in the same run, regardless of the compute each task uses. Spark configuration, temporary views and widgets do not cross task boundaries, because each task runs in its own session.
# upstream task named stage_orders
dbutils.jobs.taskValues.set(key="batch_id", value=batch_id)
# downstream notebook
batch_id = dbutils.jobs.taskValues.get(
taskKey="stage_orders", key="batch_id",
debugValue="test-batch")
Downstream tasks can also receive a task value as a parameter with the reference {{tasks.<task_name>.values.<value_name>}}; the task name comes first. debugValue is returned only when the notebook runs outside a job, which makes interactive testing possible while scheduled runs still read the real value. default is returned inside a job when the key is missing, which can hide a broken upstream task, so Databricks does not recommend relying on it. A task value is stored as JSON and its JSON representation cannot exceed 48 KiB; pass larger data by reference, writing it to a table or volume file and passing the location as the task value.
Tip. Expect short scenarios about a data engineer's job with a stated requirement: recover from a transient error, always run a cleanup step, react only to failures, finish a partly failed run without repeating appends, or route a batch on a computed value. Items ask which task type, Run if condition, retry setting or value-passing mechanism meets the requirement, and harder ones combine two facts, such as the Run if choice together with the resulting run status, or how Excluded and Upstream failed statuses propagate. Options are plausible near-neighbours (job parameter versus task value versus task parameter, For each concurrency versus maximum concurrent runs, repair versus run now), so know the exact behaviour of each rather than the feature names.
- A job is a DAG: tasks with no dependencies start in parallel, and a task that needs several inputs must list every one in Depends on.
- Retries rerun only the failed task; two retries means three attempts, the timeout applies to each attempt, and a timed-out attempt is retried only with retry on timeout enabled.
- Run if defaults to All succeeded; use All done for cleanup, At least one failed for handlers, and At least one succeeded to continue when any input loaded.
- Excluded tasks count as successful downstream; Upstream failed tasks count as failed, so failures propagate down the DAG.
- The run's status comes from its leaf tasks: all leaves succeeded but something failed is Succeeded with failures; any unsuccessful leaf makes it Failed.
- If/else branches on a comparison: == and != compare as strings, the other operators compare as numbers.
- For each runs one nested task per input with {{input}} or {{input.key}}; no nested For each, concurrency defaults to 1, and inputs come from the Inputs box, a job parameter or a task value.
- Job parameters are set per run, pushed to every task and win over task parameters with the same key; task values pass small (48 KiB) results within a run.
- SQL and dashboard tasks need a serverless or pro SQL warehouse; a dashboard must be published; a job with a Pipeline task runs one run at a time.
- Repair reruns failed tasks and their dependents with the current settings and optional parameter overrides; succeeded tasks are not rerun.
Frequently asked questions
What is the difference between Run if and the If/else condition task in Lakeflow Jobs?
Run if decides whether a task runs from the success or failure of its direct dependencies, for example All done for a cleanup task. The If/else condition task evaluates a comparison of values, such as a job parameter or a task value against a literal, and runs its true or false branch. Use Run if to react to failures and If/else to route on data.
How do I pass a value from one Databricks job task to another?
Set it in the upstream task with dbutils.jobs.taskValues.set(key, value) and read it downstream with dbutils.jobs.taskValues.get(taskKey, key) or pass it as a parameter with {{tasks.<task_name>.values.<value_name>}}. Task values work across different compute within the same run, and their JSON form must stay within 48 KiB; larger data should be written to a table or file and passed by location.
Why does my If/else condition return false when the values look equal?
The == and != operators compare their operands as strings, so 3.0 and 3 are different. The >, >=, < and <= operators compare numerically. Compare numbers with a numeric operator, or make sure both sides have the same string form.
What job run status does Databricks show when a task fails but a cleanup task succeeds?
Run status is decided by leaf tasks, the tasks nothing depends on. If every leaf task succeeded but some other task failed, the run is Succeeded with failures. If any leaf task failed or was skipped as Upstream failed, the run is Failed.
Can a For each task contain another For each task?
No. A For each task has exactly one nested task, and it cannot be another For each. For combinations, loop once over an array of objects and read fields with {{input.<key>}}; for several steps per iteration, make the nested task a Run Job task that runs a child job.
Does a retried Databricks task restart the whole job?
No. A task retry policy reruns only the failed task, after the configured wait, up to the number of retries. A task that hits its timeout is retried only when retry on timeout is enabled. To finish a run whose retries were exhausted, repair the run (repair is for failed or canceled runs), which reruns failed tasks and their dependents but not tasks that succeeded.
Which takes precedence, a job parameter or a task parameter with the same name?
The job parameter. Job parameters are pushed down to every task that accepts key-value parameters, and when a task parameter uses the same key, the job parameter's value is the one the task receives, for both manual and scheduled runs.
Why can't my job with a Pipeline task run several runs at once?
A Lakeflow Spark Declarative Pipelines pipeline runs only one update at a time, so a job that contains a Pipeline task is limited to one concurrent run, even if its maximum concurrent runs is set higher.
Source
This lesson covers the "Working with Lakeflow Jobs" domain of the official Databricks DEA exam guide. Vendors revise their guides — check the source for the current version.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.