{"record":{"id":"3478a8af5f17ef34","repo":"kestra-io/kestra","slug":"the-subflow-function-exceeded-the-maximum-nestin","errorCode":null,"errorMessage":"The 'subflow' function exceeded the maximum nesting depth of %s (a subflow's inputs likely call subflow() recursively).","messagePattern":"The 'subflow' function exceeded the maximum nesting depth of (.+?) \\(a subflow's inputs likely call subflow\\(\\) recursively\\)\\.","errorType":"exception","errorClass":"PebbleException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/io/kestra/core/runners/pebble/functions/SubflowFunction.java","lineNumber":139,"sourceCode":"        if (flow == null) {\n            throw new PebbleException(\n                null, \"The 'subflow' function can only be used in a flow context (e.g. an input's 'values'); the caller flow could not be resolved.\", lineNumber, self.getName()\n            );\n        }\n        String tenantId = flow.get(\"tenantId\");\n        String callerNamespace = flow.get(NAMESPACE_ARG);\n        String callerId = flow.get(\"id\");\n\n        Optional<Integer> revision = Optional.ofNullable(args.get(REVISION_ARG)).map(r -> ((Number) r).intValue());\n        Map<String, Object> rawInputs = (Map<String, Object>) args.get(INPUTS_ARG);\n        Map<String, Object> inputs = rawInputs != null ? rawInputs : Map.of();\n\n        List<Label> labels = buildLabels(args.get(LABELS_ARG), self, lineNumber);\n        Duration timeout = resolveTimeout(args.get(TIMEOUT_ARG), self, lineNumber);\n\n        int depth = DEPTH.get();\n        if (depth >= configuration.maxDepth()) {\n            throw new PebbleException(\n                null, \"The 'subflow' function exceeded the maximum nesting depth of \" + configuration.maxDepth()\n                    + \" (a subflow's inputs likely call subflow() recursively).\",\n                lineNumber, self.getName()\n            );\n        }\n\n        DEPTH.set(depth + 1);\n        try {\n            // ACL is scoped to the caller flow (callerNamespace/callerId), matching the Subflow task trust model:\n            // if the caller flow may reference the target, so may subflow(). Note this is reachable at execute-form\n            // render time, not only at execution time, so anyone able to open the form triggers this resolution.\n            // resolved for runtime so a governance rejection surfaces as a FlowWithException here, rather than\n            // becoming a created-then-failed execution\n            FlowWithSource targetFlow = flowMetaStore.get()\n                .findByIdFromTaskForRuntime(tenantId, namespace, id, revision, tenantId, callerNamespace, callerId)\n                .orElseThrow(\n                    () -> new PebbleException(\n                        null, \"Unable to find flow '\" + namespace + \"'.'\" + id + \"'\"","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/kestra-io/kestra/blob/823fada9274c4f9c251ea0a516460a4f7d958032/core/src/main/java/io/kestra/core/runners/pebble/functions/SubflowFunction.java#L121-L157","documentation":"The subflow() function uses a ThreadLocal depth counter to guard against runaway recursion — a subflow whose own inputs call subflow(), whose inputs call subflow(), and so on. Each entry increments the counter; when it reaches the configured maximum depth (SubflowFunctionConfiguration.maxDepth), the function aborts. This catches both direct self-recursion (flow A's input calls subflow(A)) and mutual recursion (A calls B, B calls A) because input resolution runs synchronously on the same thread.","triggerScenarios":"Flow A has a SELECT input with values: {{ subflow(namespace='ns', id='A').outputs.x }}. Flow B has a similar input calling subflow('B'). When the form for A renders, it triggers B, which triggers A, which triggers B... Two or more flows form a cycle through their input-value subflow() calls.","commonSituations":"Flows that mutually reference each other for dynamic input options (e.g., a cascading select where each level calls a different flow that itself has subflow()-driven inputs). Accidental self-reference when a flow's input values call subflow on itself. Refactoring that introduced a circular dependency.","solutions":["Break the cycle: ensure no chain of subflow() input-value calls forms a loop. Use static data or outputs from completed executions for dependent values.","If you need cascading selects, fetch all options in a single subflow call rather than chaining subflow() calls across flows.","Increase the max depth in SubflowFunctionConfiguration if legitimate deep (but non-circular) nesting is required (use with caution)."],"exampleFix":"# before — flow A calls flow B which calls flow A (circular)\n# flow_a.yaml\ninputs:\n  - id: region\n    type: SELECT\n    values: \"{{ subflow(namespace='ns', id='flow_b').outputs.regions }}\"\n# flow_b.yaml\ninputs:\n  - id: prefix\n    type: SELECT\n    values: \"{{ subflow(namespace='ns', id='flow_a').outputs.prefixes }}\"\n\n# after — break the cycle; fetch data from a single source\n# flow_a.yaml\ninputs:\n  - id: region\n    type: SELECT\n    values: \"{{ subflow(namespace='ns', id='data_provider').outputs.regions }}\"","handlingStrategy":"validation","validationCode":"# Design flows so no cycle exists in subflow() input-value calls.\n# Map out the dependency graph: if A's input calls subflow(B) and B's input calls subflow(A),\n# you have a cycle — break it by sourcing data from a terminal (non-subflow) provider.\n# There is no runtime Pebble guard; this must be correct by design.","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Draw the subflow() call graph and verify it is a DAG (no cycles).","Use a single data-provider flow at the root of the dependency tree.","Never let a flow's input values call subflow() on itself.","Review SubflowFunctionConfiguration.maxDepth and ensure it is not set too high for your use case."],"tags":["pebble","subflow","recursion","depth-limit","circular-dependency"],"backgroundTag":null,"analyzedSha":"823fada9274c4f9c251ea0a516460a4f7d958032","analyzedAt":"2026-08-14T06:15:17.947Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}