Repository: kestra-io/kestra
Issue: Disable Pebble rendering on webhook trigger variables (keep rendering secrets/env vars on key, but don't parse the trigger body)
Labels: enhancement

### Feature description

## Problem 

Take the following flow as example:

```yaml
id: github_to_notion
namespace: product

concurrency:
  behavior: QUEUE
  limit: 1

inputs:
  - name: github_url
    type: STRING
    defaults: https://github.com/kestra-io/docs/issues/678

variables:
  github_url: "{{ trigger.body.pull_request.html_url ?? trigger.body.issue.html_url ?? inputs.github_url }}"

tasks:
  - id: github_to_notion
    type: io.kestra.plugin.scripts.python.Commands
    warningOnStdErr: false
    env:
      GITHUB_TOKEN: "{{ secret('GITHUB_TOKEN') }}"
      NOTION_TOKEN: "{{ secret('NOTION_TOKEN') }}"
      DATABASE_ID: "81a8f4c0b1af4f3481da3b9bcab5537b"
    beforeCommands:
      - pip install requests markdown2 beautifulsoup4 --no-warn-script-location
    runner: PROCESS
    namespaceFiles:
      enabled: true
    commands:
      - python scripts/github_to_notion.py "{{ vars.github_url }}"

triggers:
  - id: github
    type: io.kestra.core.models.triggers.types.Webhook
    key: "mykey"
```

It takes a GitHub issue URL and loads it to Notion.  

The execution triggered by a webhook fails with error:

```
Missing variable: 'secrets' on 'Hi, I've set-up a github action as per readme.
...
Root attribute [secrets] does not exist or can not be accessed and strict variables is set to true.
```

## Suggested solution

Disable Pebble rendering on webhook trigger variables. 

![image](https://github.com/kestra-io/kestra/assets/86264395/6c3fe9e3-6629-4c30-a317-ea9217eacc9a)
  

Issue comments:
Notion automation got a bit crazy, sorry Ludo for this back and forth on changing Milestone, haha - this wasn't me, this was a flow that got mad
We won't tackle the recursive vs non-recursive approach for 0.14.0. Will be tackled for 0.15.0

Pull request review comments:
I had no choice but to put them as static since I need to access them in the new static `renderOnce` & `renderRecursively` methods which are also called from RenderFunction (and in such Pebble functions you don't have access to Micronaut context or the whole PebbleEngine)
No, your function can themself be CDI beans, this is what we do already for some of them.
Look inside `io.kestra.core.runners.pebble.Extension`
Not great to override a parameter
This is strange that it takes an integer argument, I think this should be an implementation detail.
I thought we want to discourage recursiveness, in this case shouldn't it be false by default?
By default, we don't render recursively. If you use the render function you opt-in to render (recursively if needed)  explicitly. 

recursive=False is an edge case to render only once as for trigger payload

I'll add a draft PR to docs to show based on examples 👍 

(so this is intentional)
That's true my bad
https://github.com/kestra-io/docs/pull/776
I had to move the VariableRenderer bean injection to a runtime one as it's a circular dependency otherwise (VariableRenderer spawns the Pebble extension which spawns this function which needs VariableRenderer) so fetching it a runtime will work
```suggestion
    rivate String renderRecursively(int renderingCount, String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
```
Why is it nullable?
I think you can do it at postConstruct time instead of doing it each time
Is it really needed? This looks like a bad idea (same for the other getter)
Because the function isn't created only if the recursive settings is set to true
Oh god!
Maybe it would be better to create it and throw, with this we would be able to display a better error message.
Anyway, OK.
No because PostConstruct is still within the Lifecycle of the bean (if it can't execute, the bean isn't in the context) so if I try to get the variableRenderer in PostConstruct it will circle as the variableRenderer will need that bean :'( 
I forgot to remove it
Ludo advised not to inject the function and let Pebble say that this function doesn't exist, it will be documented that render function cannot be used with recursive setting

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/runners/VariableRenderer.java b/core/src/main/java/io/kestra/core/runners/VariableRenderer.java
index 95fcd42dc2..e63f74eea5 100644
--- a/core/src/main/java/io/kestra/core/runners/VariableRenderer.java
+++ b/core/src/main/java/io/kestra/core/runners/VariableRenderer.java
@@ -5,11 +5,6 @@
 import com.github.jknack.handlebars.HandlebarsException;
 import com.github.jknack.handlebars.Template;
 import com.github.jknack.handlebars.helper.*;
-import io.pebbletemplates.pebble.PebbleEngine;
-import io.pebbletemplates.pebble.error.AttributeNotFoundException;
-import io.pebbletemplates.pebble.error.PebbleException;
-import io.pebbletemplates.pebble.extension.AbstractExtension;
-import io.pebbletemplates.pebble.template.PebbleTemplate;
 import io.kestra.core.exceptions.IllegalVariableEvaluationException;
 import io.kestra.core.runners.handlebars.VariableRendererPlugins;
 import io.kestra.core.runners.handlebars.helpers.*;
@@ -18,6 +13,15 @@
 import io.kestra.core.runners.pebble.PebbleLruCache;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.context.annotation.ConfigurationProperties;
+import io.micronaut.core.annotation.Nullable;
+import io.pebbletemplates.pebble.PebbleEngine;
+import io.pebbletemplates.pebble.error.AttributeNotFoundException;
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.extension.AbstractExtension;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import lombok.Getter;
 
 import java.io.IOException;
 import java.io.StringWriter;
@@ -26,17 +30,13 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import io.micronaut.core.annotation.Nullable;
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import lombok.Getter;
-
 @Singleton
 public class VariableRenderer {
     private static final Pattern RAW_PATTERN = Pattern.compile("\\{%[-]*\\s*raw\\s*[-]*%\\}(.*?)\\{%[-]*\\s*endraw\\s*[-]*%\\}");
+    public static final int MAX_RENDERING_AMOUNT = 100;
 
     private Handlebars handlebars;
-    private final PebbleEngine pebbleEngine;
+    private PebbleEngine pebbleEngine;
     private final VariableConfiguration variableConfiguration;
 
     @SuppressWarnings("unchecked")
@@ -89,10 +89,27 @@ public VariableRenderer(ApplicationContext applicationContext, @Nullable Variabl
             pebbleBuilder.templateCache(new PebbleLruCache(this.variableConfiguration.getCacheSize()));
         }
 
-        pebbleEngine = pebbleBuilder.build();
+        this.pebbleEngine = pebbleBuilder.build();
+    }
+
+    public static IllegalVariableEvaluationException properPebbleException(PebbleException e) {
+        if (e instanceof AttributeNotFoundException current) {
+            return new IllegalVariableEvaluationException(
+                "Missing variable: '" + current.getAttributeName() +
+                    "' on '" + current.getFileName() +
+                    "' at line " + current.getLineNumber(),
+                e
+            );
+        }
+
+        return new IllegalVariableEvaluationException(e);
+    }
+
+    public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.render(inline, variables, this.variableConfiguration.getRecursiveRendering());
     }
 
-    public String recursiveRender(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+    public String render(String inline, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
         if (inline == null) {
             return null;
         }
@@ -102,81 +119,83 @@ public String recursiveRender(String inline, Map<String, Object> variables) thro
             return inline;
         }
 
+        return recursive
+            ? renderRecursively(inline, variables)
+            : renderOnce(inline, variables);
+    }
+
+    public String renderOnce(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
         // pre-process raw tags
         Matcher rawMatcher = RAW_PATTERN.matcher(inline);
         Map<String, String> replacers = new HashMap<>((int) Math.ceil(rawMatcher.groupCount() / 0.75));
-        String currentTemplate = rawMatcher.replaceAll(result -> {
+        String result = rawMatcher.replaceAll(matchResult -> {
             var uuid = UUID.randomUUID().toString();
-            replacers.put(uuid, result.group(1));
+            replacers.put(uuid, matchResult.group(1));
             return uuid;
         });
 
-        boolean isSame = false;
-        String current = "";
-        PebbleTemplate compiledTemplate;
-        while (!isSame) {
-            try {
-                compiledTemplate = pebbleEngine.getLiteralTemplate(currentTemplate);
-
-                Writer writer = new JsonWriter(new StringWriter());
-                compiledTemplate.evaluate(writer, variables);
-                current = writer.toString();
-            } catch (IOException | PebbleException e) {
-                if (this.variableConfiguration.disableHandlebars) {
-                    if (e instanceof PebbleException) {
-                        throw properPebbleException((PebbleException) e);
-                    }
-
-                    throw new IllegalVariableEvaluationException(e);
-                }
+        try {
+            PebbleTemplate compiledTemplate = this.pebbleEngine.getLiteralTemplate(result);
 
-                try {
-                    Template  template = handlebars.compileInline(currentTemplate);
-                    current = template.apply(variables);
-                } catch (HandlebarsException | IOException hbE) {
-                    throw new IllegalVariableEvaluationException(
-                        "Pebble evaluation failed with '" + e.getMessage() +  "' " +
-                        "and Handlebars fallback failed also  with '" + hbE.getMessage() + "'" ,
-                        e
-                    );
+            Writer writer = new JsonWriter(new StringWriter());
+            compiledTemplate.evaluate(writer, variables);
+            result = writer.toString();
+        } catch (IOException | PebbleException e) {
+            if (this.handlebars == null) {
+                if (e instanceof PebbleException) {
+                    throw properPebbleException((PebbleException) e);
                 }
+
+                throw new IllegalVariableEvaluationException(e);
             }
 
-            isSame = currentTemplate.equals(current);
-            currentTemplate = current;
+            try {
+                Template template = this.handlebars.compileInline(inline);
+                result = template.apply(variables);
+            } catch (HandlebarsException | IOException hbE) {
+                throw new IllegalVariableEvaluationException(
+                    "Pebble evaluation failed with '" + e.getMessage() + "' " +
+                        "and Handlebars fallback failed also  with '" + hbE.getMessage() + "'",
+                    e
+                );
+            }
         }
 
         // post-process raw tags
-        for(var entry: replacers.entrySet()) {
-            current = current.replace(entry.getKey(), entry.getValue());
+        for (var entry : replacers.entrySet()) {
+            result = result.replace(entry.getKey(), entry.getValue());
         }
-        return current;
+
+        return result;
     }
 
-    public IllegalVariableEvaluationException properPebbleException(PebbleException e) {
-        if (e instanceof AttributeNotFoundException current) {
-            return new IllegalVariableEvaluationException(
-                "Missing variable: '" + current.getAttributeName() +
-                    "' on '" + current.getFileName() +
-                    "' at line " + current.getLineNumber(),
-                e
-            );
+    public String renderRecursively(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.renderRecursively(0, inline, variables);
+    }
+
+    private String renderRecursively(int renderingCount, String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        if (renderingCount > MAX_RENDERING_AMOUNT) {
+            throw new IllegalVariableEvaluationException("Too many rendering attempts");
         }
 
-        return new IllegalVariableEvaluationException(e);
-    }
+        String result = this.renderOnce(inline, variables);
+        if (result.equals(inline)) {
+            return result;
+        }
 
-    public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
-        return this.recursiveRender(inline, variables);
+        return renderRecursively(++renderingCount, result, variables);
     }
 
-
     public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.render(in, variables, this.variableConfiguration.getRecursiveRendering());
+    }
+
+    public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
         Map<String, Object> map = new HashMap<>();
 
         for (Map.Entry<String, Object> r : in.entrySet()) {
             String key = this.render(r.getKey(), variables);
-            Object value = renderObject(r.getValue(), variables).orElse(r.getValue());
+            Object value = renderObject(r.getValue(), variables, recursive).orElse(r.getValue());
 
             map.putIfAbsent(
                 key,
@@ -187,24 +206,34 @@ public Map<String, Object> render(Map<String, Object> in, Map<String, Object> va
         return map;
     }
 
+    public Optional<Object> renderObject(Object object, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.renderObject(object, variables, this.variableConfiguration.getRecursiveRendering());
+    }
+
     @SuppressWarnings({"unchecked", "rawtypes"})
-    private Optional<Object> renderObject(Object object, Map<String, Object> variables) throws IllegalVariableEvaluationException {
-        if (object instanceof Map) {
-            return Optional.of(this.render((Map) object, variables));
-        } else if (object instanceof Collection) {
-            return Optional.of(this.renderList((List) object, variables));
-        } else if (object instanceof String) {
-            return Optional.of(this.render((String) object, variables));
+    public Optional<Object> renderObject(Object object, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
+        if (object instanceof Map map) {
+            return Optional.of(this.render(map, variables, recursive));
+        } else if (object instanceof List list) {
+            return Optional.of(this.renderList(list, variables, recursive));
+        } else if (object instanceof Set set) {
+            return Optional.of(this.render(set, variables, recursive));
+        } else if (object instanceof String string) {
+            return Optional.of(this.render(string, variables, recursive));
         }
 
         return Optional.empty();
     }
 
     public List<Object> renderList(List<Object> list, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.renderList(list, variables, this.variableConfiguration.getRecursiveRendering());
+    }
+
+    public List<Object> renderList(List<Object> list, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
         List<Object> result = new ArrayList<>();
 
         for (Object inline : list) {
-            this.renderObject(inline, variables)
+            this.renderObject(inline, variables, recursive)
                 .ifPresent(result::add);
         }
 
@@ -212,18 +241,26 @@ public List<Object> renderList(List<Object> list, Map<String, Object> variables)
     }
 
     public List<String> render(List<String> list, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.render(list, variables, this.variableConfiguration.getRecursiveRendering());
+    }
+
+    public List<String> render(List<String> list, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
         List<String> result = new ArrayList<>();
         for (String inline : list) {
-            result.add(this.recursiveRender(inline, variables));
+            result.add(this.render(inline, variables, recursive));
         }
 
         return result;
     }
 
-    public Set<String> render(Set<String> list, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+    public Set<String> render(Set<String> set, Map<String, Object> variables) throws IllegalVariableEvaluationException {
+        return this.render(set, variables, this.variableConfiguration.getRecursiveRendering());
+    }
+
+    public Set<String> render(Set<String> list, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
         Set<String> result = new HashSet<>();
         for (String inline : list) {
-            result.add(this.recursiveRender(inline, variables));
+            result.add(this.render(inline, variables, recursive));
         }
 
         return result;
@@ -236,10 +273,12 @@ public VariableConfiguration() {
             this.disableHandlebars = true;
             this.cacheEnabled = true;
             this.cacheSize = 1000;
+            this.recursiveRendering = false;
         }
 
         Boolean disableHandlebars;
         Boolean cacheEnabled;
         Integer cacheSize;
+        Boolean recursiveRendering;
     }
 }
diff --git a/core/src/main/java/io/kestra/core/runners/handlebars/helpers/EvalHelper.java b/core/src/main/java/io/kestra/core/runners/handlebars/helpers/EvalHelper.java
index ee2f941f0b..3233769bd6 100644
--- a/core/src/main/java/io/kestra/core/runners/handlebars/helpers/EvalHelper.java
+++ b/core/src/main/java/io/kestra/core/runners/handlebars/helpers/EvalHelper.java
@@ -18,9 +18,9 @@ public EvalHelper(VariableRenderer variableRenderer) {
     @SneakyThrows
     @Override
     public CharSequence apply(final String value, final Options options) {
-        String finalTemplate = variableRenderer.recursiveRender(value, (Map<String, Object>) options.context.model());
+        String finalTemplate = variableRenderer.renderRecursively(value, (Map<String, Object>) options.context.model());
 
-        return variableRenderer.recursiveRender("{{" + finalTemplate + "}}", (Map<String, Object>) options.context.model());
+        return variableRenderer.renderRecursively("{{" + finalTemplate + "}}", (Map<String, Object>) options.context.model());
     }
 }
 
diff --git a/core/src/main/java/io/kestra/core/runners/handlebars/helpers/FirstDefinedEvalHelper.java b/core/src/main/java/io/kestra/core/runners/handlebars/helpers/FirstDefinedEvalHelper.java
index 82ae8dfb67..b0d5f28b9b 100644
--- a/core/src/main/java/io/kestra/core/runners/handlebars/helpers/FirstDefinedEvalHelper.java
+++ b/core/src/main/java/io/kestra/core/runners/handlebars/helpers/FirstDefinedEvalHelper.java
@@ -38,9 +38,9 @@ public CharSequence apply(final String value, final Options options) {
         while (result == null && i < params.size()) {
             try {
                 String param = params.get(i++);
-                String finalTemplate = variableRenderer.recursiveRender(param, (Map<String, Object>) options.context.model());
+                String finalTemplate = variableRenderer.render(param, (Map<String, Object>) options.context.model());
 
-                result = variableRenderer.recursiveRender("{{" + finalTemplate + "}}", (Map<String, Object>) options.context.model());
+                result = variableRenderer.render("{{" + finalTemplate + "}}", (Map<String, Object>) options.context.model());
             } catch (IllegalVariableEvaluationException | IllegalStateException | HandlebarsException ignored) {
             }
         }
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/Extension.java b/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
index 3529889b90..e72a79d55d 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
@@ -1,6 +1,8 @@
 package io.kestra.core.runners.pebble;
 
+import io.kestra.core.runners.VariableRenderer;
 import io.kestra.core.runners.pebble.functions.*;
+import io.micronaut.core.annotation.Nullable;
 import io.pebbletemplates.pebble.extension.*;
 import io.pebbletemplates.pebble.operator.Associativity;
 import io.pebbletemplates.pebble.operator.BinaryOperator;
@@ -29,6 +31,10 @@ public class Extension extends AbstractExtension {
     @Inject
     private ReadFileFunction readFileFunction;
 
+    @Inject
+    @Nullable
+    private RenderFunction renderFunction;
+
     @Override
     public List<TokenParser> getTokenParsers() {
         return null;
@@ -84,15 +90,18 @@ public Map<String, Test> getTests() {
 
     @Override
     public Map<String, Function> getFunctions() {
-        Map<String, Function> tests = new HashMap<>();
-
-        tests.put("now", new NowFunction());
-        tests.put("json", new JsonFunction());
-        tests.put("currentEachOutput", new CurrentEachOutputFunction());
-        tests.put("secret", secretFunction);
-        tests.put("read", readFileFunction);
-
-        return tests;
+        Map<String, Function> functions = new HashMap<>();
+
+        functions.put("now", new NowFunction());
+        functions.put("json", new JsonFunction());
+        functions.put("currentEachOutput", new CurrentEachOutputFunction());
+        functions.put("secret", secretFunction);
+        functions.put("read", readFileFunction);
+        if (this.renderFunction != null) {
+            functions.put("render", renderFunction);
+        }
+
+        return functions;
     }
 
     @Override
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java b/core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java
new file mode 100644
index 0000000000..fb60921e01
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/runners/pebble/functions/RenderFunction.java
@@ -0,0 +1,61 @@
+package io.kestra.core.runners.pebble.functions;
+
+import io.kestra.core.exceptions.IllegalVariableEvaluationException;
+import io.kestra.core.runners.VariableRenderer;
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.core.util.StringUtils;
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.extension.Function;
+import io.pebbletemplates.pebble.template.EvaluationContext;
+import io.pebbletemplates.pebble.template.EvaluationContextImpl;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+@Requires(property = "kestra.variables.recursive-rendering", value = StringUtils.FALSE, defaultValue = StringUtils.FALSE)
+public class RenderFunction implements Function {
+    @Inject
+    private ApplicationContext applicationContext;
+
+    public List<String> getArgumentNames() {
+        return List.of("toRender", "recursive");
+    }
+
+    @Override
+    public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+        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)) {
+            throw new PebbleException(null, "The 'render' function expects an optional argument 'recursive' with type boolean.", lineNumber, self.getName());
+        }
+        Boolean recursive = (Boolean) recursiveArg;
+
+        EvaluationContextImpl evaluationContext = (EvaluationContextImpl) context;
+        Map<String, Object> variables = evaluationContext.getScopeChain().getGlobalScopes().stream()
+            .flatMap(scope -> scope.getKeys().stream())
+            .distinct()
+            .collect(HashMap::new, (m, v) -> m.put(v, context.getVariable(v)), HashMap::putAll);
+
+        VariableRenderer variableRenderer = applicationContext.getBean(VariableRenderer.class);
+
+        try {
+            return variableRenderer.renderObject(toRender, variables, recursive).orElse(null);
+        } catch (IllegalVariableEvaluationException e) {
+            throw new PebbleException(e, e.getMessage());
+        }
+    }
+}


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/handlebars/helpers/HandlebarsVariableRendererTest.java b/core/src/test/java/io/kestra/core/runners/handlebars/helpers/HandlebarsVariableRendererTest.java
index 18f4734294..b123c367ba 100644
--- a/core/src/test/java/io/kestra/core/runners/handlebars/helpers/HandlebarsVariableRendererTest.java
+++ b/core/src/test/java/io/kestra/core/runners/handlebars/helpers/HandlebarsVariableRendererTest.java
@@ -48,12 +48,19 @@ void map() throws IllegalVariableEvaluationException {
     void recursive() throws IllegalVariableEvaluationException {
         ImmutableMap<String, Object> vars = ImmutableMap.of(
             "first", "1",
-            "second", "{{third}}",
-            "third", "{{first}}"
+            "second", "{{first}}",
+            "third", "{{second}}",
+            "fourth", "{{render(third, recursive=false)}}"
         );
 
-        String render = variableRenderer.render("{{ second }}", vars);
+        String render = variableRenderer.render("{{ third }}", vars);
+        assertThat(render, is("{{second}}"));
 
+        render = variableRenderer.render("{{ render(third) }}", vars);
+        assertThat(render, is("1"));
+
+        // even if recursive = false in the underneath variable, we don't disable recursiveness since it's too hacky and an edge case
+        render = variableRenderer.render("{{ render(fourth) }}", vars);
         assertThat(render, is("1"));
     }
 
diff --git a/core/src/test/java/io/kestra/core/runners/handlebars/helpers/JqHelperTest.java b/core/src/test/java/io/kestra/core/runners/handlebars/helpers/JqHelperTest.java
index 3ffe3f5463..3b97ef77ed 100644
--- a/core/src/test/java/io/kestra/core/runners/handlebars/helpers/JqHelperTest.java
+++ b/core/src/test/java/io/kestra/core/runners/handlebars/helpers/JqHelperTest.java
@@ -26,7 +26,7 @@ void simple() throws IllegalVariableEvaluationException {
             "third", "{{end}}"
         );
 
-        String render = variableRenderer.render("{{ jq first \".second.third\" }}", vars);
+        String render = variableRenderer.render("{{ jq (eval '{{first}}') \".second.third\" }}", vars);
 
         assertThat(render, is("[\"awesome\"]"));
     }
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/PebbleVariableRendererTest.java b/core/src/test/java/io/kestra/core/runners/pebble/PebbleVariableRendererTest.java
index d74c3cfde9..9cbe97e2e6 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/PebbleVariableRendererTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/PebbleVariableRendererTest.java
@@ -1,6 +1,8 @@
 package io.kestra.core.runners.pebble;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import io.kestra.core.exceptions.IllegalVariableEvaluationException;
 import io.kestra.core.runners.VariableRenderer;
 import io.kestra.core.utils.Rethrow;
@@ -154,13 +156,65 @@ void map() throws IllegalVariableEvaluationException {
     void recursive() throws IllegalVariableEvaluationException {
         ImmutableMap<String, Object> vars = ImmutableMap.of(
             "first", "1",
-            "second", "{{third}}",
-            "third", "{{first}}"
+            "second", "{{first}}",
+            "third", "{{second}}",
+            "fourth", "{{render(third, recursive=false)}}",
+            "map", ImmutableMap.of(
+                "third", "{{third}}"
+            ),
+            "list", ImmutableList.of(
+                "{{third}}"
+            ),
+            "set", ImmutableSet.of(
+                "{{third}}"
+            )
         );
 
-        String render = variableRenderer.render("{{ second }}", vars);
+        String render = variableRenderer.render("{{ third }}", vars);
+        assertThat(render, is("{{second}}"));
+        render = variableRenderer.render("{{ render(third, recursive=false) }}", vars);
+        assertThat(render, is("{{first}}"));
+        render = variableRenderer.render("{{ render(third) }}", vars);
+        assertThat(render, is("1"));
 
+        // even if recursive = false in the underneath variable, we don't disable recursiveness since it's too hacky and an edge case
+        render = variableRenderer.render("{{ render(fourth) }}", vars);
         assertThat(render, is("1"));
+
+        render = variableRenderer.render("{{ map }}", vars);
+        assertThat(render, is("{\"third\":\"{{third}}\"}"));
+        render = variableRenderer.render("{{ render(map, recursive=false) }}", vars);
+        assertThat(render, is("{\"third\":\"{{second}}\"}"));
+        render = variableRenderer.render("{{ render(map) }}", vars);
+        assertThat(render, is("{\"third\":\"1\"}"));
+
+        render = variableRenderer.render("{{ list }}", vars);
+        assertThat(render, is("[\"{{third}}\"]"));
+        render = variableRenderer.render("{{ render(list, recursive=false) }}", vars);
+        assertThat(render, is("[\"{{second}}\"]"));
+        render = variableRenderer.render("{{ render(list) }}", vars);
+        assertThat(render, is("[\"1\"]"));
+
+        render = variableRenderer.render("{{ set }}", vars);
+        assertThat(render, is("[\"{{third}}\"]"));
+        render = variableRenderer.render("{{ render(set, recursive=false) }}", vars);
+        assertThat(render, is("[\"{{second}}\"]"));
+        render = variableRenderer.render("{{ render(set) }}", vars);
+        assertThat(render, is("[\"1\"]"));
+    }
+
+    @Test
+    void recursiveRenderingAmountLimit() {
+        ImmutableMap<String, Object> vars = ImmutableMap.of(
+            "first", "{{second}}",
+            "second", "{{first}}"
+        );
+
+        IllegalVariableEvaluationException illegalVariableEvaluationException = assertThrows(
+            IllegalVariableEvaluationException.class,
+            () -> variableRenderer.render("{{ render(first) }}", vars)
+        );
+        assertThat(illegalVariableEvaluationException.getMessage(), containsString("Too many rendering attempts"));
     }
 
     @Test
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/RecursivePebbleVariableRendererTest.java b/core/src/test/java/io/kestra/core/runners/pebble/RecursivePebbleVariableRendererTest.java
new file mode 100644
index 0000000000..9452d7a7ff
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/runners/pebble/RecursivePebbleVariableRendererTest.java
@@ -0,0 +1,66 @@
+package io.kestra.core.runners.pebble;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.kestra.core.exceptions.IllegalVariableEvaluationException;
+import io.kestra.core.runners.VariableRenderer;
+import io.micronaut.context.annotation.Property;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@MicronautTest
+@Property(name = "kestra.variables.recursive-rendering", value = "true")
+class RecursivePebbleVariableRendererTest {
+    @Inject
+    VariableRenderer variableRenderer;
+
+    @Test
+    void recursive() throws IllegalVariableEvaluationException {
+        ImmutableMap<String, Object> vars = ImmutableMap.of(
+            "first", "1",
+            "second", "{{first}}",
+            "third", "{{second}}",
+            "map", ImmutableMap.of(
+                "third", "{{third}}"
+            ),
+            "list", ImmutableList.of(
+                "{{third}}"
+            ),
+            "set", ImmutableSet.of(
+                "{{third}}"
+            )
+        );
+
+        String render = variableRenderer.render("{{ third }}", vars);
+        assertThat(render, is("1"));
+
+        render = variableRenderer.render("{{ map }}", vars);
+        assertThat(render, is("{\"third\":\"1\"}"));
+
+        render = variableRenderer.render("{{ list }}", vars);
+        assertThat(render, is("[\"1\"]"));
+
+        render = variableRenderer.render("{{ set }}", vars);
+        assertThat(render, is("[\"1\"]"));
+    }
+
+    @Test
+    void renderFunctionNotInjectedIfRecursiveSettingsTrue() {
+        ImmutableMap<String, Object> vars = ImmutableMap.of(
+            "first", "1"
+        );
+
+        IllegalVariableEvaluationException illegalVariableEvaluationException = assertThrows(
+            IllegalVariableEvaluationException.class,
+            () -> variableRenderer.render("{{ render(first) }}", vars)
+        );
+        assertThat(illegalVariableEvaluationException.getMessage(), containsString("Function or Macro [render] does not exist"));
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/filters/JqFilterTest.java b/core/src/test/java/io/kestra/core/runners/pebble/filters/JqFilterTest.java
index 6d85c48e3f..1299285f63 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/filters/JqFilterTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/filters/JqFilterTest.java
@@ -43,7 +43,7 @@ void simple() throws IllegalVariableEvaluationException {
             "third", "{{end}}"
         );
 
-        String render = variableRenderer.render("{{  first | jq(\".second.third\") }}", vars);
+        String render = variableRenderer.render("{{  render(first) | jq(\".second.third\") }}", vars);
         assertThat(render, is("[\"awesome\"]"));
     }
 
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/functions/ReadFileFunctionTest.java b/core/src/test/java/io/kestra/core/runners/pebble/functions/ReadFileFunctionTest.java
index 7514069e1c..44c241eb65 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/functions/ReadFileFunctionTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/functions/ReadFileFunctionTest.java
@@ -34,7 +34,7 @@ void readNamespaceFile() throws IllegalVariableEvaluationException, IOException
         storageInterface.createDirectory(null, URI.create(storageInterface.namespaceFilePrefix(namespace)));
         storageInterface.put(null, URI.create(storageInterface.namespaceFilePrefix(namespace) + "/" + filePath), new ByteArrayInputStream("Hello from {{ flow.namespace }}".getBytes()));
 
-        String render = variableRenderer.render("{{ read('" + filePath + "') }}", Map.of("flow", Map.of("namespace", namespace)));
+        String render = variableRenderer.render("{{ render(read('" + filePath + "')) }}", Map.of("flow", Map.of("namespace", namespace)));
         assertThat(render, is("Hello from " + namespace));
     }
 
diff --git a/core/src/test/resources/flows/valids/variables.yaml b/core/src/test/resources/flows/valids/variables.yaml
index 853df75b63..ed7b25fefe 100644
--- a/core/src/test/resources/flows/valids/variables.yaml
+++ b/core/src/test/resources/flows/valids/variables.yaml
@@ -9,7 +9,7 @@ variables:
 tasks:
   - id: variable
     type: io.kestra.core.tasks.debugs.Return
-    format: "{{vars.third}}"
+    format: "{{render(vars.third)}}"
   - id: env
     type: io.kestra.core.tasks.debugs.Return
     format: "{{envs.test1}} {{envs.test2}}"