{"record":{"id":"a5d60f5c4929938f","repo":"kestra-io/kestra","slug":"maximum-render-nesting-depth-s-exceeded-at-li","errorCode":null,"errorMessage":"Maximum render() nesting depth (%s) exceeded at line %s — check for circular render() calls in your template.","messagePattern":"Maximum render\\(\\) nesting depth \\((.+?)\\) exceeded at line (.+?) — check for circular render\\(\\) calls in your template\\.","errorType":"exception","errorClass":"PebbleException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java","lineNumber":49,"sourceCode":"\n    public List<String> getArgumentNames() {\n        return List.of(\"toRender\", \"recursive\");\n    }\n\n    @Override\n    public Map<String, String> getArgumentDefaults() {\n        return Map.of(\n            \"toRender\", \"inputs.inputWithPebble\",\n            \"recursive\", \"true\"\n        );\n    }\n\n    @Override\n    public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {\n        int depth = context.getVariable(VariableRenderer.RENDER_DEPTH_VAR) instanceof Number n ? n.intValue() : 0;\n        int maxDepth = variableConfiguration.getMaxRenderDepth();\n        if (depth >= maxDepth) {\n            throw new PebbleException(\n                null,\n                \"Maximum render() nesting depth (\" + maxDepth + \") exceeded at line \" + lineNumber +\n                    \" — check for circular render() calls in your template.\",\n                lineNumber, self.getName()\n            );\n        }\n\n        if (!args.containsKey(\"toRender\")) {\n            throw new PebbleException(null, \"The 'render' function expects an argument 'toRender'.\", lineNumber, self.getName());\n        }\n        Object toRender = args.get(\"toRender\");\n\n        Object recursiveArg = args.get(\"recursive\");\n        if (recursiveArg == null) {\n            recursiveArg = true;\n        }\n\n        if (!(recursiveArg instanceof Boolean recursive)) {","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/kestra-io/kestra/blob/823fada9274c4f9c251ea0a516460a4f7d958032/core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java#L31-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Audit the value being rendered for any embedded {{ render(...) }} or self-referential patterns and break the cycle.","Call render() with recursive=false if you only need a single pass: {{ render(inputs.x, recursive=false) }}.","Increase kestra.variables.max-render-depth in configuration if legitimate deep (but non-circular) nesting exists.","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."],"exampleFix":"# before — circular render\ninputs:\n  - id: x\n    type: STRING\n    defaults: \"{{ render(inputs.x) }}\"\ntasks:\n  - id: log\n    type: io.kestra.plugin.core.log.Log\n    message: \"{{ render(inputs.x) }}\"\n\n# after — single-pass render, no self-reference\ninputs:\n  - id: x\n    type: STRING\n    defaults: \"hello {{ 'world' }}\"\ntasks:\n  - id: log\n    type: io.kestra.plugin.core.log.Log\n    message: \"{{ render(inputs.x, recursive=false) }}\"","handlingStrategy":"validation","validationCode":"# Avoid render() on values that may contain render() themselves.\n# Use recursive=false for single-pass rendering to avoid depth buildup.\n# In flow YAML, prefer:\n#   {{ render(toRender=inputs.x, recursive=false) }}\n# over recursive=true when you only need one expansion level.\n# Verify the value does not contain '{{ render' before recursive rendering:\n{% if not (inputs.x contains 'render(') %}{{ render(toRender=inputs.x) }}{% else %}RENDER_LOOP_AVOIDED{% endif %}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Audit any value passed to render() for embedded render() calls or self-referential patterns.","Default to recursive=false unless you explicitly need multi-pass rendering.","Keep render depth shallow by computing intermediate values in dedicated tasks.","Monitor the kestra.variables.max-render-depth setting and understand your flow's nesting requirements."],"tags":["pebble","render","recursion","depth-limit","configuration"],"backgroundTag":null,"analyzedSha":"823fada9274c4f9c251ea0a516460a4f7d958032","analyzedAt":"2026-08-14T06:15:17.947Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}