dbt-labs/dbt-core · error · InvalidOperation

Argument must be a string

Error message

Argument must be a string

What it means

After the arity check, render() requires its single argument to be a string so it can compile it as a Jinja template. If the argument is any non-string, non-none value (number, dict, list, bool), as_str() fails and this InvalidOperation error is thrown. This mirrors stricter minijinja typing versus dbt-core's permissive Python stringification.

Source

Thrown at crates/dbt-jinja-utils/src/functions/base.rs:741

    move |state: &State, args: &[Value], _kwargs: Kwargs| -> Result<Value, Error> {
        if args.len() != 1 {
            return Err(Error::new(
                ErrorKind::InvalidOperation,
                "render requires exactly one argument (the string to render)",
            ));
        }
        // dbt-core (Jinja2/Python) effectively accepts any value here and stringifies it.
        // In practice, many dbt projects call `render(...)` on values that can legitimately be
        // `none` (e.g. optional metadata-driven SQL snippets from `run_query`), expecting
        // `"None"` and handling that downstream.
        //
        // Fusion uses minijinja which is stricter by default; align behavior by accepting
        // `none` and treating it like Python's `str(None)` => `"None"`.
        let sql = if args[0].is_none() {
            "None"
        } else {
            args[0].as_str().ok_or_else(|| {
                Error::new(ErrorKind::InvalidOperation, "Argument must be a string")
            })?
        };

        let env = state.env();

        let template = env.template_from_str(sql)?;
        let rendered = template.render(state.get_base_context(), &[])?;
        Ok(Value::from(rendered))
    }
}

/// Strict version of set() that fails if the input is not iterable.
///
/// Args:
///     value: An iterable value to convert to a set
///
/// Example:
/// ```jinja

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Convert the value to a string before calling: render(value | string)
  2. For numbers/bools use ~ or |string interpolation to build the template string
  3. Keep the none special case in mind: none is accepted and becomes 'None'

Example fix

// before
{% set out = render(row_count) %}
// after
{% set out = render(row_count | string) %}
Defensive patterns

Strategy: type-guard

Validate before calling

{% if value is not string and value is not none %}
  {% set value = value | string %}
{% endif %}
{% set out = render(value) %}

Type guard

{% macro ensure_str(x) %}{{ return(x if x is string else (x | string)) }}{% endmacro %}

Prevention

When it happens

Trigger: Calling render(123), render(my_dict), render(my_list) or passing any Jinja value that is not a string and not none. Also occurs when a variable silently holds a non-string type (e.g. a number from a config).

Common situations: Variables read from YAML config or a query result that are numbers/objects rather than strings; assuming dbt-core's behavior of str()-ing any value carries over to Fusion; concatenation that produced a non-string container instead of a string.

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/e609eac2f3144f2b. Report an issue: GitHub.