Repository: kestra-io/kestra (java)

Issue:
Improve support for ION/JSON processing in Pebble expressions

Required todos:
- [x] rename the `json` filter to `jsonEncode` for more clarity about what it does  — it takes a data structure or object and converts it into a JSON string e.g. `{{ [1, 2, 3] | jsonEncode  }}`
- [x] rename the `json()` function to `jsonDecode()` to make it clear that it takes a JSON string and converts it into a data structure or object so the user can access further attributes using a dot notation e.g. `'{{ jsonDecode(read(taskrun.items)) }}'`
- [x] add a new `ionDecode()` function that will take an ION string (from reading an ION file) and convert it into a data structure or object e.g. `'{{ ionDecode(read(taskrun.items)) }}'`; this function can only process a single row


Important notes:
- The renaming should ideally be implemented in a non-breaking way e.g. using `json()` will raise a warning in the UI but it will still work
- The goal of these tasks is to make it easy to process data split by `ForEachItem`. However, the `ionDecode()` function should raise an error when trying to parse a multi-line string (i.e. ION file with multiple rows); this is important for performance reasons

Pull request description:
Feat/json in pebble
Fixes #3715

Example flow:

```yaml
id: ion
namespace: company.myteam
tasks:
  - id: output
    type: io.kestra.plugin.core.output.OutputValues
    values:
      data.json: "{{ ionDecode('{date:2024-04-21T23:00:00.000Z, title:\"Main_Page\",views:109787}').title }}"
```

Patch:
diff --git a/core/src/main/java/io/kestra/core/docs/DocumentationGenerator.java b/core/src/main/java/io/kestra/core/docs/DocumentationGenerator.java
index 0c9f9a191..87dfc9111 100644
--- a/core/src/main/java/io/kestra/core/docs/DocumentationGenerator.java
+++ b/core/src/main/java/io/kestra/core/docs/DocumentationGenerator.java
@@ -27,7 +27,6 @@ import lombok.Getter;
 import org.apache.commons.io.IOUtils;
 
 import java.io.IOException;
-import java.io.StringWriter;
 import java.io.Writer;
 import java.util.*;
 import java.util.regex.Pattern;
@@ -53,7 +52,7 @@ public class DocumentationGenerator {
                 @Override
                 public Map<String, Filter> getFilters() {
                     Map<String, Filter> filters = new HashMap<>();
-                    filters.put("json", new JsonFilter());
+                    filters.put("json", new JsonEncodeFilter());
                     return filters;
                 }
             })
diff --git a/core/src/main/java/io/kestra/core/models/tasks/runners/ScriptService.java b/core/src/main/java/io/kestra/core/models/tasks/runners/ScriptService.java
index 1e8d7e0d0..974ecadbd 100644
--- a/core/src/main/java/io/kestra/core/models/tasks/runners/ScriptService.java
+++ b/core/src/main/java/io/kestra/core/models/tasks/runners/ScriptService.java
@@ -17,7 +17,6 @@ import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.*;
-import java.util.function.BiConsumer;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -47,7 +46,6 @@ public final class ScriptService {
     public static String replaceInternalStorage(
         RunContext runContext,
         @Nullable String command,
-        BiConsumer<String, String> internalStorageToLocalFileConsumer,
         boolean replaceWithRelativePath
     ) throws IOException {
         if (command == null) {
@@ -59,13 +57,11 @@ public final class ScriptService {
             .replaceAll(throwFunction(matchResult -> {
                 String localFile = saveOnLocalStorage(runContext, matchResult.group()).replace("\\", "/");
 
-                internalStorageToLocalFileConsumer.accept(matchResult.group(), localFile);
-
                 if (!replaceWithRelativePath) {
                     return localFile;
                 }
 
-                return localFile.startsWith("/") ? localFile.substring(1) : localFile;
+                return runContext.workingDir().path().relativize(Path.of(localFile)).toString();
             }));
     }
 
@@ -73,27 +69,25 @@ public final class ScriptService {
         RunContext runContext,
         Map<String, Object> additionalVars,
         String command,
-        BiConsumer<String, String> internalStorageToLocalFileConsumer,
         boolean replaceWithRelativePath
     ) throws IOException, IllegalVariableEvaluationException {
         if (command == null) {
             return null;
         }
 
-        return ScriptService.replaceInternalStorage(runContext, additionalVars, List.of(command), internalStorageToLocalFileConsumer, replaceWithRelativePath).getFirst();
+        return ScriptService.replaceInternalStorage(runContext, additionalVars, List.of(command), replaceWithRelativePath).getFirst();
     }
 
     public static List<String> replaceInternalStorage(
         RunContext runContext,
         Map<String, Object> additionalVars,
         List<String> commands,
-        BiConsumer<String, String> internalStorageToLocalFileConsumer,
         boolean replaceWithRelativePath
     ) throws IOException, IllegalVariableEvaluationException {
         return commands
             .stream()
             .map(throwFunction(c -> runContext.render(c, additionalVars)))
-            .map(throwFunction(c -> ScriptService.replaceInternalStorage(runContext, c, internalStorageToLocalFileConsumer, replaceWithRelativePath)))
+            .map(throwFunction(c -> ScriptService.replaceInternalStorage(runContext, c, replaceWithRelativePath)))
             .toList();
 
     }
@@ -102,8 +96,7 @@ public final class ScriptService {
         RunContext runContext,
         List<String> commands
     ) throws IOException, IllegalVariableEvaluationException {
-        return ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), commands, (ignored, file) -> {
-        }, false);
+        return ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), commands, false);
     }
 
     private static String saveOnLocalStorage(RunContext runContext, String uri) throws IOException {
diff --git a/core/src/main/java/io/kestra/core/models/tasks/runners/TaskCommands.java b/core/src/main/java/io/kestra/core/models/tasks/runners/TaskCommands.java
index 43b64a9ef..d75df0895 100644
--- a/core/src/main/java/io/kestra/core/models/tasks/runners/TaskCommands.java
+++ b/core/src/main/java/io/kestra/core/models/tasks/runners/TaskCommands.java
@@ -1,9 +1,14 @@
 package io.kestra.core.models.tasks.runners;
 
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Duration;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 
 /**
@@ -33,4 +38,23 @@ public interface TaskCommands {
     Duration getTimeout();
 
     TargetOS getTargetOS();
+
+    default List<Path> relativeWorkingDirectoryFilesPaths() throws IOException {
+        Path workingDirectory = this.getWorkingDirectory();
+        if (workingDirectory == null) {
+            return Collections.emptyList();
+        }
+
+        try (Stream<Path> walk = Files.walk(workingDirectory)) {
+            Stream<Path> filtered = walk.filter(Files::isRegularFile);
+            Path outputDirectory = this.getOutputDirectory();
+            if (outputDirectory != null) {
+                filtered = filtered.filter(Predicate.not(path -> path.startsWith(outputDirectory)));
+            }
+
+            return filtered
+                .map(workingDirectory::relativize)
+                .toList();
+        }
+    }
 }
diff --git a/core/src/main/java/io/kestra/core/models/tasks/runners/TaskRunner.java b/core/src/main/java/io/kestra/core/models/tasks/runners/TaskRunner.java
index c2e09efed..a680bbe5f 100644
--- a/core/src/main/java/io/kestra/core/models/tasks/runners/TaskRunner.java
+++ b/core/src/main/java/io/kestra/core/models/tasks/runners/TaskRunner.java
@@ -62,7 +62,7 @@ public abstract class TaskRunner implements Plugin, WorkerJobLifecycle {
      * For remote task runner (like Kubernetes or in a cloud provider), <code>filesToUpload</code> must be used to upload input and namespace files to the runner,
      * and <code>filesToDownload</code> must be used to download output files from the runner.
      */
-    public abstract RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToUpload, List<String> filesToDownload) throws Exception;
+    public abstract RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) throws Exception;
 
     public Map<String, Object> additionalVars(RunContext runContext, TaskCommands taskCommands) throws IllegalVariableEvaluationException {
         if (this.additionalVars == null) {
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 a077fa0cc..2f32d231b 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
@@ -73,6 +73,7 @@ public class Extension extends AbstractExtension {
         filters.put("jq", new JqFilter());
         filters.put("escapeChar", new EscapeCharFilter());
         filters.put("json", new JsonFilter());
+        filters.put("jsonEncode", new JsonEncodeFilter());
         filters.put("keys", new KeysFilter());
         filters.put("number", new NumberFilter());
         filters.put("urldecode", new UrlDecoderFilter());
@@ -106,6 +107,7 @@ public class Extension extends AbstractExtension {
 
         functions.put("now", new NowFunction());
         functions.put("json", new JsonFunction());
+        functions.put("jsonDecode", new JsonDecodeFunction());
         functions.put("currentEachOutput", new CurrentEachOutputFunction());
         functions.put("secret", secretFunction);
         functions.put("kv", kvFunction);
@@ -120,6 +122,7 @@ public class Extension extends AbstractExtension {
         functions.put("decrypt", new DecryptFunction());
         functions.put("yaml", new YamlFunction());
         functions.put("printContext", new PrintContextFunction());
+        functions.put("ionDecode", new IonDecodeFunction());
 
         return functions;
     }
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilter.java b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilter.java
new file mode 100644
index 000000000..ed3ee1e2f
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilter.java
@@ -0,0 +1,34 @@
+package io.kestra.core.runners.pebble.filters;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.kestra.core.serializers.JacksonMapper;
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.extension.Filter;
+import io.pebbletemplates.pebble.template.EvaluationContext;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+
+import java.util.List;
+import java.util.Map;
+
+public class JsonEncodeFilter implements Filter {
+    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
+
+    @Override
+    public List<String> getArgumentNames() {
+        return null;
+    }
+
+    @Override
+    public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException {
+        if (input == null) {
+            return "null";
+        }
+
+        try {
+            return MAPPER.writeValueAsString(input);
+        } catch (JsonProcessingException e) {
+            throw new PebbleException(e, "Unable to transform to json value '" + input +  "' with type '" + input.getClass().getName() + "'", lineNumber, self.getName());
+        }
+    }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
index 79be1fe4f..8da940ddf 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/filters/JsonFilter.java
@@ -1,34 +1,20 @@
 package io.kestra.core.runners.pebble.filters;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.kestra.core.serializers.JacksonMapper;
 import io.pebbletemplates.pebble.error.PebbleException;
-import io.pebbletemplates.pebble.extension.Filter;
 import io.pebbletemplates.pebble.template.EvaluationContext;
 import io.pebbletemplates.pebble.template.PebbleTemplate;
+import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
 import java.util.Map;
 
-public class JsonFilter implements Filter {
-    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
-
-    @Override
-    public List<String> getArgumentNames() {
-        return null;
-    }
+@Slf4j
+@Deprecated
+public class JsonFilter extends JsonEncodeFilter {
 
     @Override
     public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException {
-        if (input == null) {
-            return "null";
-        }
+        log.warn("The 'json' filter is deprecated, please use 'jsonEncode' instead");
 
-        try {
-            return MAPPER.writeValueAsString(input);
-        } catch (JsonProcessingException e) {
-            throw new PebbleException(e, "Unable to transform to json value '" + input +  "' with type '" + input.getClass().getName() + "'", lineNumber, self.getName());
-        }
+        return super.apply(input, args, self, context, lineNumber);
     }
 }
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/functions/IonDecodeFunction.java b/core/src/main/java/io/kestra/core/runners/pebble/functions/IonDecodeFunction.java
new file mode 100644
index 000000000..63ded1bfd
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/runners/pebble/functions/IonDecodeFunction.java
@@ -0,0 +1,43 @@
+package io.kestra.core.runners.pebble.functions;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.kestra.core.serializers.JacksonMapper;
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.extension.Function;
+import io.pebbletemplates.pebble.template.EvaluationContext;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+
+import java.util.List;
+import java.util.Map;
+
+public class IonDecodeFunction implements Function {
+        private static final ObjectMapper MAPPER = JacksonMapper.ofIon();
+
+        public List<String> getArgumentNames() {
+            return List.of("ion");
+        }
+
+        @Override
+        public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+            if (!args.containsKey("ion")) {
+                throw new PebbleException(null, "The 'ion' function expects an argument 'ion'.", lineNumber, self.getName());
+            }
+
+            if (args.get("ion") == null) {
+                return null;
+            }
+
+            if (!(args.get("ion") instanceof String)) {
+                throw new PebbleException(null, "The 'ion' function expects an argument 'ion' with type string.", lineNumber, self.getName());
+            }
+
+            String ion = (String) args.get("ion");;
+
+            try {
+                return MAPPER.readValue(ion, JacksonMapper.OBJECT_TYPE_REFERENCE);
+            } catch (JsonProcessingException e) {
+                throw new PebbleException(null, "Invalid ion: " + e.getMessage(), lineNumber, self.getName());
+            }
+        }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunction.java b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunction.java
new file mode 100644
index 000000000..011f4466e
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunction.java
@@ -0,0 +1,43 @@
+package io.kestra.core.runners.pebble.functions;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.kestra.core.serializers.JacksonMapper;
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.extension.Function;
+import io.pebbletemplates.pebble.template.EvaluationContext;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+
+import java.util.List;
+import java.util.Map;
+
+public class JsonDecodeFunction implements Function {
+    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
+
+    public List<String> getArgumentNames() {
+        return List.of("json");
+    }
+
+    @Override
+    public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+        if (!args.containsKey("json")) {
+            throw new PebbleException(null, "The 'json' function expects an argument 'json'.", lineNumber, self.getName());
+        }
+
+        if (args.get("json") == null) {
+            return null;
+        }
+
+        if (!(args.get("json") instanceof String)) {
+            throw new PebbleException(null, "The 'json' function expects an argument 'json' with type string.", lineNumber, self.getName());
+        }
+
+        String json = (String) args.get("json");;
+
+        try {
+            return MAPPER.readValue(json, JacksonMapper.OBJECT_TYPE_REFERENCE);
+        } catch (JsonProcessingException e) {
+            throw new PebbleException(null, "Invalid json: " + e.getMessage(), lineNumber, self.getName());
+        }
+    }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
index 527030006..6208ef74a 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/functions/JsonFunction.java
@@ -1,39 +1,19 @@
 package io.kestra.core.runners.pebble.functions;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.kestra.core.serializers.JacksonMapper;
-import io.pebbletemplates.pebble.error.PebbleException;
-import io.pebbletemplates.pebble.extension.Function;
 import io.pebbletemplates.pebble.template.EvaluationContext;
 import io.pebbletemplates.pebble.template.PebbleTemplate;
+import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
 import java.util.Map;
 
-public class JsonFunction implements Function {
-    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
-
-    public List<String> getArgumentNames() {
-        return List.of("json");
-    }
+@Slf4j
+@Deprecated
+public class JsonFunction extends JsonDecodeFunction {
 
     @Override
     public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
-        if (!args.containsKey("json")) {
-            throw new PebbleException(null, "The 'json' function expects an argument 'json'.", lineNumber, self.getName());
-        }
-
-        if (!(args.get("json") instanceof String)) {
-            throw new PebbleException(null, "The 'json' function expects an argument 'json' with type string.", lineNumber, self.getName());
-        }
-
-        String json = (String) args.get("json");;
+        log.warn("The 'json' function is deprecated, please use 'jsonDecode' instead");
 
-        try {
-            return MAPPER.readValue(json, JacksonMapper.OBJECT_TYPE_REFERENCE);
-        } catch (JsonProcessingException e) {
-            throw new PebbleException(null, "Invalid json: " + e.getMessage(), lineNumber, self.getName());
-        }
+        return super.execute(args, self, context, lineNumber);
     }
 }
diff --git a/core/src/main/java/io/kestra/plugin/core/runner/Process.java b/core/src/main/java/io/kestra/plugin/core/runner/Process.java
index 5d0fb8e95..360b12104 100644
--- a/core/src/main/java/io/kestra/plugin/core/runner/Process.java
+++ b/core/src/main/java/io/kestra/plugin/core/runner/Process.java
@@ -87,7 +87,7 @@ public class Process extends TaskRunner {
     public static final Process INSTANCE = Process.builder().type(Process.class.getName()).build();
 
     @Override
-    public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToUpload, List<String> filesToDownload) throws Exception {
+    public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) throws Exception {
         Logger logger = runContext.logger();
         AbstractLogConsumer defaultLogConsumer = taskCommands.getLogConsumer();
 
diff --git a/script/src/main/java/io/kestra/plugin/scripts/exec/scripts/runners/CommandsWrapper.java b/script/src/main/java/io/kestra/plugin/scripts/exec/scripts/runners/CommandsWrapper.java
index a15ea4471..12045d8c3 100644
--- a/script/src/main/java/io/kestra/plugin/scripts/exec/scripts/runners/CommandsWrapper.java
+++ b/script/src/main/java/io/kestra/plugin/scripts/exec/scripts/runners/CommandsWrapper.java
@@ -132,7 +132,6 @@ public class CommandsWrapper implements TaskCommands {
     }
 
     public ScriptOutput run() throws Exception {
-        List<String> filesToUpload = new ArrayList<>();
         if (this.namespaceFiles != null && Boolean.TRUE.equals(this.namespaceFiles.getEnabled())) {
 
             List<NamespaceFile> matchedNamespaceFiles = runContext.storage()
@@ -143,22 +142,19 @@ public class CommandsWrapper implements TaskCommands {
                     InputStream content = runContext.storage().getFile(namespaceFile.uri());
                     runContext.workingDir().createFile(namespaceFile.path().toString(), content);
                 }));
-
-            matchedNamespaceFiles.forEach(file -> filesToUpload.add(file.path().toString()));
         }
 
         TaskRunner realTaskRunner = this.getTaskRunner();
         if (this.inputFiles != null) {
-            Map<String, String> finalInputFiles = FilesService.inputFiles(runContext, realTaskRunner.additionalVars(runContext, this), this.inputFiles);
-            filesToUpload.addAll(finalInputFiles.keySet());
+            FilesService.inputFiles(runContext, realTaskRunner.additionalVars(runContext, this), this.inputFiles);
         }
 
         RunContextInitializer initializer = ((DefaultRunContext) runContext).getApplicationContext().getBean(RunContextInitializer.class);
 
         RunContext taskRunnerRunContext = initializer.forPlugin(((DefaultRunContext) runContext).clone(), realTaskRunner);
-        this.commands = this.render(runContext, commands, filesToUpload);
+        this.commands = this.render(runContext, commands);
 
-        RunnerResult runnerResult = realTaskRunner.run(taskRunnerRunContext, this, filesToUpload, this.outputFiles);
+        RunnerResult runnerResult = realTaskRunner.run(taskRunnerRunContext, this, this.outputFiles);
 
         Map<String, URI> outputFiles = new HashMap<>();
         if (this.outputDirectoryEnabled()) {
@@ -221,18 +217,16 @@ public class CommandsWrapper implements TaskCommands {
             this.runContext,
             taskRunner.additionalVars(runContext, this),
             command,
-            (ignored, localFilePath) -> internalStorageLocalFiles.add(localFilePath),
             taskRunner instanceof RemoteRunnerInterface
         );
     }
 
-    public List<String> render(RunContext runContext, List<String> commands, List<String> internalStorageLocalFiles) throws IllegalVariableEvaluationException, IOException {
+    public List<String> render(RunContext runContext, List<String> commands) throws IllegalVariableEvaluationException, IOException {
         TaskRunner taskRunner = this.getTaskRunner();
         return ScriptService.replaceInternalStorage(
             this.runContext,
             taskRunner.additionalVars(runContext, this),
             commands,
-            (ignored, localFilePath) -> internalStorageLocalFiles.add(localFilePath),
             taskRunner instanceof RemoteRunnerInterface
         );
     }
diff --git a/script/src/main/java/io/kestra/plugin/scripts/runner/docker/Docker.java b/script/src/main/java/io/kestra/plugin/scripts/runner/docker/Docker.java
index 38f7135cc..960819da5 100644
--- a/script/src/main/java/io/kestra/plugin/scripts/runner/docker/Docker.java
+++ b/script/src/main/java/io/kestra/plugin/scripts/runner/docker/Docker.java
@@ -230,7 +230,7 @@ public class Docker extends TaskRunner {
 
 
     @Override
-    public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToUpload, List<String> filesToDownload) throws Exception {
+    public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) throws Exception {
         if (taskCommands.getContainerImage() == null && this.image == null) {
             throw new IllegalArgumentException("This task runner needs the `containerImage` property to be set");
         }
diff --git a/ui/src/components/inputs/EditorView.vue b/ui/src/components/inputs/EditorView.vue
index b30007902..27072cdf8 100644
--- a/ui/src/components/inputs/EditorView.vue
+++ b/ui/src/components/inputs/EditorView.vue
@@ -1034,8 +1034,8 @@
 
                 <iframe
                     width="60%"
-                    height="400px"
-                    src="https://www.youtube.com/embed/o-d-GaXUiKQ?si=3tCTtUMx3CLH11_D" 
+                    height="400px"                    
+                    src="https://www.youtube.com/embed/o-d-GaXUiKQ?si=TTjV8jgRg6-lj_cC" 
                     frameborder="0"
                     allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                     allowfullscreen
diff --git a/ui/src/components/namespace/Namespace.vue b/ui/src/components/namespace/Namespace.vue
index 151a0f0b6..0ecab0585 100644
--- a/ui/src/components/namespace/Namespace.vue
+++ b/ui/src/components/namespace/Namespace.vue
@@ -60,15 +60,15 @@
             ...mapState("auth", ["user"]),
             ...mapState("namespace", ["dependencies"]),
             canCreateSecret() {
-                return this.$route.params.id && this.user &&
-                    this.user.isAllowed(permission.SECRET, action.CREATE, this.$route.params.id);
+                return this.$route.params.namespace && this.user &&
+                    this.user.isAllowed(permission.SECRET, action.CREATE, this.$route.params.namespace);
             },
             canCreateKv() {
-                return this.$route.params.id;
+                return this.$route.params.namespace;
             },
             routeInfo() {
                 return {
-                    title: this.$route.params.id || this.$t("namespaces"),
+                    title: this.$route.params.namespace || this.$t("namespaces"),
                     breadcrumb: [
                         {
                             label: this.$t("namespaces"),
@@ -216,7 +216,7 @@
         },
         methods: {
             loadItem() {
-                if (this.$route.params.id) {
+                if (this.$route.params.namespace) {
                     this.$store
                         .dispatch(
                             "namespace/load",
diff --git a/ui/src/components/namespace/NamespaceDependenciesWrapper.vue b/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
index d3192ebe4..4a17a2b5d 100644
--- a/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
+++ b/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
@@ -1,7 +1,10 @@
 <template>
-    <NamespaceDependencies :id="$route.query?.id ?? $route.params.id" />
+    <NamespaceDependencies :id="route.params.namespace" />
 </template>
 
 <script setup>
+    import {useRoute} from "vue-router";
+    const route = useRoute();
+
     import NamespaceDependencies from "./NamespaceDependencies.vue";
 </script>
\ No newline at end of file
diff --git a/ui/src/components/namespace/NamespaceFlows.vue b/ui/src/components/namespace/NamespaceFlows.vue
index 338d95c70..65ced1dc6 100644
--- a/ui/src/components/namespace/NamespaceFlows.vue
+++ b/ui/src/components/namespace/NamespaceFlows.vue
@@ -129,7 +129,7 @@
         },
         methods: {
             loadData(callback) {
-                this.$store.dispatch("flow/findFlows", {namespace: this.$route.params.id}).then((flows) => {
+                this.$store.dispatch("flow/findFlows", {namespace: this.$route.params.namespace}).then((flows) => {
                     this.dailyGroupByFlowReady = false;
                     this.lastExecutionByFlowReady = false;
 
diff --git a/ui/src/components/namespace/NamespaceKV.vue b/ui/src/components/namespace/NamespaceKV.vue
index aa1cf86af..b7d92f8ad 100644
--- a/ui/src/components/namespace/NamespaceKV.vue
+++ b/ui/src/components/namespace/NamespaceKV.vue
@@ -138,10 +138,10 @@
                 return this.kv.key ? this.$t("kv.update", {key: this.kv.key}) : this.$t("kv.add");
             },
             canUpdateKv() {
-                return this.$route.params.id                
+                return this.$route.params.namespace                
             },
             canDeleteKv() {
-                return this.$route.params.id
+                return this.$route.params.namespace
             },
             addKvDrawerVisible: {
                 get() {
@@ -208,7 +208,7 @@
         },
         methods: {
             durationValidator(rule, value, callback) {
-                if (value !== undefined && !value.match(/^P(?=.)(?:\d*D)?(?:T(?=.)(?:\d*H)?(?:\d*M)?(?:\d*S)?)?$/)) {
+                if (value !== undefined && !value.match(/^P(?=[^T]|T.)(?:\d*D)?(?:T(?=.)(?:\d*H)?(?:\d*M)?(?:\d*S)?)?$/)) {
                     callback(new Error(this.$t("datepicker.error")));
                 } else {
                     callback();
@@ -216,7 +216,7 @@
             },
             loadKvs() {
                 this.$store
-                    .dispatch("namespace/kvsList", {id: this.$route.params.id})
+                    .dispatch("namespace/kvsList", {id: this.$route.params.namespace})
             },
             kvKeyDuplicate(rule, value, callback) {
                 if (this.kv.update === undefined && this.kvs && this.kvs.find(r => r.key === value)) {
@@ -227,7 +227,7 @@
             },
             async updateKvModal(key) {
                 this.kv.key = key;
-                const {type, value} = await this.$store.dispatch("namespace/kv", {namespace: this.$route.params.id, key});
+                const {type, value} = await this.$store.dispatch("namespace/kv", {namespace: this.$route.params.namespace, key});
                 this.kv.type = type;
                 if (type === "JSON") {
                     this.kv.value = JSON.stringify(value);
@@ -242,7 +242,7 @@
             removeKv(key) {
                 this.$toast().confirm(this.$t("delete confirm", {name: key}), () => {
                     return this.$store
-                        .dispatch("namespace/deleteKv", {namespace: this.$route.params.id, key: key})
+                        .dispatch("namespace/deleteKv", {namespace: this.$route.params.namespace, key: key})
                         .then(() => {
                             this.$toast().deleted(key);
                         });
@@ -269,7 +269,7 @@
                     return this.$store
                         .dispatch(
                             "namespace/createKv",
-                            {namespace: this.$route.params.id, ...this.kv, value}
+                            {namespace: this.$route.params.namespace, ...this.kv, value}
                         )
                         .then(() => {
                             this.$toast().saved(this.kv.key);
diff --git a/ui/src/components/namespace/Overview.vue b/ui/src/components/namespace/Overview.vue
index 360c3adb4..3d9b66c31 100644
--- a/ui/src/components/namespace/Overview.vue
+++ b/ui/src/components/namespace/Overview.vue
@@ -1,9 +1,9 @@
 <template>
     <div>
         <home
-            :namespace="$route.params.id || $route.query.id"
+            :namespace="$route.params.namespace || $route.query.id"
             :restore-url="false"
-            :description="$route.params.id ? namespace?.description : undefined"
+            :description="$route.params.namespace ? namespace?.description : undefined"
             embed
         />
     </div>


Test patch:
diff --git a/core/src/test/java/io/kestra/core/models/tasks/runners/ScriptServiceTest.java b/core/src/test/java/io/kestra/core/models/tasks/runners/ScriptServiceTest.java
index b89954fec..cc76fd7de 100644
--- a/core/src/test/java/io/kestra/core/models/tasks/runners/ScriptServiceTest.java
+++ b/core/src/test/java/io/kestra/core/models/tasks/runners/ScriptServiceTest.java
@@ -6,37 +6,36 @@ import io.kestra.core.models.executions.TaskRun;
 import io.kestra.core.models.flows.Flow;
 import io.kestra.core.models.flows.State;
 import io.kestra.core.models.tasks.Task;
-import io.kestra.core.models.tasks.runners.ScriptService;
 import io.kestra.core.runners.RunContext;
 import io.kestra.core.runners.RunContextFactory;
 import io.kestra.core.junit.annotations.KestraTest;
 import jakarta.inject.Inject;
 import org.junit.jupiter.api.Test;
 
+import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
 
 @KestraTest
 class ScriptServiceTest {
+    public static final Pattern COMMAND_PATTERN_CAPTURE_LOCAL_PATH = Pattern.compile("my command with an internal storage file: (.*)");
     @Inject private RunContextFactory runContextFactory;
 
     @Test
     void replaceInternalStorage() throws IOException {
         var runContext = runContextFactory.of();
-        var command  = ScriptService.replaceInternalStorage(runContext, null, (ignored, file) -> {}, false);
+        var command  = ScriptService.replaceInternalStorage(runContext, null, false);
         assertThat(command, is(""));
 
-        command = ScriptService.replaceInternalStorage(runContext, "my command", (ignored, file) -> {}, false);
+        command = ScriptService.replaceInternalStorage(runContext, "my command", false);
         assertThat(command, is("my command"));
 
         Path path = Path.of("/tmp/unittest/file.txt");
@@ -45,16 +44,24 @@ class ScriptServiceTest {
         }
 
         String internalStorageUri = "kestra://some/file.txt";
-        AtomicReference<String> localFile = new AtomicReference<>();
+        File localFile = null;
         try {
-            command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, (ignored, file) -> localFile.set(file), false);
-            assertThat(command, is("my command with an internal storage file: " + localFile.get()));
-            assertThat(Path.of(localFile.get()).toFile().exists(), is(true));
-
-            command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, (ignored, file) -> localFile.set(file), true);
-            assertThat(command, is("my command with an internal storage file: " + localFile.get().substring(1)));
+            command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, false);
+
+            Matcher matcher = COMMAND_PATTERN_CAPTURE_LOCAL_PATH.matcher(command);
+            assertThat(matcher.matches(), is(true));
+            Path absoluteLocalFilePath = Path.of(matcher.group(1));
+            localFile = absoluteLocalFilePath.toFile();
+            assertThat(localFile.exists(), is(true));
+
+            command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, true);
+            matcher = COMMAND_PATTERN_CAPTURE_LOCAL_PATH.matcher(command);
+            assertThat(matcher.matches(), is(true));
+            String relativePath = matcher.group(1);
+            assertThat(relativePath, not(startsWith("/")));
+            assertThat(runContext.workingDir().resolve(Path.of(relativePath)).toFile().exists(), is(true));
         } finally {
-            Path.of(localFile.get()).toFile().delete();
+            localFile.delete();
             path.toFile().delete();
         }
     }
@@ -68,8 +75,9 @@ class ScriptServiceTest {
             Files.createFile(path);
         }
 
-        Map<String, String> localFileByInternalStorage = new HashMap<>();
+        List<File> filesToDelete = new ArrayList<>();
         String internalStorageUri = "kestra://some/file.txt";
+
         try {
             String wdir = "/my/wd";
             var commands = ScriptService.replaceInternalStorage(
@@ -79,20 +87,29 @@ class ScriptServiceTest {
                     "my command with an internal storage file: " + internalStorageUri,
                     "my command with some additional var usage: {{ workingDir }}"
                 ),
-                localFileByInternalStorage::put,
                 false
             );
             assertThat(commands, not(empty()));
-            assertThat(commands.getFirst(), is("my command with an internal storage file: " + localFileByInternalStorage.get(internalStorageUri)));
-            assertThat(Path.of(localFileByInternalStorage.get(internalStorageUri)).toFile().exists(), is(true));
+
+            assertThat(commands.getFirst(), not(is("my command with an internal storage file: " + internalStorageUri)));
+            Matcher matcher = COMMAND_PATTERN_CAPTURE_LOCAL_PATH.matcher(commands.getFirst());
+            assertThat(matcher.matches(), is(true));
+            File file = Path.of(matcher.group(1)).toFile();
+            assertThat(file.exists(), is(true));
+            filesToDelete.add(file);
+
             assertThat(commands.get(1), is("my command with some additional var usage: " + wdir));
 
-            commands = ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), List.of("my command with an internal storage file: " + internalStorageUri), localFileByInternalStorage::put, true);
-            assertThat(commands.getFirst(), is("my command with an internal storage file: " + localFileByInternalStorage.get(internalStorageUri).substring(1)));
+            commands = ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), List.of("my command with an internal storage file: " + internalStorageUri), true);
+            matcher = COMMAND_PATTERN_CAPTURE_LOCAL_PATH.matcher(commands.getFirst());
+            assertThat(matcher.matches(), is(true));
+            file = runContext.workingDir().resolve(Path.of(matcher.group(1))).toFile();
+            assertThat(file.exists(), is(true));
+            filesToDelete.add(file);
         } catch (IllegalVariableEvaluationException e) {
             throw new RuntimeException(e);
         } finally {
-            localFileByInternalStorage.forEach((k, v) -> Path.of(v).toFile().delete());
+            filesToDelete.forEach(File::delete);
             path.toFile().delete();
         }
     }
diff --git a/core/src/test/java/io/kestra/core/models/tasks/runners/TaskRunnerTest.java b/core/src/test/java/io/kestra/core/models/tasks/runners/TaskRunnerTest.java
index 05d8f0501..f4ccf169f 100644
--- a/core/src/test/java/io/kestra/core/models/tasks/runners/TaskRunnerTest.java
+++ b/core/src/test/java/io/kestra/core/models/tasks/runners/TaskRunnerTest.java
@@ -97,7 +97,7 @@ public class TaskRunnerTest {
         }
 
         @Override
-        public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToUpload, List<String> filesToDownload) {
+        public RunnerResult run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) {
             return null;
         }
 
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java b/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilterTest.java
similarity index 68%
rename from core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java
rename to core/src/test/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilterTest.java
index ad4c7a834..7487a440e 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonFilterTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/filters/JsonEncodeFilterTest.java
@@ -16,14 +16,15 @@ import jakarta.inject.Inject;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
 import static org.hamcrest.Matchers.endsWith;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @KestraTest
-class JsonFilterTest {
+class JsonEncodeFilterTest {
     @Inject
     VariableRenderer variableRenderer;
 
     @Test
-    void map() throws IllegalVariableEvaluationException {
+    void jsonEncodeFilter() throws IllegalVariableEvaluationException {
         ZonedDateTime date = ZonedDateTime.parse("2013-09-08T16:19:00+02").withZoneSameLocal(ZoneId.systemDefault());
 
         ImmutableMap<String, Object> vars = ImmutableMap.of(
@@ -46,25 +47,25 @@ class JsonFilterTest {
             ))
         );
 
-        String render = variableRenderer.render("{{ vars.second.string | json }}", vars);
+        String render = variableRenderer.render("{{ vars.second.string | jsonEncode }}", vars);
         assertThat(render, is("\"string\""));
 
-        render = variableRenderer.render("{{ vars.second.int | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.int | jsonEncode }}", vars);
         assertThat(render, is("1"));
 
-        render = variableRenderer.render("{{ vars.second.float | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.float | jsonEncode }}", vars);
         assertThat(render, is("1.123"));
 
-        render = variableRenderer.render("{{ vars.second.list | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.list | jsonEncode }}", vars);
         assertThat(render, is("[\"string\",1,1.123]"));
 
-        render = variableRenderer.render("{{ vars.second.bool | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.bool | jsonEncode }}", vars);
         assertThat(render, is("true"));
 
-        render = variableRenderer.render("{{ vars.second.date | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.date | jsonEncode }}", vars);
         assertThat(render, is("\"" + date.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + "\""));
 
-        render = variableRenderer.render("{{ vars.second.map | json }}", vars);
+        render = variableRenderer.render("{{ vars.second.map | jsonEncode }}", vars);
         assertThat(render, containsString("\"int\":1"));
         assertThat(render, containsString("\"int\":1"));
         assertThat(render, containsString("\"float\":1.123"));
@@ -72,7 +73,29 @@ class JsonFilterTest {
         assertThat(render, startsWith("{"));
         assertThat(render, endsWith("}"));
 
-        render = variableRenderer.render("{{ {\"empty_object\":{}} | json }}", Map.of());
+        render = variableRenderer.render("{{ {\"empty_object\":{}} | jsonEncode }}", Map.of());
         assertThat(render, is("{\"empty_object\":{}}"));
+
+        render = variableRenderer.render("{{ null | jsonEncode }}", Map.of());
+        assertThat(render, is("null"));
+    }
+
+    @Test
+    void exception() {
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ | jsonEncode }}", Map.of()));
+
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ {not: json} | jsonEncode }}", Map.of()));
+    }
+
+    @Test
+    void jsonFilter() throws IllegalVariableEvaluationException {
+        ImmutableMap<String, Object> vars = ImmutableMap.of(
+            "vars", ImmutableMap.of("second", Map.of(
+                "string", "string"
+            ))
+        );
+
+        String render = variableRenderer.render("{{ vars.second.string | jsonEncode }}", vars);
+        assertThat(render, is("\"string\""));
     }
 }
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/functions/IonDecodeFunctionTest.java b/core/src/test/java/io/kestra/core/runners/pebble/functions/IonDecodeFunctionTest.java
new file mode 100644
index 000000000..b173921f4
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/runners/pebble/functions/IonDecodeFunctionTest.java
@@ -0,0 +1,35 @@
+package io.kestra.core.runners.pebble.functions;
+
+import io.kestra.core.exceptions.IllegalVariableEvaluationException;
+import io.kestra.core.junit.annotations.KestraTest;
+import io.kestra.core.runners.VariableRenderer;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@KestraTest
+class IonDecodeFunctionTest {
+    @Inject
+    VariableRenderer variableRenderer;
+
+    @Test
+    void ionDecodeFunction() throws IllegalVariableEvaluationException {
+        String render = variableRenderer.render("{{ ionDecode('{date:2024-04-21T23:00:00.000Z, title:\"Main_Page\",views:109787}').title }}", Map.of());
+        assertThat(render, is("Main_Page"));
+
+        render = variableRenderer.render("{{ ionDecode(null) }}", Map.of());
+        assertThat(render, emptyString());
+    }
+
+    @Test
+    void exception() {
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ ionDecode() }}", Map.of()));
+
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ ionDecode('{not: ion') }}", Map.of()));
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunctionTest.java b/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunctionTest.java
new file mode 100644
index 000000000..8c7f88ce1
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonDecodeFunctionTest.java
@@ -0,0 +1,50 @@
+package io.kestra.core.runners.pebble.functions;
+
+import io.kestra.core.exceptions.IllegalVariableEvaluationException;
+import io.kestra.core.runners.VariableRenderer;
+import io.kestra.core.junit.annotations.KestraTest;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import jakarta.inject.Inject;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@KestraTest
+class JsonDecodeFunctionTest {
+    @Inject
+    VariableRenderer variableRenderer;
+
+    @Test
+    void jsonDecodeFunction() throws IllegalVariableEvaluationException {
+        String render = variableRenderer.render("{{ jsonDecode('{\"test1\": 1, \"test2\": 2, \"test3\": 3}').test1 }}", Map.of());
+        assertThat(render, is("1"));
+
+        render = variableRenderer.render("{{ jsonDecode('{\"test1\": [{\"test1\": 666}, 2, 3], \"test2\": 2, \"test3\": 3}').test1[0].test1 }}", Map.of());
+        assertThat(render, is("666"));
+
+        render = variableRenderer.render("{{ jsonDecode('[1, 2, 3]')[0] }}", Map.of());
+        assertThat(render, is("1"));
+
+        render = variableRenderer.render("{{ jsonDecode('{\"empty_object\":{}}') }}", Map.of());
+        assertThat(render, is("{\"empty_object\":{}}"));
+
+        render = variableRenderer.render("{{ jsonDecode(null) }}", Map.of());
+        assertThat(render, emptyString());
+    }
+
+    @Test
+    void exception() {
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ jsonDecode() }}", Map.of()));
+
+        assertThrows(IllegalVariableEvaluationException.class, () -> variableRenderer.render("{{ jsonDecode('{not: json}') }}", Map.of()));
+    }
+
+    @Test
+    void jsonFunction() throws IllegalVariableEvaluationException {
+        String render = variableRenderer.render("{{ json('{\"test1\": 1, \"test2\": 2, \"test3\": 3}').test1 }}", Map.of());
+        assertThat(render, is("1"));
+    }
+}
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java b/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java
deleted file mode 100644
index 19328eee3..000000000
--- a/core/src/test/java/io/kestra/core/runners/pebble/functions/JsonFunctionTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.kestra.core.runners.pebble.functions;
-
-import io.kestra.core.exceptions.IllegalVariableEvaluationException;
-import io.kestra.core.runners.VariableRenderer;
-import io.kestra.core.junit.annotations.KestraTest;
-import org.junit.jupiter.api.Test;
-
-import java.util.Map;
-import jakarta.inject.Inject;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-@KestraTest
-class JsonFunctionTest {
-    @Inject
-    VariableRenderer variableRenderer;
-
-    @Test
-    void fromString() throws IllegalVariableEvaluationException {
-        String render = variableRenderer.render("{{ json('{\"test1\": 1, \"test2\": 2, \"test3\": 3}').test1 }}", Map.of());
-        assertThat(render, is("1"));
-
-        render = variableRenderer.render("{{ json('{\"test1\": [{\"test1\": 666}, 2, 3], \"test2\": 2, \"test3\": 3}').test1[0].test1 }}", Map.of());
-        assertThat(render, is("666"));
-
-        render = variableRenderer.render("{{ json('[1, 2, 3]')[0] }}", Map.of());
-        assertThat(render, is("1"));
-
-        render = variableRenderer.render("{{ json('{\"empty_object\":{}}') }}", Map.of());
-        assertThat(render, is("{\"empty_object\":{}}"));
-    }
-}
diff --git a/script/src/test/java/io/kestra/plugin/scripts/runners/LogConsumerTest.java b/script/src/test/java/io/kestra/plugin/scripts/runners/LogConsumerTest.java
index ef8a00a46..9d1c0af55 100644
--- a/script/src/test/java/io/kestra/plugin/scripts/runners/LogConsumerTest.java
+++ b/script/src/test/java/io/kestra/plugin/scripts/runners/LogConsumerTest.java
@@ -53,7 +53,6 @@ public class LogConsumerTest {
         RunnerResult run = Docker.from(DockerOptions.builder().image("alpine").build()).run(
             runContext,
             taskCommands,
-            Collections.emptyList(),
             Collections.emptyList()
         );
         Await.until(() -> run.getLogConsumer().getStdOutCount() == 2, null, Duration.ofSeconds(5));
@@ -89,7 +88,6 @@ public class LogConsumerTest {
         RunnerResult run = Docker.from(DockerOptions.builder().image("alpine").build()).run(
             runContext,
             taskCommands,
-            Collections.emptyList(),
             Collections.emptyList()
         );
 
diff --git a/tests/src/main/java/io/kestra/core/models/tasks/runners/AbstractTaskRunnerTest.java b/tests/src/main/java/io/kestra/core/models/tasks/runners/AbstractTaskRunnerTest.java
index ed05b74ee..4a4733c4b 100644
--- a/tests/src/main/java/io/kestra/core/models/tasks/runners/AbstractTaskRunnerTest.java
+++ b/tests/src/main/java/io/kestra/core/models/tasks/runners/AbstractTaskRunnerTest.java
@@ -18,14 +18,14 @@ import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
+import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.*;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.*;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @KestraTest
@@ -40,7 +40,7 @@ public abstract class AbstractTaskRunnerTest {
         Mockito.when(commands.getCommands()).thenReturn(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World'")));
 
         var taskRunner = taskRunner();
-        var result = taskRunner.run(runContext, commands, Collections.emptyList(), Collections.emptyList());
+        var result = taskRunner.run(runContext, commands, Collections.emptyList());
         assertThat(result, notNullValue());
         assertThat(result.getExitCode(), is(0));
     }
@@ -57,19 +57,19 @@ public abstract class AbstractTaskRunnerTest {
         assertThat(taskRunner.additionalVars(runContext, commands).containsKey(ScriptService.VAR_OUTPUT_DIR), is(false));
         assertThat(taskRunner.env(runContext, commands).containsKey(ScriptService.ENV_OUTPUT_DIR), is(false));
 
-        var result = taskRunner.run(runContext, commands, Collections.emptyList(), Collections.emptyList());
+        var result = taskRunner.run(runContext, commands, Collections.emptyList());
         assertThat(result, notNullValue());
         assertThat(result.getExitCode(), is(0));
     }
 
     @Test
-    protected void fail() {
+    protected void fail() throws IOException {
         var runContext = runContext(this.runContextFactory);
         var commands = initScriptCommands(runContext);
         Mockito.when(commands.getCommands()).thenReturn(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("return 1")));
 
         var taskRunner = taskRunner();
-        assertThrows(TaskException.class, () -> taskRunner.run(runContext, commands, Collections.emptyList(), Collections.emptyList()));
+        assertThrows(TaskException.class, () -> taskRunner.run(runContext, commands, Collections.emptyList()));
     }
 
     @Test
@@ -98,9 +98,6 @@ public abstract class AbstractTaskRunnerTest {
             }
         });
 
-        List<String> filesToUpload = new ArrayList<>();
-        filesToUpload.add("hello.txt");
-
         String wdir = this.needsToSpecifyWorkingDirectory() ? "{{ workingDir }}/" : "";
         List<String> renderedCommands = ScriptService.replaceInternalStorage(
             runContext,
@@ -114,13 +111,12 @@ public abstract class AbstractTaskRunnerTest {
                 "echo -n 'nested file from output dir' > {{outputDir}}/nested/file.txt",
                 "echo '::{\"outputs\":{\"logOutput\":\"Hello World\"}}::'"
             )),
-            (ignored, file) -> filesToUpload.add(file),
             taskRunner instanceof RemoteRunnerInterface
         );
         Mockito.when(commands.getCommands()).thenReturn(renderedCommands);
 
         List<String> filesToDownload = List.of("output.txt");
-        RunnerResult run = taskRunner.run(runContext, commands, filesToUpload, filesToDownload);
+        RunnerResult run = taskRunner.run(runContext, commands, filesToDownload);
 
         Map<String, URI> outputFiles = ScriptService.uploadOutputFiles(runContext, commands.getOutputDirectory());
         outputFiles.putAll(FilesService.outputFiles(runContext, filesToDownload));
@@ -186,7 +182,7 @@ public abstract class AbstractTaskRunnerTest {
         return "ubuntu";
     }
 
-    protected TaskCommands initScriptCommands(RunContext runContext) {
+    protected TaskCommands initScriptCommands(RunContext runContext) throws IOException {
         var commands = Mockito.mock(TaskCommands.class);
         Mockito.when(commands.getContainerImage()).thenReturn(defaultImage());
         Mockito.when(commands.getLogConsumer()).thenReturn(new AbstractLogConsumer() {
@@ -205,6 +201,7 @@ public abstract class AbstractTaskRunnerTest {
         Mockito.when(commands.getEnableOutputDirectory()).thenReturn(true);
         Mockito.when(commands.outputDirectoryEnabled()).thenReturn(true);
         Mockito.when(commands.getTimeout()).thenReturn(null);
+        Mockito.when(commands.relativeWorkingDirectoryFilesPaths()).thenCallRealMethod();
 
         return commands;
     }