Skip to content

direct engine: neither $${...} nor \${...} escapes a literal ${...} in a string field - both still treated as an interpolation reference #6480

Description

@Himaanshu-CT

Describe the issue

A string field (e.g. a job's base_parameters) sometimes needs to contain a literal ${name} for its own downstream purposes — unrelated to any bundle variable, resource substitution, or Terraform interpolation. Under the Terraform engine, this was correctly escaped using Terraform's documented $${...} convention (double dollar sign).

After our bundle was migrated to the direct engine, $${...} is no longer recognized as escaped. Neither is the alternative \${...} (backslash) convention. Both are treated identically to a fully unescaped ${...} and fail with "invalid dependency" errors.

Why we need this

Job parameters are often the input to a generic, metadata-driven ingestion notebook: a single notebook implementation is shared across many similar jobs, and each job supplies its own JSON "config template" plus a list of concrete parameter sets for each source it ingests. At runtime, our own notebook code resolves placeholders like ${source_file}, ${business_date[0..9]} (a custom slicing syntax we implement ourselves, not standard Databricks/Python syntax), ${batch_id}, etc. against values from the job's own declared parameters ({{job.id}}, {{job.start_time.iso_date}}, ...) and from the matching entry in a parallel parameters_list array. None of this resolution happens via, or is related to, Databricks bundle variables or Terraform interpolation -- the bundle's only job is to deploy these string values as written; substitution happens entirely inside our own notebook code, after the job has already started running.

A simplified, representative excerpt (structure preserved, business-specific names genericized):

resources:
  jobs:
    example_ingestion_job:
      tasks:
        - task_key: run_generic_ingestion
          notebook_task:
            notebook_path: src/Notebooks/common/elt/Generic_Runs
            base_parameters:
              generic_config: '{"source": {"path": "landing/$${p_folder}/$${source_file}_*.csv"}, "schema": {"columns": [{"name": "w_batch_id", "expression": "$${batch_id}"}, {"name": "w_business_dt", "expression": "cast(''$${business_date[0..9]}'' as date)"}]}, "target": {"table": "${var.catalog_name}.$${p_env_stg}.$${target_table}"}}'
              parameters_list: '[{"source_file": "TableA", "target_table": "table_a", "p_env_stg": "stg_example", "p_folder": "foldera"}, {"source_file": "TableB", "target_table": "table_b", "p_env_stg": "stg_example", "p_folder": "folderb"}]'
      parameters:
        - name: batch_id
          default: '{{job.id}}'
        - name: business_date
          default: '{{job.start_time.iso_date}}'

Note ${var.catalog_name} sitting in the same string as several $${...}-escaped literals -- a genuine bundle variable that does need to resolve, right alongside content that must not be. Any fix needs to let both coexist correctly in the same field, the same way they did under the Terraform engine.

This pattern appears in dozens of our job definitions -- metadata-driven ingestion is our standard approach for any source with a repeatable ingest-and-load shape -- so this isn't an edge case for us. It currently blocks every deploy, and every "invalid dependency" error we've hit in production (${source_file}, ${business_date}, ${batch_id}, ${folder}, each on a different attempt) is the same underlying problem surfacing on whichever escaped placeholder the dependency-graph pass happens to reach first -- not several unrelated bugs.

Direct question for maintainers: is there a currently-supported way to escape a literal ${...} in a string field under the direct engine? We couldn't find one documented, and our minimal repro below shows neither of the two conventions we tried works.

Steps to reproduce

Minimal databricks.yml:

bundle:
  name: escape_test

workspace:
  host: https://<workspace-host>
  root_path: /Workspace/Users/<username>/escape_test

resources:
  jobs:
    escape_test_job:
      name: escape_test_job
      tasks:
        - task_key: test
          notebook_task:
            notebook_path: /Workspace/Users/<username>/dummy
            base_parameters:
              option_a: '$${test_var}'

Run:

databricks bundle plan

Result:

Error: invalid dependency "${test_var}", no such node ""

Now replace the parameter with the backslash form instead:

              option_a: '\${test_var}'

Same result, same error shape:

Error: invalid dependency "${test_var}", no such node ""

A fully unescaped control case ('${test_var}', no leading character at all) fails identically. In every case, the error message shows exactly one $ before {test_var} — regardless of whether the source had $$, \$, or a single unescaped $. This suggests the dependency-graph pass is scanning for the literal substring ${ anywhere in the string, independent of whatever character precedes it, rather than checking for either escape convention.

Expected behavior

Some mechanism — ideally $${...}, matching Terraform's own documented escaping and our prior working behavior — should let a literal ${...} appear in a string field without being treated as an interpolation reference, the same way it worked under the Terraform engine.

Actual behavior

$${...}, \${...}, and a fully unescaped ${...} all fail identically with invalid dependency "${name}", no such node "".

Is this a regression?

Yes. This exact $${...} pattern worked correctly under the Terraform engine (last confirmed working on CLI v1.13.0). It broke after our bundle was automatically migrated to the direct engine — triggered by upgrading to CLI v1.14.0, per that release's note: "Bundles still on Terraform state are now migrated to the direct engine automatically, after a deploy whose dry-run conversion comes back clean."

We also confirmed this can't be reverted by setting bundle.engine: terraform locally — the CLI reports:

Warning: Deployment engine "terraform" configured in bundle.engine setting ... does not match the existing state (engine "direct"). Using "direct" engine from the existing state.

The remote state itself now forces the direct engine regardless of local configuration, so there's no way back to the previously-working behavior for an already-migrated bundle.

OS and CLI version

  • OS: Windows
  • CLI version tested: Databricks CLI v1.14.1
  • Also reproduced in our CI pipeline (Azure DevOps, Ubuntu hosted agent) on CLI v1.14.1

Additional context

This pattern is common for us across dozens of job definitions — job parameters frequently hold JSON-encoded configuration blobs where application-level placeholders (e.g. ${source_file}, ${business_date}) are resolved by downstream notebook code at runtime, not by the bundle itself. All of these were previously escaped correctly via Terraform's $$ convention. We couldn't find documentation describing the direct engine's equivalent for this specific case — the docs we found (interpolation parser rewrite, "Bash convention" \$ escaping) describe escape handling for malformed/typo'd variable references (e.g. catching ${var.my_clster_id} as a likely typo), which appears to be a different code path from escaping a deliberately literal, non-bundle ${...} in an arbitrary string field.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingDABsDABs related issuesengine/directSpecific to direct deployment engine in Databricks Asset Bundles

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions