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:
/// ```jinjaView on GitHub (pinned to 0267ce9170)
Solutions
- Convert the value to a string before calling: render(value | string)
- For numbers/bools use ~ or |string interpolation to build the template string
- 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
- Pipe non-strings through |string before render
- Check var()/config() return types — they may be numbers or dicts
- Do not rely on dbt-core's implicit str() behavior; Fusion is stricter
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
- Column 'name' must be a string
- Column 'data_type' must be a string
- Failed to convert payload to string
- {type(df)} is not a supported type for dbt Python materializ
- describe_dynamic_table is not supported by the {} adapter
AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07).
Data as JSON: /api/errors/e609eac2f3144f2b.
Report an issue: GitHub.