kestra-io/kestra · error · PebbleException

Maximum render() nesting depth (%s) exceeded at line %s — ch

Error message

Maximum render() nesting depth (%s) exceeded at line %s — check for circular render() calls in your template.

What it means

The render() function re-invokes the variable renderer on a given value. Each nested call increments a depth counter stored in the evaluation context. When the depth reaches the configured maximum (kestra.variables.max-render-depth, default is typically a small number), the function aborts to prevent infinite recursion. This is a safety valve against templates that call render() on values that themselves contain render() expressions, creating unbounded expansion.

Source

Thrown at core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java:49

    public List<String> getArgumentNames() {
        return List.of("toRender", "recursive");
    }

    @Override
    public Map<String, String> getArgumentDefaults() {
        return Map.of(
            "toRender", "inputs.inputWithPebble",
            "recursive", "true"
        );
    }

    @Override
    public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
        int depth = context.getVariable(VariableRenderer.RENDER_DEPTH_VAR) instanceof Number n ? n.intValue() : 0;
        int maxDepth = variableConfiguration.getMaxRenderDepth();
        if (depth >= maxDepth) {
            throw new PebbleException(
                null,
                "Maximum render() nesting depth (" + maxDepth + ") exceeded at line " + lineNumber +
                    " — check for circular render() calls in your template.",
                lineNumber, self.getName()
            );
        }

        if (!args.containsKey("toRender")) {
            throw new PebbleException(null, "The 'render' function expects an argument 'toRender'.", lineNumber, self.getName());
        }
        Object toRender = args.get("toRender");

        Object recursiveArg = args.get("recursive");
        if (recursiveArg == null) {
            recursiveArg = true;
        }

        if (!(recursiveArg instanceof Boolean recursive)) {

View on GitHub (pinned to 823fada927)

Solutions

  1. Audit the value being rendered for any embedded {{ render(...) }} or self-referential patterns and break the cycle.
  2. Call render() with recursive=false if you only need a single pass: {{ render(inputs.x, recursive=false) }}.
  3. Increase kestra.variables.max-render-depth in configuration if legitimate deep (but non-circular) nesting exists.
  4. Restructure the flow so the value is computed once in a dedicated task and passed as an output, rather than re-rendered in a loop.

Example fix

# before — circular render
inputs:
  - id: x
    type: STRING
    defaults: "{{ render(inputs.x) }}"
tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "{{ render(inputs.x) }}"

# after — single-pass render, no self-reference
inputs:
  - id: x
    type: STRING
    defaults: "hello {{ 'world' }}"
tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "{{ render(inputs.x, recursive=false) }}"
Defensive patterns

Strategy: validation

Validate before calling

# Avoid render() on values that may contain render() themselves.
# Use recursive=false for single-pass rendering to avoid depth buildup.
# In flow YAML, prefer:
#   {{ render(toRender=inputs.x, recursive=false) }}
# over recursive=true when you only need one expansion level.
# Verify the value does not contain '{{ render' before recursive rendering:
{% if not (inputs.x contains 'render(') %}{{ render(toRender=inputs.x) }}{% else %}RENDER_LOOP_AVOIDED{% endif %}

Prevention

When it happens

Trigger: A template expression like {{ render(inputs.x) }} where inputs.x contains the string '{{ render(inputs.x) }}'. A flow input whose value is dynamically populated from another source that embeds a render() call. Recursive rendering with recursive=true on a self-referential variable.

Common situations: Migrating from recursive global rendering to explicit render() calls and accidentally creating a loop. A flow input fed back into itself via webhook payloads or external systems that echo rendered output back into the template. Complex templating where multiple layers of indirection compound the depth beyond the limit.

Related errors


AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14). Data as JSON: /api/errors/a5d60f5c4929938f. Report an issue: GitHub.