Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow - #66613

Merged
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params
May 26, 2026
Merged

Forward Airflow Dag params to Databricks job parameters in CreateJobs/SubmitRun/RunNow#66613
eladkal merged 6 commits into
apache:mainfrom
moomindani:providers/39002-databricks-airflow-params-as-job-params

Conversation

@moomindani

Copy link
Copy Markdown
Contributor

The Databricks operators currently require job-level parameters to be hardcoded inside json. This forwards the operator's self.params (Airflow Dag / task / dag_run.conf params) into the corresponding Databricks parameter slot when the user has not explicitly populated it:

  • DatabricksCreateJobsOperator -> top-level parameters list ([{"name": k, "default": v}, ...]).
  • DatabricksRunNowOperator -> top-level job_parameters dict.
  • DatabricksSubmitRunOperator -> dict-shaped per-task fields: notebook_task.base_parameters, python_wheel_task.named_parameters, sql_task.parameters, run_job_task.job_parameters. Tasks whose only parameter slot is List[str] (spark_jar_task, spark_python_task, spark_submit_task) are skipped because there is no canonical mapping from a key/value dict to positional CLI arguments.

The injection only fires when the corresponding slot is empty, so users who explicitly pass parameters in json keep their existing behaviour.

Builds on @SubhamSinghal's earlier work in #39007 (closed as stale). Picks up @dirrao's and @Lee-W's review feedback (list-comprehension refactor) and @galafis's request to extend the feature to RunNow / SubmitRun.

closes: #39002


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Apply Lee-W's review suggestion from PR apache#39007: replace the manual loop
with a list comprehension that uses ``params.dump()`` (the original
``params.items()`` iteration yielded ``Param`` objects rather than the
resolved values, which would not serialise into the Databricks API).
Extend the same pattern to:
* DatabricksRunNowOperator -> populate top-level ``job_parameters`` (the
dict-shaped slot already supported by the run-now endpoint).
* DatabricksSubmitRunOperator -> populate dict-shaped per-task parameter
fields (notebook_task.base_parameters, python_wheel_task.named_parameters,
sql_task.parameters, run_job_task.job_parameters). Tasks whose only
parameter field is ``List[str]`` (spark_jar_task, spark_python_task,
spark_submit_task) are intentionally skipped because there is no
canonical mapping from a key/value dict to positional CLI arguments.
Drop the ``"parameters": []`` expectation that was added to the existing
test_exec_create / test_exec_reset cases by PR apache#39007 — it never matched
the source logic (``self.params`` is falsy when no params are set, so no
``parameters`` key is added).
Add tests covering: auto-injection for each operator, no override when
the field is already populated, and the per-task injection rules for
SubmitRun.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from 2126811 to ec4120fCompareMay 9, 2026 00:03
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Real-environment validation

Ran the operators end-to-end through airflow dags test against a real Databricks workspace, with verification tasks in the same DAG that read the workspace state back via the REST API. All seven tasks succeeded.

Tasks (DAG pr66613_realenv, with DAG-level params={"env": "prod", "batch_size": "100"})

TaskStatus
create_job (DatabricksCreateJobsOperator)success
verify_create_job — assert workspace settings.parameters contains env=prod, batch_size=100success
run_now (DatabricksRunNowOperator, params={"env": "staging", "batch_size": "42"})success
verify_run_now — assert run's job_parameters is {"env": "staging", "batch_size": "42"} (operator overrides DAG)success
submit_run (DatabricksSubmitRunOperator, params={"env": "dev", "shard": "1"})success
verify_submit_run — assert task's notebook_task.base_parameters is {"env": "dev", "batch_size": "100", "shard": "1"} (DAG-level batch_size is inherited where the operator does not override)success
cleanup_jobsuccess

The verification tasks confirm the params actually arrive at Databricks (not just that the request body is constructed locally), and that DAG-level params and operator-level params merge correctly.

DAG

dev/dag_pr66613_realenv.py
"""Real-environment validation DAG for PR #66613 (GH-39002)."""from __future__ importannotationsimportosfromdatetimeimportdatetimefromairflow.providers.databricks.hooks.databricksimportDatabricksHookfromairflow.providers.databricks.operators.databricksimport (
DatabricksCreateJobsOperator,
DatabricksRunNowOperator,
DatabricksSubmitRunOperator,
)
fromairflow.sdkimportDAG, taskNOTEBOOK_PATH=os.environ.get(
"PR66613_NOTEBOOK_PATH", "/Users/<your-user>@example.com/airflow-pr66613-noop"
)
def_hook() ->DatabricksHook:
returnDatabricksHook(databricks_conn_id="databricks_default")
withDAG(
dag_id="pr66613_realenv",
start_date=datetime(2026, 1, 1),
schedule=None,
catchup=False,
params={"env": "prod", "batch_size": "100"},
tags=["databricks", "pr66613"],
) asdag:
create_job=DatabricksCreateJobsOperator(
task_id="create_job",
json={
"name": "{{ dag.dag_id }}-{{ ts_nodash }}",
"tasks": [{"task_key": "noop", "notebook_task": {"notebook_path": NOTEBOOK_PATH}}],
},
)
@task(task_id="verify_create_job")defverify_create_job(job_id: int) ->int:
job=_hook()._do_api_call(("GET", "2.2/jobs/get"), {"job_id": job_id})
params=job["settings"].get("parameters", [])
assert {"name": "env", "default": "prod"} inparams, paramsassert {"name": "batch_size", "default": "100"} inparams, paramsreturnjob_idrun_now=DatabricksRunNowOperator(
task_id="run_now",
job_id="{{ ti.xcom_pull(task_ids='verify_create_job') }}",
params={"env": "staging", "batch_size": "42"},
wait_for_termination=False,
)
@task(task_id="verify_run_now")defverify_run_now(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
found= {p["name"]: p.get("value", p.get("default")) forpinrun.get("job_parameters", [])}
assertfound.get("env") =="staging", foundassertfound.get("batch_size") =="42", found_hook().cancel_run(databricks_run_id)
submit_run=DatabricksSubmitRunOperator(
task_id="submit_run",
notebook_task={"notebook_path": NOTEBOOK_PATH},
new_cluster={
"spark_version": "15.4.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 0,
"spark_conf": {"spark.master": "local[*]"},
"custom_tags": {"ResourceClass": "SingleNode"},
},
params={"env": "dev", "shard": "1"},
wait_for_termination=False,
)
@task(task_id="verify_submit_run")defverify_submit_run(databricks_run_id: int) ->None:
run=_hook()._do_api_call(("GET", "2.2/jobs/runs/get"), {"run_id": databricks_run_id})
base_params=run["tasks"][0]["notebook_task"]["base_parameters"]
# Operator-level params override DAG-level params for shared keys; DAG-level# keys not overridden are still inherited (here: batch_size from the DAG).assertbase_params== {"env": "dev", "batch_size": "100", "shard": "1"}, base_params_hook().cancel_run(databricks_run_id)
@task(task_id="cleanup_job", trigger_rule="all_done")defcleanup_job(job_id: int) ->None:
try:
_hook()._do_api_call(("POST", "2.2/jobs/delete"), {"job_id": job_id})
exceptException: # noqa: BLE001passjob_id_xcom=verify_create_job(create_job.output)
run_now_done=verify_run_now(
databricks_run_id="{{ ti.xcom_pull(task_ids='run_now', key='run_id') }}"
)
submit_run_done=verify_submit_run(
databricks_run_id="{{ ti.xcom_pull(task_ids='submit_run', key='run_id') }}"
)
create_job>>job_id_xcom>>run_now>>run_now_donesubmit_run>>submit_run_done
[run_now_done, submit_run_done] >>cleanup_job(job_id_xcom)

Run with:

export AIRFLOW_CONN_DATABRICKS_DEFAULT='{"conn_type":"databricks","host":"https://<workspace>","password":"<token>"}'export PR66613_NOTEBOOK_PATH=/Users/<you>/airflow-pr66613-noop # any notebook in the workspace
airflow dags test pr66613_realenv

Add a "Forwarding Airflow Dag params" section to the jobs_create, run_now,
and submit_run operator guides describing the new behaviour: when the
operator's params dict is non-empty and the corresponding json slot is
empty, params are auto-injected as job-level parameters / job_parameters /
per-task dict-shaped parameters respectively.
@moomindani
moomindaniforce-pushed the providers/39002-databricks-airflow-params-as-job-params branch from cbe66a8 to 942de26CompareMay 9, 2026 00:31
@eladkal

Copy link
Copy Markdown
Contributor

Static checks are failing

…params
- pytest.mark.parametrize first arg must be a tuple of names, not a comma-separated
string (PT006).
- Replace self.params.dump() with dict(self.params) so the call works on both the
ParamsDict and the plain-dict legs of self.params' union type, satisfying
mypy union-attr.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks @eladkal — pushed fixes for both static check failures:

  • PT006 on the new @pytest.mark.parametrize calls (changed the first arg to a tuple of names).
  • union-attr mypy errors on self.params.dump() (self.params can be a plain dict; switched to dict(self.params) which works on both ParamsDict and plain dict).

Comment threadproviders/databricks/docs/operators/jobs_create.rst Outdated
@potiuk

Copy link
Copy Markdown
Member

@moomindani — There is 1 unresolved review thread on this PR from @Lee-W. Could you either push a fix or reply in the thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks!


Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you.

Address Lee-W's review feedback that the auto-injection example was hard to
parse. Each operator's section now:
- Names the exact Databricks API field being populated and links to its
schema (parameters / job_parameters / per-task slots).
- States explicitly that each <key>: <value> pair in params becomes one
{"name": <key>, "default": <value>} entry (CreateJobs) or is passed
through unchanged (RunNow / SubmitRun).
- Splits params into a named variable in the CreateJobs example so the
key/value to name/default mapping reads top-to-bottom.
@moomindani

Copy link
Copy Markdown
ContributorAuthor

Thanks for the ping @potiuk — replied in the Lee-W thread and pushed e4c73f3 clarifying the params-to-Databricks-API shape mapping in the docs. Resolving the thread now.

@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:databricksready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow passing airflow params as job parameter in databricks job

5 participants

@moomindani@eladkal@potiuk@Lee-W@subham611