# check the result in a notebook
spark.sql("SELECT count(*) FROM dbx_project.silver_orders").show()
Module 27: Run the Project with Databricks Jobs
Run the project as a Databricks job: tasks, parameters, schedule, retries, repair runs, job as code, CI deploy.
Run the orders_pipeline wheel from Module 26 as a scheduled, monitored Databricks job. You already know what a job is (Module 10): this module applies it to the project.
- Needs the wheel from Module 26 topic 7 in
/Volumes/<catalog>/dbx_project/artifacts/ - The UI path (topics 1 to 7) works on Free Edition: serverless compute, up to 5 concurrent tasks
- Topics 8 and 9 (job as code, deploy from CI) need the CLI and a token: unconfirmed on Free Edition, so read them as reference if you can't create one
- Use a 2025 date in tests, for example
2025-03-15: the practice data covers 2025
Create a Job from the Project
By the end: create the first task in the Jobs UI from the wheel.
Steps:
- Sidebar Jobs & Pipelines → Create → Job. Name it
orders-pipeline - Task name
load_silver. Type Python wheel - Package name
orders_pipeline. Entry pointload_silver - Compute: Serverless. Under Environment and libraries, add the wheel path
/Volumes/<catalog>/dbx_project/artifacts/orders_pipeline-0.1.0-py3-none-any.whl - Parameters, keyword:
catalog= your catalog,schema=dbx_project,run_date=2025-03-15 - Save, then Run now
- Wheel tasks send parameters as command-line arguments. If the run fails with an argument error, open the run output and compare it with
parse_args(Module 26.5) - Dependencies for a wheel on serverless go in the environment, not a cluster library list
- Use a service principal as run-as for production (Module 17). For learning, your own identity is fine
Tasks and Dependencies
By the end: add the other tasks and wire dependencies.
| Task | Entry point | Depends on |
|---|---|---|
load_silver |
load_silver |
none |
check_quality |
check_quality |
load_silver |
build_gold |
build_gold |
check_quality |
- Add task each time, same wheel, a different entry point. Set Depends on in the task panel
- A failing
check_qualityraises (Module 26.9), sobuild_goldis skipped. Gold never sees bad silver - By default a task needs all upstream tasks to succeed. Run if changes that: at least one succeeded, none failed, at least one failed, all done
- A cleanup or notify task with "at least one failed" is the pattern for a failure hook
Try it: run with a bad quantity row injected into silver_orders after load_silver. See build_gold skipped.
- Tasks with no dependencies run in parallel (limited to 5 at once on Free Edition)
Parameters and Dynamic Values
By the end: pass job parameters to every task with dynamic values.
| Where | What |
|---|---|
| Job parameters | catalog, schema, run_date, days: set once for the whole job |
| Task parameters | Reference them: run_date = {{job.parameters.run_date}} |
| Dynamic values | Filled in at run time |
| Dynamic value | Gives |
|---|---|
{{job.parameters.run_date}} |
A job parameter |
{{job.start_time.iso_date}} |
Date the run started (UTC) |
{{job.id}} / {{job.run_id}} |
Ids for logging |
{{tasks.<task>.values.<key>}} |
A task value from an earlier task |
Edit each task's parameters to the references:
catalog = {{job.parameters.catalog}}
schema = {{job.parameters.schema}}
run_date = {{job.parameters.run_date}}
days = {{job.parameters.days}}
- Set the job parameter
run_dateempty for scheduled runs: the code uses today (UTC). Backfills and reruns pass an explicit date - Parameters are strings:
--daysis converted byargparse(type=int) - If a job parameter and a task parameter have the same key, the job parameter wins
- Run now with different settings overrides parameters for one run
Schedules and Triggers
By the end: schedule the job and know the other triggers.
Schedule: job page → Schedules & Triggers → Add trigger → Scheduled. Pick a time, a timezone, save.
| Trigger | Runs when | Notes |
|---|---|---|
| Scheduled | Quartz cron or a simple picker | 0 30 6 * * ? = 06:30 every day |
| File arrival | New file in a volume or external location | Limits apply: check the docs |
| Table update | A source table changes | Good for gold jobs that follow silver |
| Continuous | Always, restarts after each run | Not for batch projects |
- Use an explicit timezone. UTC avoids daylight-saving surprises
- Pause the schedule instead of deleting it while debugging
- Set max concurrent runs = 1 so a slow run is not overlapped by the next
- Free Edition: scheduled jobs work. Continuous and large file-arrival setups: check what your workspace allows
Retries, Timeouts and Notifications
By the end: make the job survive failures and tell someone.
| Setting | Where | Suggestion |
|---|---|---|
| Retries | Per task | 2 retries with a delay for load_silver. None for check_quality: a bad row doesn't fix itself |
| Timeout | Per task or job | About 2× the normal run time |
| Duration warning | Job notifications | Alert if a run is much slower than usual |
| Failure email | Job → Notifications | Always set this |
| Queue | Job setting | Let a run wait instead of being skipped |
- Retry only for transient causes (network, a busy service). A retry that repeats a code error wastes compute
- Retries are safe only if the task is idempotent: this project overwrites or
replaceWheres, so yes - Add yourself to failure notifications. A job that fails silently is worse than no job
- Email is the simplest on Free Edition. Slack or webhook destinations need to be set up first
Running and Monitoring
By the end: read a run: the matrix, the logs and the history.
Where to look:
| Need | Open |
|---|---|
| Did the last runs pass? | Job page → Runs tab |
| Which task failed? | The run's graph / matrix view |
| Why? | Click the failed task → output / error. Your log.info lines show here |
| Which parameters and commit ran? | Run details (the task shows the Git commit when a Git source is used) |
| How long and how much? | Duration per task, and query/compute details |
%sql
-- optional: every job run in one table (system tables; availability varies)
SELECT job_id, period_start_time, result_state
FROM system.lakeflow.job_run_timeline
ORDER BY period_start_time DESC
LIMIT 20
- Run history is kept about 60 days: copy what you need longer into your own
ops_run_log(Module 16) - Failure in logs of one task, not of the job. Start from the red box
- Compare a slow run with a fast one: same parameters? same data volume?
Repair Runs and Backfills
By the end: repair a failed run and backfill past dates.
Repair run:
- Run page → Repair run. It reruns failed and skipped tasks plus their dependents. Successful tasks are not rerun
- It applies the current job settings: fix the cause first (parameter, wheel version, data)
- Repair is for jobs with 2 or more tasks. For a one-task job use Run now
- Databricks does not make tasks idempotent. Your tasks must be: ours overwrite, so a repair can't duplicate
Backfill: run the job for past dates.
| Step | Detail |
|---|---|
| Job → Backfill (or Run now with different settings) | Pick the date range |
| Parameter override | run_date = {{backfill.iso_datetime}} or an explicit date |
| Limit | A backfill creates up to 100 runs |
Try it: run for 2025-03-10 through 2025-03-12, then check:
%sql
SELECT order_date, orders, revenue
FROM dbx_project.gold_daily_revenue
WHERE order_date BETWEEN '2025-03-10' AND '2025-03-12'
ORDER BY order_date
daysdefaults to 7 so each run rewrites a window. Late data inside that window is picked up automatically
Defining the Job as Code
By the end: define the job as code in a bundle.
# databricks.yml
bundle:
name: orders-pipeline
variables:
catalog:
description: Unity Catalog catalog
schema:
default: dbx_project
artifacts:
orders_wheel:
type: whl
build: python -m build --wheel
path: .
resources:
jobs:
orders_pipeline:
name: orders-pipeline-${bundle.target}
max_concurrent_runs: 1
parameters:
- { name: catalog, default: "${var.catalog}" }
- { name: schema, default: "${var.schema}" }
- { name: run_date, default: "" }
- { name: days, default: "7" }
schedule:
quartz_cron_expression: "0 30 6 * * ?"
timezone_id: UTC
pause_status: PAUSED
email_notifications:
on_failure: ["you@example.com"]
environments:
- environment_key: default
spec:
environment_version: "3" # copy the current value from `databricks bundle init`
dependencies:
- ./dist/*.whl
tasks:
- task_key: load_silver
environment_key: default
max_retries: 2
python_wheel_task:
package_name: orders_pipeline
entry_point: load_silver
named_parameters: { catalog: "{{job.parameters.catalog}}", schema: "{{job.parameters.schema}}", run_date: "{{job.parameters.run_date}}", days: "{{job.parameters.days}}" }
- task_key: check_quality
depends_on: [{ task_key: load_silver }]
environment_key: default
python_wheel_task:
package_name: orders_pipeline
entry_point: check_quality
named_parameters: { catalog: "{{job.parameters.catalog}}", schema: "{{job.parameters.schema}}", run_date: "{{job.parameters.run_date}}", days: "{{job.parameters.days}}" }
- task_key: build_gold
depends_on: [{ task_key: check_quality }]
environment_key: default
python_wheel_task:
package_name: orders_pipeline
entry_point: build_gold
named_parameters: { catalog: "{{job.parameters.catalog}}", schema: "{{job.parameters.schema}}", run_date: "{{job.parameters.run_date}}", days: "{{job.parameters.days}}" }
targets:
dev:
mode: development
default: true
variables:
catalog: workspace # use the result of SELECT current_catalog()
prod:
mode: production
variables:
catalog: prod_catalog
workspace:
host: https://<prod-workspace-url>
databricks bundle validate -t dev
databricks bundle deploy -t dev
databricks bundle run orders_pipeline -t dev
- Development mode prefixes the job name and pauses its schedule: unpause only in prod
- The wheel is built by the
artifactsstep and uploaded ondeploy: you don't upload it by hand - Check
environment_versionand the wheeldependenciespath against a template fromdatabricks bundle init: they change between versions - Production mode also wants
run_asandpermissions: add a service principal (Module 15) - Edit in the UI only to experiment. The repo is the source of truth
Free Edition
Bundles and the CLI aren't documented for Free Edition. If you can create a token (Settings → Developer), try topic 8 in dev. Otherwise keep the UI job from topics 1 to 7.
Deploying the Job from CI
By the end: deploy the job from CI on every merge.
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
concurrency: deploy-prod
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install build pytest && pip install -e .
- run: pytest tests/unit
- uses: databricks/setup-cli@main
- run: databricks bundle validate -t prod
env:
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.SP_TOKEN }}
- run: databricks bundle deploy -t prod
env:
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.SP_TOKEN }}
- CI uses a service principal token, stored in the CI secrets. Not your personal token
- Run unit tests without Databricks first (local Spark): they need no secrets
- Bump
versioninpyproject.tomlper release so the new wheel has a new name - Rollback: deploy the previous git tag
- A smoke run after deploy (
databricks bundle run ... -t dev) catches a broken wheel before prod - Module 28 triggers this same job from Airflow without changing it
What you can do now
- One job, three wheel tasks, dependencies in a line
- Parameters at job level, passed to tasks with
{{job.parameters.x}} - Retry what is transient, notify on failure, keep tasks idempotent
- Repair reruns failed tasks; backfill reruns dates
- The job definition and the deploy live in Git