Repository: kestra-io/kestra (2025-08-12T12:40:55Z)

Issue:
ForEachItem task delayed/stuck RUNNING after all child subflows complete including afterExecution
## Description:

When using the **io.kestra.plugin.core.flow.ForEachItem** task  to execute a large number of child subflows, the ForEachItem task itself remains in a RUNNING state for an extended period after all spawned child subflow executions, **including their afterExecution blocks**, have successfully completed and reached a final status in the Kestra UI. 
 
 
 
## Steps to Reproduce:

- Define two flows: a parent flow and a subflow, as provided below.
- The parent flow uses ForEachItem to iterate a number of items
- The ForEachItem task is configured with wait: true, batch.rows: 1, and calls the defined subflow.
- The subflow contains simple tasks (e.g., Log) and importantly, includes an afterExecution block.
- The parent flow has a task defined after the ForEachItem task.
 
 
 
## Expected behavior:

The ForEachItem task should transition from RUNNING to a final state shortly after the last child execution it was waiting for ts afterExecution phase completes. 

## Observed behavior:

The ForEachItem task in the parent flow remains in the RUNNING state for a considerable amount of time after all 1000 child subflow executions are confirmed as SUCCESS in the UI. This includes confirmation that the afterExecution task ran and completed for all child subflows. 

## Minimal Reproducible Example:

### Parent Flow:

```yaml
id: flow
namespace: test.flow

tasks:
  - id: extract
    type: io.kestra.plugin.jdbc.duckdb.Query
    sql: |
      INSTALL httpfs;
      LOAD httpfs;
      SELECT *
      FROM read_csv_auto('https://huggingface.co/datasets/kestra/datasets/raw/main/csv/orders.csv', header=True);
    store: true
  - id: each
    type: io.kestra.plugin.core.flow.ForEachItem
    items: "{{ outputs.extract.uri }}"
    batch:
      rows: 1
    namespace: test.flow
    flowId: subflow
    wait: true # wait for the subflow execution (including its afterExecution)
    transmitFailed: true # fail the task run if the subflow execution fails
    inputs:
      order: "{{ taskrun.items }}"
  - id: hello 
    type: io.kestra.plugin.core.log.Log
    message: Hello World!
```

### Sub Flow:

```yaml
id: subflow
namespace: test.flow

tasks:
  - id: hello
    type: io.kestra.plugin.core.log.Log
    message: Hello World! 🚀

afterExecution:
  - id: hello21
    type: io.kestra.plugin.core.log.Log
    message: Hello from afterExecution!
```

### Environment

Kestra Version: 0.22.4
OS: Linux
Executions, stuck in QUEUED state due to concurrency, never finish
### Describe the issue

Hello,

I am experiencing issues with flow concurrency. I use QUEUE mechanism for concurrency but it doesn't seem to work properly. All the executions that were put in QUEUED state due to concurrent multiple concurrent executions, will stuck in it forever and never finish. Here is yaml of such workflow:

```
id: flow_6
namespace: default
inputs:
- id: data_soubor
  type: FILE
  extension: .upl
- id: definice_soubor
  type: FILE
  extension: .upl
tasks:
- id: dag
  type: io.kestra.plugin.core.flow.Dag
  tasks:
  - task:
      id: task_27_1000017_sequential
      type: io.kestra.plugin.core.flow.Sequential
      tasks:
      - id: task_27_1000017_request
        type: io.kestra.plugin.core.http.Request
        uri: "{{ envs.api_public_url }}/api/processing/6/project-version-operation/27"
        options:
          timeout:
            connectTimeout: 30
            readIdleTimeout: 30
          followRedirects: "true"
          allowFailed: "false"
          defaultCharset: UTF-8
      - id: task_27_1000017_request_params
        type: io.kestra.plugin.core.http.Request
        uri: "{{ envs.api_public_url }}/api/processing/6/body-params"
        method: POST
        body: "{ \"operationVersionId\": 1000017, \"projectVersionOperationId\": 27 }"
        options:
          timeout:
            connectTimeout: 30
            readIdleTimeout: 30
          followRedirects: "true"
          allowFailed: "false"
          defaultCharset: UTF-8
      - id: task_27_1000017_pause
        type: io.kestra.plugin.core.flow.Pause
        runIf: "{{ json(outputs.task_27_1000017_request.body).breakpoint ?? false }}"
      - id: task_27_1000017
        type: io.kestra.plugin.scripts.python.Script
        runIf: "{{ json(outputs.task_27_1000017_request.body).process ?? true }}"
        taskRunner:
          type: io.kestra.plugin.scripts.runner.docker.Docker
        beforeCommands: "[\"pip install amazon.ion\",\"pip install pyyaml\"]"
        outputFiles: "[\"soubor.ion\"]"
        containerImage: ghcr.io/kestra-io/kestrapy
        script: |-
          import yaml
          import re
          from functools import reduce
          from kestra import Kestra
          import csv
          from amazon.ion import simpleion
          definition1_url = '{{inputs.definice_soubor ?? null}}'
          input_url = '{{inputs.data_soubor ?? null}}'
          output_url = 'soubor.ion'
          headers = []
          def evaluate_expression(expression, row):
              # expression is list, evaluate as function
              if isinstance(expression, list):
                  func, args = expression[0], expression[1:]
                  # evaluate function
                  match (func):
                      case '+':
                          return reduce(lambda x, y: (x + y) if x is not None and y is not None else x, [evaluate_expression(arg, element) for arg in args])
                      case '-':
                          return reduce(lambda x, y: (x - y) if x is not None and y is not None else x, [evaluate_expression(arg, element) for arg in args])
                      case '*':
                          return reduce(lambda x, y: (x * y) if x is not None and y is not None else x, [evaluate_expression(arg, element) for arg in args])
                      case '/':
                          return reduce(lambda x, y: (x / y) if x is not None and y is not None and y != 0 else x, [evaluate_expression(arg, element) for arg in args])
                      case "concat":
                          return ''.join([str(evaluate_expression(arg, element)) for arg in args])
              else:
                  # extract value from row if wrapped in $()
                  if isinstance(expression, str) and expression.startswith('$('):
                      column = re.findall(r'\$\((\w+)\)', expression)[0]
                      if column is None:
                          return column
                      return row.get(column)
                  else:
                      return eval(expression, {'__builtins__': {}})
          with open(definition1_url, 'r') as input_file:
              with open(output_url, 'w') as output_file:
                  with open(input_url, 'r') as file:
                      definition = yaml.safe_load(input_file)
                      mappings = definition['SPECIFIKACE_POLOZEK']
                      if mappings is None:
                          raise Exception('Specifikace polozek je prazdna')
                      index = 0
                      for row in csv.DictReader(file):
                          row_data = {}
                          for key, expression in mappings.items():
                              if index == 0:
                                  headers.append(key)
                              row_data[key] = evaluate_expression(expression, row)
                          output_file.write(simpleion.dumps(row_data, binary=False, omit_version_marker=True) + '\n')
                          index += 1
          Kestra.outputs({ 'hlavicka': ','.join(headers), 'insert_vyraz': ','.join(len(headers) * ['?']) })
afterExecution:
- id: post_request
  type: io.kestra.plugin.core.http.Request
  uri: "{{ envs.api_public_url }}/api/processing/6/execution/{{ execution.id }}/finished"
  method: POST
  options:
    timeout:
      connectTimeout: 30
      readIdleTimeout: 30
    followRedirects: "true"
    allowFailed: "false"
    defaultCharset: UTF-8
concurrency:
  limit: 1
  behavior: QUEUE
```

I am using 0.22.1 version of Kestra. Thank you in advance for any help.

### Environment

- Kestra Version: 0.22.1



Discussion:


Hello, can you try on version 0.24 of Kestra?
@Ben8t sure will do, will keep you updated
@Ben8t I confirm that in 0.24 new concurrent executions, stuck in QUEUED state, also never finish

<img width="3561" height="967" alt="Image" src="https://github.com/user-attachments/assets/e71eaa00-2023-449f-9e28-cec59a5c6c5d" />
@Ben8t weird it is even worse in 0.24.

I can't finish another flow execution after previous successfully finished. It goes to QUEUED state, even though there is no running execution of that flow, and then it never finishes

<img width="3095" height="295" alt="Image" src="https://github.com/user-attachments/assets/54aa8ff5-ac91-4124-af69-597f3e310ebf" />
@Ben8t after further investigation It looks like this behaviour is caused by `afterExecution` block.

Executing following `concurrency_test_dag` flow multiple times put concurrent executions in QUEUED state, which are later picked up and run

```
id: concurrency_test_dag
namespace: company.team

concurrency:
  limit: 1
  behavior: QUEUE

tasks:
  - id: dag
    type: io.kestra.plugin.core.flow.Dag
    tasks:
      - task:
          id: task_27_1000017_sequential
          type: io.kestra.plugin.core.flow.Sequential
          tasks:
            - id: task
              type: io.kestra.plugin.scripts.python.Script
              taskRunner:
                type: io.kestra.plugin.scripts.runner.docker.Docker
              containerImage: ghcr.io/kestra-io/kestrapy
              script: |-
                import time
                time.sleep(10)
            - id: hello
              type: io.kestra.plugin.scripts.shell.Commands
              commands:
                - sleep 10
```

Executing following `concurrency_test_dag` flow multiple times put concurrent executions in QUEUED state, but this executions stuck in QUEUED state and never picked up to run

```
id: concurrency_test_after_execution
namespace: company.team

tasks:
  - id: dag
    type: io.kestra.plugin.core.flow.Dag
    tasks:
    - task:
        id: task_27_1000017_sequential
        type: io.kestra.plugin.core.flow.Sequential
        tasks:
        - id: task
          type: io.kestra.plugin.scripts.python.Script
          taskRunner:
            type: io.kestra.plugin.scripts.runner.docker.Docker
          containerImage: ghcr.io/kestra-io/kestrapy
          script: |-
            import time
            time.sleep(10)
        - id: hello
          type: io.kestra.plugin.scripts.shell.Commands
          commands:
            - sleep 10
afterExecution:
  - id: post_request
    type: io.kestra.plugin.core.http.Request
    uri: "{{ envs.csu_spn_api_public_url }}/api/processing/6/execution/2xRB1ZWdjCNW6I5t7mSsGB/finished"
    method: POST
    options:
      timeout:
        connectTimeout: 30
        readIdleTimeout: 30
      followRedirects: "true"
      allowFailed: "false"
      defaultCharset: UTF-8
  concurrency:
    limit: 1
    behavior: QUEUE
```

<img width="2430" height="442" alt="Image" src="https://github.com/user-attachments/assets/f504bbe6-cc27-4f87-9050-f686c600b6bb" />
Can you try with this simpler flow please?

```yaml
id: jay_386588
namespace: company.team
concurrency:
  behavior: QUEUE
  limit: 1

tasks:
  - id: wait
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT15S
```

I'm not able to reproduce with this one, but maybe there's something deeper to check. Let me know what's the behavior with this one on your side
@Ben8t Hello, I tried with simpler flows and behavior is exactly the same. Following flow works correctly - executions that were initially put in QUEUED state due to concurrency limits, later will be picked up and executed:
```
id: concurrency_test_sleep
namespace: company.team

concurrency:
  limit: 1
  behavior: QUEUE

tasks:
  - id: wait
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT10S
```

However concurrent executions of following flow, stuck in QUEUED state because of concurrency limit, will never finish and indefinitely hang in QUEUED state
```
id: concurrency_test_sleep_after_execution
namespace: company.team

concurrency:
  limit: 1
  behavior: QUEUE

tasks:
  - id: wait
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT10S

afterExecution:
  - id: wait_after_execution
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT5S
```

<img width="1544" height="346" alt="Image" src="https://github.com/user-attachments/assets/094dc27b-5e66-4db4-bb7a-cba0ce97cada" />
@Ben8t investigated this problem in Kestra 0.24.* and the behavior there is even worse. Flow, that has tasks in `afterExecution` block, will block following executions regardless if they were executed concurrently or not. It means each such flow can be executed only once, all subsequent will be placed in QUEUE or CANCELLED according to concurrency behavior, regardless if they were executed concurrently or not.

I tried it with the following simple flow:

```
id: concurrency_test_sleep_after_execution
namespace: company.team

concurrency:
  limit: 1
  behavior: QUEUE

tasks:
  - id: wait
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT10S

afterExecution:
  - id: wait_after_execution
    type: io.kestra.plugin.core.flow.Sleep
    duration: PT5S
```

<img width="3246" height="150" alt="Image" src="https://github.com/user-attachments/assets/b9badcaf-6895-468f-b84c-6876c738e045" />

In the picture is clearly seen that second was executed after the first one, thus not reaching concurrency limit. However it will end up in QUEUED state indefinitely, blocking all future executions



Patch:
diff --git a/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java b/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
index 537e40c6063..516e4d35957 100644
--- a/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
+++ b/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
@@ -1065,7 +1065,11 @@ private void toExecution(Executor executor, boolean ignoreFailure) {
                 executorService.log(log, false, executor);
             }
 
-            // the terminated state can only come from the execution queue, in this case we always have a flow in the executor
+            // the terminated state can come from the execution queue, in this case we always have a flow in the executor
+            // or from a worker task in an afterExecution block, in this case we need to load the flow
+            if (executor.getFlow() == null && executor.getExecution().getState().isTerminated()) {
+                executor = executor.withFlow(findFlow(executor.getExecution()));
+            }
             boolean isTerminated = executor.getFlow() != null && executionService.isTerminated(executor.getFlow(), executor.getExecution());
 
             // purge the executionQueue


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/AbstractRunnerTest.java b/core/src/test/java/io/kestra/core/runners/AbstractRunnerTest.java
index 60972d4ecb9..a1be581cdb7 100644
--- a/core/src/test/java/io/kestra/core/runners/AbstractRunnerTest.java
+++ b/core/src/test/java/io/kestra/core/runners/AbstractRunnerTest.java
@@ -387,6 +387,13 @@ protected void forEachItemInIf() throws Exception {
         forEachItemCaseTest.forEachItemInIf();
     }
 
+    @Test
+    @LoadFlows({"flows/valids/for-each-item-subflow-after-execution.yaml",
+        "flows/valids/for-each-item-after-execution.yaml"})
+    protected void forEachItemWithAfterExecution() throws Exception {
+        forEachItemCaseTest.forEachItemWithAfterExecution();
+    }
+
     @Test
     @LoadFlows({"flows/valids/flow-concurrency-cancel.yml"})
     void concurrencyCancel() throws Exception {
@@ -429,6 +436,12 @@ void concurrencyQueueRestarted() throws Exception {
         flowConcurrencyCaseTest.flowConcurrencyQueueRestarted();
     }
 
+    @Test
+    @LoadFlows({"flows/valids/flow-concurrency-queue-after-execution.yml"})
+    void concurrencyQueueAfterExecution() throws Exception {
+        flowConcurrencyCaseTest.flowConcurrencyQueueAfterExecution();
+    }
+
     @Test
     @ExecuteFlow("flows/valids/executable-fail.yml")
     void badExecutable(Execution execution) {
diff --git a/core/src/test/java/io/kestra/core/runners/FlowConcurrencyCaseTest.java b/core/src/test/java/io/kestra/core/runners/FlowConcurrencyCaseTest.java
index bc083fa083b..203793b13ef 100644
--- a/core/src/test/java/io/kestra/core/runners/FlowConcurrencyCaseTest.java
+++ b/core/src/test/java/io/kestra/core/runners/FlowConcurrencyCaseTest.java
@@ -342,6 +342,55 @@ public void flowConcurrencyQueueRestarted() throws Exception {
         assertThat(executionResult2.get().getState().getHistories().get(2).getState()).isEqualTo(State.Type.RUNNING);
     }
 
+    public void flowConcurrencyQueueAfterExecution() throws TimeoutException, QueueException, InterruptedException {
+        Execution execution1 = runnerUtils.runOneUntilRunning(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-after-execution", null, null, Duration.ofSeconds(30));
+        Flow flow = flowRepository
+            .findById(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-after-execution", Optional.empty())
+            .orElseThrow();
+        Execution execution2 = Execution.newExecution(flow, null, null, Optional.empty());
+        executionQueue.emit(execution2);
+
+        assertThat(execution1.getState().isRunning()).isTrue();
+        assertThat(execution2.getState().getCurrent()).isEqualTo(State.Type.CREATED);
+
+        var executionResult1  = new AtomicReference<Execution>();
+        var executionResult2  = new AtomicReference<Execution>();
+
+        CountDownLatch latch1 = new CountDownLatch(1);
+        CountDownLatch latch2 = new CountDownLatch(1);
+        CountDownLatch latch3 = new CountDownLatch(1);
+
+        Flux<Execution> receive = TestsUtils.receive(executionQueue, e -> {
+            if (e.getLeft().getId().equals(execution1.getId())) {
+                executionResult1.set(e.getLeft());
+                if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
+                    latch1.countDown();
+                }
+            }
+
+            if (e.getLeft().getId().equals(execution2.getId())) {
+                executionResult2.set(e.getLeft());
+                if (e.getLeft().getState().getCurrent() == State.Type.RUNNING) {
+                    latch2.countDown();
+                }
+                if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
+                    latch3.countDown();
+                }
+            }
+        });
+
+        assertTrue(latch1.await(1, TimeUnit.MINUTES));
+        assertTrue(latch2.await(1, TimeUnit.MINUTES));
+        assertTrue(latch3.await(1, TimeUnit.MINUTES));
+        receive.blockLast();
+
+        assertThat(executionResult1.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
+        assertThat(executionResult2.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
+        assertThat(executionResult2.get().getState().getHistories().getFirst().getState()).isEqualTo(State.Type.CREATED);
+        assertThat(executionResult2.get().getState().getHistories().get(1).getState()).isEqualTo(State.Type.QUEUED);
+        assertThat(executionResult2.get().getState().getHistories().get(2).getState()).isEqualTo(State.Type.RUNNING);
+    }
+
     private URI storageUpload() throws URISyntaxException, IOException {
         File tempFile = File.createTempFile("file", ".txt");
 
diff --git a/core/src/test/java/io/kestra/plugin/core/flow/ForEachItemCaseTest.java b/core/src/test/java/io/kestra/plugin/core/flow/ForEachItemCaseTest.java
index 8e4abffbef9..d9ddea70ecd 100644
--- a/core/src/test/java/io/kestra/plugin/core/flow/ForEachItemCaseTest.java
+++ b/core/src/test/java/io/kestra/plugin/core/flow/ForEachItemCaseTest.java
@@ -372,6 +372,51 @@ public void forEachItemInIf() throws TimeoutException, InterruptedException, URI
         assertThat(correlationId.get().value()).isEqualTo(execution.getId());
     }
 
+    public void forEachItemWithAfterExecution() throws TimeoutException, InterruptedException, URISyntaxException, IOException, QueueException {
+        CountDownLatch countDownLatch = new CountDownLatch(26);
+        AtomicReference<Execution> triggered = new AtomicReference<>();
+
+        Flux<Execution> receive = TestsUtils.receive(executionQueue, either -> {
+            Execution execution = either.getLeft();
+            if (execution.getFlowId().equals("for-each-item-subflow-after-execution") && execution.getState().getCurrent().isTerminated()) {
+                triggered.set(execution);
+                countDownLatch.countDown();
+            }
+        });
+
+        URI file = storageUpload();
+        Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
+        Execution execution = runnerUtils.runOne(MAIN_TENANT, TEST_NAMESPACE, "for-each-item-after-execution", null,
+            (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
+            Duration.ofSeconds(30));
+
+        // we should have triggered 26 subflows
+        assertThat(countDownLatch.await(1, TimeUnit.MINUTES)).isTrue();
+        receive.blockLast();
+
+        // assert on the main flow execution
+        assertThat(execution.getTaskRunList()).hasSize(5);
+        assertThat(execution.getTaskRunList().get(2).getAttempts()).hasSize(1);
+        assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
+        assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
+        Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
+        assertThat(outputs.get("numberOfBatches")).isEqualTo(26);
+        assertThat(outputs.get("iterations")).isNotNull();
+        Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations");
+        assertThat(iterations.get("CREATED")).isZero();
+        assertThat(iterations.get("RUNNING")).isZero();
+        assertThat(iterations.get("SUCCESS")).isEqualTo(26);
+
+        // assert on the last subflow execution
+        assertThat(triggered.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
+        assertThat(triggered.get().getFlowId()).isEqualTo("for-each-item-subflow-after-execution");
+        assertThat((String) triggered.get().getInputs().get("items")).matches("kestra:///io/kestra/tests/for-each-item-after-execution/executions/.*/tasks/each-split/.*\\.txt");
+        assertThat(triggered.get().getTaskRunList()).hasSize(2);
+        Optional<Label> correlationId = triggered.get().getLabels().stream().filter(label -> label.key().equals(Label.CORRELATION_ID)).findAny();
+        assertThat(correlationId.isPresent()).isTrue();
+        assertThat(correlationId.get().value()).isEqualTo(execution.getId());
+    }
+
     private URI storageUpload() throws URISyntaxException, IOException {
         File tempFile = File.createTempFile("file", ".txt");
 
diff --git a/core/src/test/resources/flows/valids/flow-concurrency-queue-after-execution.yml b/core/src/test/resources/flows/valids/flow-concurrency-queue-after-execution.yml
new file mode 100644
index 00000000000..8c988b62e4e
--- /dev/null
+++ b/core/src/test/resources/flows/valids/flow-concurrency-queue-after-execution.yml
@@ -0,0 +1,17 @@
+id: flow-concurrency-queue-after-execution
+namespace: io.kestra.tests
+
+concurrency:
+  behavior: QUEUE
+  limit: 1
+
+tasks:
+  - id: sleep
+    type: io.kestra.plugin.core.flow.Sleep
+    duration: PT2S
+
+afterExecution:
+  - id: afterExecution
+    type: io.kestra.plugin.core.output.OutputValues
+    values:
+      some: value
\ No newline at end of file
diff --git a/core/src/test/resources/flows/valids/for-each-item-after-execution.yaml b/core/src/test/resources/flows/valids/for-each-item-after-execution.yaml
new file mode 100644
index 00000000000..4defbb2ddf6
--- /dev/null
+++ b/core/src/test/resources/flows/valids/for-each-item-after-execution.yaml
@@ -0,0 +1,26 @@
+id: for-each-item-after-execution
+namespace: io.kestra.tests
+
+inputs:
+  - id: file
+    type: FILE
+  - id: batch
+    type: INT
+
+tasks:
+  - id: each
+    type: io.kestra.plugin.core.flow.ForEachItem
+    items: "{{ inputs.file }}"
+    batch:
+      rows: "{{inputs.batch}}"
+    namespace: io.kestra.tests
+    flowId: for-each-item-subflow-after-execution
+    wait: true
+    transmitFailed: true
+    inputs:
+      items: "{{ taskrun.items }}"
+
+afterExecution:
+  - id: afterExecution
+    type: io.kestra.plugin.core.log.Log
+    message: Hello from afterExecution!
\ No newline at end of file
diff --git a/core/src/test/resources/flows/valids/for-each-item-subflow-after-execution.yaml b/core/src/test/resources/flows/valids/for-each-item-subflow-after-execution.yaml
new file mode 100644
index 00000000000..2a1c7ec7ee4
--- /dev/null
+++ b/core/src/test/resources/flows/valids/for-each-item-subflow-after-execution.yaml
@@ -0,0 +1,16 @@
+id: for-each-item-subflow-after-execution
+namespace: io.kestra.tests
+
+inputs:
+  - id: items
+    type: STRING
+
+tasks:
+  - id: per-item
+    type: io.kestra.plugin.core.log.Log
+    message: "{{ inputs.items }}"
+
+afterExecution:
+  - id: afterExecution
+    type: io.kestra.plugin.core.log.Log
+    message: Hello from afterExecution!
\ No newline at end of file