dbt-labs/dbt-core · error · minijinja::Error (InvalidOperation)

compiled_code must be a string

Error message

compiled_code must be a string

What it means

`submit_python_job` accepts the compiled Python code only as a string; the second argument is coerced via `as_str()` and this error is thrown when it is not a string value. The library throws it because the adapter's `submit_python_job` method forwards the value directly and cannot accept other types.

Solutions

  1. Pass the compiled code as a string, e.g. the rendered result of the python model's code macro.
  2. If the value is an object, serialize/render it to a string before calling: `{% do submit_python_job(model, compiled_code | string) %}`.
  3. Log/print the argument with `| string` in the template to verify it holds actual Python source.

Example fix

-- before
{% set code = my_python_code_macro(model) %}{% do submit_python_job(model, code) %}
-- after
{% set code = my_python_code_macro(model) | string %}{% do submit_python_job(model, code) %}
Defensive patterns

Strategy: type-guard

Validate before calling

{% if compiled_code is not string %}
  {% set compiled_code = compiled_code | string %}
{% endif %}
{% do submit_python_job(model, compiled_code) %}

Type guard

// minijinja
fn is_string_value(v: &MinijinjaValue) -> bool { v.as_str().is_some() }

Try / catch

try:
    submit_python_job(model, code)
except Error as e:
    if "compiled_code must be a string" in str(e):
        raise ValueError("pass rendered python source as a string") from e
    raise

Prevention

When it happens

Trigger: Calling `submit_python_job(model, some_non_string)` where compiled_code is e.g. a dict, a number, or an unset/undefined Minijinja value instead of the rendered Python source string.

Common situations: Passing a macro result object instead of its string; forgetting to `render()` or call the macro that produces the code; a variable holding `none` because compilation failed silently upstream.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/33adcf1baae03aa1. Report an issue: GitHub.

Appendix: source

Thrown at crates/dbt-jinja-utils/src/phases/run/run_node_context.rs:801

            format!("Failed to write to {}: {}", full_path.display(), e),
        )),
    }
}

/// Returns the function used for the submit_python_job context.
fn submit_python_job_context_fn()
-> impl Fn(&State, &[MinijinjaValue]) -> Result<MinijinjaValue, Error> + Copy {
    |state: &State, args: &[MinijinjaValue]| {
        // Parse arguments: submit_python_job(parsed_model, compiled_code)
        if args.len() != 2 {
            return Err(Error::new(
                ErrorKind::InvalidOperation,
                format!("submit_python_job expects 2 arguments, got {}", args.len()),
            ));
        }
        let parsed_model = &args[0];
        let compiled_code = args[1].as_str().ok_or_else(|| {
            Error::new(
                ErrorKind::InvalidOperation,
                "compiled_code must be a string",
            )
        })?;

        // Note(Ani):
        // dbt-core validates:
        //   - macro_stack.depth == 2
        //   - call_stack[1] == "macro.dbt.statement"
        //   - "materialization" in call_stack[0]
        //
        // In fusion, we shouldn't need to do this because this funciton is only registered in the run node context
        // so if a user tries to use it outside of a statement.sql macro, in a materialization macro, it will fail earlier due to an unrecongized function call.

        // Get adapter from context and call submit_python_job
        let adapter = state
            .lookup("adapter", &[])
            .ok_or_else(|| Error::new(ErrorKind::UndefinedError, "adapter not found in context"))?;

View on GitHub (pinned to 0267ce9170)