This is an automated email from the ASF dual-hosted git repository.

bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 496044bda2 Fixes #8670: Fix execution details and parent links in 
execution information (#8850)
496044bda2 is described below

commit 496044bda20e0b32381439bc5576c6507c10eee2
Author: Matt Casters <[email protected]>
AuthorDate: Sun Oct 11 07:58:27 2026 +0200

    Fixes #8670: Fix execution details and parent links in execution 
information (#8850)
    
    * Fixes #8670: Fix execution information parent-child links and transform 
row sampling
    
    - Fix row sampling clearing issue: create defensive copies in RowBuffer and 
data sampler stores so that clearing store samples does not wipe out row 
buffers in ExecutionData before persisting.
    - Close execution info location before releasing published samples in 
LocalPipelineEngine.
    - Fix parent/child link resolution in caching and file execution info 
locations (BaseCachingExecutionInfoLocation, CachingFileExecutionInfoLocation, 
CachingDatabaseExecutionInfoLocation, FileExecutionInfoLocation) when resolving 
child actions and pipelines.
    - Fix drill down in WorkflowExecutionViewer when selectedExecutionData is 
null.
    - Add integration test suite 002 in integration-tests/exec-info to 
reproduce and validate the fix.
    
    * Address review feedback on #8850: fix database child matching and bound 
file scans
---
 .../java/org/apache/hop/core/row/RowBuffer.java    |   8 +-
 .../caching/BaseCachingExecutionInfoLocation.java  |  64 ++-
 .../apache/hop/execution/caching/CacheEntry.java   |  31 +-
 .../caching/CachingFileExecutionInfoLocation.java  |  62 ++-
 .../execution/local/FileExecutionInfoLocation.java |   8 +-
 .../first/FirstRowsExecutionDataSamplerStore.java  |  10 +-
 .../last/LastRowsExecutionDataSamplerStore.java    |  10 +-
 .../RandomRowsExecutionDataSamplerStore.java       |  10 +-
 .../engines/local/LocalPipelineEngine.java         |   4 +-
 .../BaseCachingExecutionInfoLocationTest.java      |  90 +++
 .../CachingFileExecutionInfoLocationTest.java      |  63 +++
 integration-tests/exec-info/002-mapping-sub.hpl    | 134 +++++
 .../exec-info/002-pipeline-with-mapping.hpl        | 219 ++++++++
 integration-tests/exec-info/002-sub-workflow.hwf   |  86 +++
 integration-tests/exec-info/002-validation.hpl     | 615 +++++++++++++++++++++
 .../main-002-workflow-pipeline-mapping.hwf         | 138 +++++
 .../CachingDatabaseExecutionInfoLocation.java      |  83 +++
 .../CachingDatabaseExecutionInfoLocationTest.java  |  66 +++
 .../execution/WorkflowExecutionViewer.java         |  25 +-
 19 files changed, 1707 insertions(+), 19 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/row/RowBuffer.java 
b/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
index 9427230ad8..9f63324767 100644
--- a/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
+++ b/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
@@ -48,7 +48,13 @@ public class RowBuffer {
    */
   public RowBuffer(IRowMeta rowMeta, List<Object[]> buffer) {
     this.rowMeta = rowMeta;
-    this.buffer = buffer;
+    if (buffer == null) {
+      this.buffer = Collections.synchronizedList(new ArrayList<>());
+    } else {
+      synchronized (buffer) {
+        this.buffer = Collections.synchronizedList(new ArrayList<>(buffer));
+      }
+    }
   }
 
   /**
diff --git 
a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
 
b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
index 4704a1c010..21425f8cce 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
@@ -798,9 +798,15 @@ public abstract class BaseCachingExecutionInfoLocation 
implements IExecutionInfo
   @Override
   public synchronized Execution getExecution(String executionId) throws 
HopException {
     CacheEntry entry = findCacheEntry(executionId);
+    if (entry == null) {
+      entry = findCacheEntryWithChild(executionId);
+    }
     if (entry == null) {
       return null;
     }
+    if (!entry.getId().equals(executionId)) {
+      return entry.getChildExecution(executionId);
+    }
     Execution execution = entry.getExecution();
     if (execution != null
         && entry.isHeavyDocumentStored()
@@ -829,9 +835,25 @@ public abstract class BaseCachingExecutionInfoLocation 
implements IExecutionInfo
     try {
       Set<Execution> executions = new HashSet<>();
 
-      for (String id : getExecutionIds(true, 10000)) {
+      if (StringUtils.isNotEmpty(parentExecutionId)) {
+        CacheEntry parentEntry = findCacheEntry(parentExecutionId);
+        if (parentEntry == null) {
+          parentEntry = findCacheEntryWithChild(parentExecutionId);
+        }
+        if (parentEntry != null && parentEntry.getChildExecutions() != null) {
+          for (Execution childExecution : 
parentEntry.getChildExecutions().values()) {
+            if (childExecution != null && 
parentExecutionId.equals(childExecution.getParentId())) {
+              executions.add(childExecution);
+            }
+          }
+        }
+      }
+
+      for (String id : getExecutionIds(false, 10000)) {
         Execution execution = getExecution(id);
-        if (execution != null && 
parentExecutionId.equals(execution.getParentId())) {
+        if (execution != null
+            && parentExecutionId != null
+            && parentExecutionId.equals(execution.getParentId())) {
           executions.add(execution);
         }
       }
@@ -974,10 +996,46 @@ public abstract class BaseCachingExecutionInfoLocation 
implements IExecutionInfo
 
   @Override
   public String findParentId(String childId) throws HopException {
+    if (StringUtils.isEmpty(childId)) {
+      return null;
+    }
     CacheEntry cacheEntry = findCacheEntry(childId);
+    if (cacheEntry == null) {
+      cacheEntry = findCacheEntryWithChild(childId);
+    }
     if (cacheEntry == null) {
       return null;
     }
-    return cacheEntry.getId();
+    if (cacheEntry.getId().equals(childId)) {
+      return cacheEntry.getExecution() != null ? 
cacheEntry.getExecution().getParentId() : null;
+    }
+    Execution child = cacheEntry.getChildExecution(childId);
+    if (child != null && child.getParentId() != null) {
+      return child.getParentId();
+    }
+    if (child != null
+        || (cacheEntry.getChildIds() != null && 
cacheEntry.getChildIds().contains(childId))) {
+      return cacheEntry.getId();
+    }
+    return null;
+  }
+
+  protected synchronized CacheEntry findCacheEntryWithChild(String childId) 
throws HopException {
+    if (StringUtils.isEmpty(childId)) {
+      return null;
+    }
+    for (CacheEntry cacheEntry : cache.values()) {
+      if (cacheEntry.peekChildExecution(childId) != null
+          || (cacheEntry.getChildIds() != null && 
cacheEntry.getChildIds().contains(childId))) {
+        cache.get(cacheEntry.getId());
+        cacheEntry.markRead();
+        return cacheEntry;
+      }
+    }
+    return loadCacheEntryWithChild(childId);
+  }
+
+  protected CacheEntry loadCacheEntryWithChild(String childId) throws 
HopException {
+    return null;
   }
 }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java 
b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
index 697e84c925..f5caf13fa9 100644
--- a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
+++ b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
@@ -35,6 +35,8 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.core.row.RowBuffer;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.execution.Execution;
@@ -168,7 +170,7 @@ public class CacheEntry {
     // was created successfully via VFS. Named schemes (db-volume://) need 
variables so providers
     // load from project metadata.
     try (OutputStream os = HopVfs.getOutputStream(filename, false, variables)) 
{
-      ObjectMapper objectMapper = new ObjectMapper();
+      ObjectMapper objectMapper = HopJson.newMapper();
       objectMapper.writeValue(os, this);
     } catch (Exception e) {
       throw new HopException("Error writing cache entry to file '" + 
targetFilename + "'", e);
@@ -257,7 +259,30 @@ public class CacheEntry {
   }
 
   public void addExecutionData(ExecutionData executionData) {
-    childExecutionData.put(executionData.getOwnerId(), executionData);
+    ExecutionData existing = 
childExecutionData.get(executionData.getOwnerId());
+    if (existing == null) {
+      childExecutionData.put(executionData.getOwnerId(), executionData);
+    } else {
+      existing.setFinished(executionData.isFinished() || 
existing.isFinished());
+      existing.setCollectionDate(executionData.getCollectionDate());
+      if (executionData.getDataSetMeta() != null) {
+        existing.setDataSetMeta(executionData.getDataSetMeta());
+      }
+      if (executionData.getSetMetaData() != null) {
+        existing.getSetMetaData().putAll(executionData.getSetMetaData());
+      }
+      if (executionData.getDataSets() != null) {
+        for (Map.Entry<String, RowBuffer> entry : 
executionData.getDataSets().entrySet()) {
+          RowBuffer newBuffer = entry.getValue();
+          RowBuffer existingBuffer = 
existing.getDataSets().get(entry.getKey());
+          if (newBuffer != null && !newBuffer.isEmpty()) {
+            existing.getDataSets().put(entry.getKey(), newBuffer);
+          } else if (existingBuffer == null) {
+            existing.getDataSets().put(entry.getKey(), newBuffer);
+          }
+        }
+      }
+    }
     flagDirty();
   }
 
@@ -287,7 +312,7 @@ public class CacheEntry {
   }
 
   /** Read a child without counting as a cache hit. */
-  Execution peekChildExecution(String id) {
+  public Execution peekChildExecution(String id) {
     if (childExecutions == null) {
       return null;
     }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
 
b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
index e3dfd0fecf..cd5728c324 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
@@ -23,11 +23,14 @@ import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import lombok.Getter;
@@ -410,7 +413,7 @@ public class CachingFileExecutionInfoLocation extends 
BaseCachingExecutionInfoLo
       if (!HopVfs.fileExists(filename, variables)) {
         return null;
       }
-      ObjectMapper objectMapper = new ObjectMapper(HopJson.newFactory());
+      ObjectMapper objectMapper = HopJson.newMapper();
       return objectMapper.readValue(HopVfs.getInputStream(filename, 
variables), CacheEntry.class);
     } catch (Exception e) {
       throw new HopException(
@@ -419,6 +422,63 @@ public class CachingFileExecutionInfoLocation extends 
BaseCachingExecutionInfoLo
     }
   }
 
+  public static final int MAX_CHILD_SEARCH_SCAN = 100;
+
+  @Override
+  protected synchronized CacheEntry loadCacheEntryWithChild(String childId) 
throws HopException {
+    if (StringUtils.isEmpty(childId)) {
+      return null;
+    }
+    try {
+      FileObject[] files = getAllFileObjects(actualRootFolder);
+      if (files == null || files.length == 0) {
+        return null;
+      }
+
+      try {
+        record FileWithTimestamp(FileObject file, long lastModified) {}
+        List<FileWithTimestamp> fileList = new ArrayList<>(files.length);
+        for (FileObject file : files) {
+          long lastModified = 0L;
+          try {
+            lastModified = file.getContent().getLastModifiedTime();
+          } catch (Exception ignored) {
+          }
+          fileList.add(new FileWithTimestamp(file, lastModified));
+        }
+        fileList.sort((a, b) -> Long.compare(b.lastModified, a.lastModified));
+
+        int maxScan = Math.min(fileList.size(), 
Math.max(MAX_CHILD_SEARCH_SCAN, maxSize * 2));
+        for (int i = 0; i < maxScan; i++) {
+          FileObject file = fileList.get(i).file();
+          String id = getIdFromFileName(file);
+          if (id.equals(childId)) {
+            continue;
+          }
+          String content =
+              HopVfs.getTextFileContent(file.getName().getURI(), 
StandardCharsets.UTF_8);
+          if (content != null && content.contains(childId)) {
+            CacheEntry entry = HopJson.newMapper().readValue(content, 
CacheEntry.class);
+            if (entry != null
+                && (entry.peekChildExecution(childId) != null
+                    || (entry.getChildIds() != null && 
entry.getChildIds().contains(childId)))) {
+              cache.put(entry.getId(), entry);
+              enforceMaxCacheSize();
+              return entry;
+            }
+          }
+        }
+      } finally {
+        for (FileObject file : files) {
+          closeQuietly(file);
+        }
+      }
+    } catch (Exception e) {
+      LogChannel.GENERAL.logError("Error searching for cache entry containing 
child " + childId, e);
+    }
+    return null;
+  }
+
   @Override
   protected void retrieveIds(
       boolean includeChildren, Set<DatedId> ids, int limit, final 
IExecutionSelector selector)
diff --git 
a/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
 
b/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
index b3add061df..beecae6a42 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
@@ -784,9 +784,15 @@ public class FileExecutionInfoLocation implements 
IExecutionInfoLocation {
   @Override
   public synchronized String findParentId(String childId) throws HopException {
     try {
+      Execution execution = getExecution(childId);
+      if (execution != null && 
StringUtils.isNotEmpty(execution.getParentId())) {
+        return execution.getParentId();
+      }
       for (String id : getExecutionIds(true, 100)) {
         ExecutionState executionState = getExecutionState(id);
-        if (executionState.getChildIds().contains(childId)) {
+        if (executionState != null
+            && executionState.getChildIds() != null
+            && executionState.getChildIds().contains(childId)) {
           return id;
         }
       }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
index c1502e9703..fb360ed220 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
@@ -69,9 +69,17 @@ public class FirstRowsExecutionDataSamplerStore
 
   @Override
   public Map<String, RowBuffer> getSamples() {
+    List<Object[]> copy;
+    if (rows != null) {
+      synchronized (rows) {
+        copy = new java.util.ArrayList<>(rows);
+      }
+    } else {
+      copy = new java.util.ArrayList<>();
+    }
     return Map.of(
         getKeyForStore(EXECUTION_DATA_SAMPLE_FIRST_OUTPUT, samplerMeta),
-        new RowBuffer(rowMeta, rows));
+        new RowBuffer(rowMeta, copy));
   }
 
   @Override
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
index dfc95a61d9..df5899f41b 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
@@ -67,9 +67,17 @@ public class LastRowsExecutionDataSamplerStore
 
   @Override
   public Map<String, RowBuffer> getSamples() {
+    List<Object[]> copy;
+    if (rows != null) {
+      synchronized (rows) {
+        copy = new java.util.ArrayList<>(rows);
+      }
+    } else {
+      copy = new java.util.ArrayList<>();
+    }
     return Map.of(
         getKeyForStore(EXECUTION_DATA_SAMPLE_LAST_OUTPUT, samplerMeta),
-        new RowBuffer(rowMeta, rows));
+        new RowBuffer(rowMeta, copy));
   }
 
   @Override
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
index dee29d4940..8c0a3470cb 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
@@ -65,9 +65,17 @@ public class RandomRowsExecutionDataSamplerStore
 
   @Override
   public Map<String, RowBuffer> getSamples() {
+    List<Object[]> copy;
+    if (rows != null) {
+      synchronized (rows) {
+        copy = new java.util.ArrayList<>(rows);
+      }
+    } else {
+      copy = new java.util.ArrayList<>();
+    }
     return Map.of(
         getKeyForStore(EXECUTION_DATA_SAMPLE_RANDOM_OUTPUT, samplerMeta),
-        new RowBuffer(rowMeta, rows));
+        new RowBuffer(rowMeta, copy));
   }
 
   @Override
diff --git 
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
 
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
index 617c27ca92..add8d54751 100644
--- 
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
+++ 
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
@@ -565,7 +565,6 @@ public class LocalPipelineEngine extends Pipeline 
implements IPipelineEngine<Pip
 
               try {
                 writeExecutionInfoState(iLocation);
-                releasePublishedSamples();
               } catch (Exception e) {
                 log.logError(
                     "Warning: unable to register execution state at location "
@@ -711,7 +710,6 @@ public class LocalPipelineEngine extends Pipeline 
implements IPipelineEngine<Pip
             ExecutionDataBuilder.fromAllTransformData(
                 LocalPipelineEngine.this, samplerStoresMap, true);
         iLocation.registerData(dataBuilder.build());
-        releasePublishedSamples();
       }
     } catch (Throwable e) {
       log.logError("Error handling writing final pipeline state to location 
(non-fatal)", e);
@@ -722,6 +720,8 @@ public class LocalPipelineEngine extends Pipeline 
implements IPipelineEngine<Pip
         iLocation.close();
       } catch (Exception e) {
         log.logError("Error closing execution information location: " + 
location.getName(), e);
+      } finally {
+        releasePublishedSamples();
       }
     }
   }
diff --git 
a/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
 
b/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
index b6c62b4e41..0c7d9962ef 100644
--- 
a/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
+++ 
b/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
@@ -29,8 +29,13 @@ import java.util.Set;
 import java.util.UUID;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.execution.Execution;
+import org.apache.hop.execution.ExecutionData;
+import org.apache.hop.execution.ExecutionDataSetMeta;
 import org.apache.hop.execution.ExecutionState;
 import org.apache.hop.execution.ExecutionType;
 import org.apache.hop.execution.IExecutionSelector;
@@ -152,6 +157,91 @@ class BaseCachingExecutionInfoLocationTest {
     assertTrue(location.persisted.contains(id));
   }
 
+  @Test
+  void testParentChildLinkResolution() throws Exception {
+    FakeLocation location = new FakeLocation();
+    String workflowId = "wf-1";
+    String actionId = "act-1";
+    String pipelineId = "pipe-1";
+
+    Execution wf = new Execution();
+    wf.setId(workflowId);
+    wf.setName("Workflow");
+    wf.setExecutionType(ExecutionType.Workflow);
+    wf.setRegistrationDate(new Date());
+
+    Execution action = new Execution();
+    action.setId(actionId);
+    action.setName("ActionPipeline");
+    action.setParentId(workflowId);
+    action.setExecutionType(ExecutionType.Action);
+    action.setRegistrationDate(new Date());
+
+    Execution pipe = new Execution();
+    pipe.setId(pipelineId);
+    pipe.setName("Pipeline");
+    pipe.setParentId(actionId);
+    pipe.setExecutionType(ExecutionType.Pipeline);
+    pipe.setRegistrationDate(new Date());
+
+    location.registerExecution(wf);
+    location.registerExecution(action);
+    location.registerExecution(pipe);
+
+    assertEquals(actionId, location.findParentId(pipelineId));
+    assertEquals(workflowId, location.findParentId(actionId));
+    assertNull(location.findParentId(workflowId));
+
+    Execution retrievedAction = location.getExecution(actionId);
+    assertNotNull(retrievedAction);
+    assertEquals(actionId, retrievedAction.getId());
+    assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+    java.util.List<Execution> actionChildren = 
location.findExecutions(actionId);
+    assertEquals(1, actionChildren.size());
+    assertEquals(pipelineId, actionChildren.get(0).getId());
+
+    java.util.List<Execution> wfChildren = location.findExecutions(workflowId);
+    assertTrue(wfChildren.stream().anyMatch(e -> e.getId().equals(actionId)));
+  }
+
+  @Test
+  void testAddExecutionDataRetainsNonEmptySamples() {
+    CacheEntry entry = new CacheEntry();
+    entry.setId("pipe-1");
+
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("col1"));
+
+    RowBuffer bufferWithRows = new RowBuffer(rowMeta);
+    bufferWithRows.addRow(new Object[] {"value1"});
+
+    ExecutionData data1 = new ExecutionData();
+    data1.setOwnerId("all-transforms");
+    data1.getDataSets().put("set1", bufferWithRows);
+    data1
+        .getSetMetaData()
+        .put("set1", new ExecutionDataSetMeta("set1", "log1", "transform1", 
"0", "desc1"));
+
+    entry.addExecutionData(data1);
+
+    assertEquals(1, 
entry.getExecutionData("all-transforms").getDataSets().get("set1").size());
+
+    // Subsequent tick with empty rows
+    RowBuffer emptyBuffer = new RowBuffer(rowMeta);
+    ExecutionData data2 = new ExecutionData();
+    data2.setOwnerId("all-transforms");
+    data2.getDataSets().put("set1", emptyBuffer);
+    data2
+        .getSetMetaData()
+        .put("set1", new ExecutionDataSetMeta("set1", "log1", "transform1", 
"0", "desc1"));
+
+    entry.addExecutionData(data2);
+
+    // Verify samples were preserved
+    assertEquals(1, 
entry.getExecutionData("all-transforms").getDataSets().get("set1").size());
+  }
+
   private static Execution pipeline(String id, String name) {
     Execution execution = new Execution();
     execution.setId(id);
diff --git 
a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
 
b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
index c05b3863b9..252c2f39ba 100644
--- 
a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
+++ 
b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
@@ -522,6 +522,69 @@ class CachingFileExecutionInfoLocationTest {
     }
   }
 
+  @Test
+  void testUncachedParentChildLinkResolution() throws Exception {
+    Path root = tempDir.resolve("uncached-parent-child");
+    CachingFileExecutionInfoLocation location = new 
CachingFileExecutionInfoLocation();
+    location.setRootFolder(root.toAbsolutePath().toString());
+    location.initialize(new Variables(), null);
+    try {
+      String workflowId = "wf-1";
+      String actionId = "act-1";
+      String pipelineId = "pipe-1";
+
+      Execution wf = new Execution();
+      wf.setId(workflowId);
+      wf.setName("Workflow");
+      wf.setExecutionType(ExecutionType.Workflow);
+      wf.setRegistrationDate(new Date());
+
+      Execution action = new Execution();
+      action.setId(actionId);
+      action.setName("ActionPipeline");
+      action.setParentId(workflowId);
+      action.setExecutionType(ExecutionType.Action);
+      action.setRegistrationDate(new Date());
+
+      Execution pipe = new Execution();
+      pipe.setId(pipelineId);
+      pipe.setName("Pipeline");
+      pipe.setParentId(actionId);
+      pipe.setExecutionType(ExecutionType.Pipeline);
+      pipe.setRegistrationDate(new Date());
+
+      location.registerExecution(wf);
+      location.registerExecution(action);
+      location.registerExecution(pipe);
+
+      // Persist to disk and clear cache so nothing is in memory
+      for (CacheEntry entry : location.getCache().values()) {
+        location.persistCacheEntry(entry);
+      }
+      location.clearCaches();
+
+      assertEquals(actionId, location.findParentId(pipelineId));
+      assertEquals(workflowId, location.findParentId(actionId));
+      assertNull(location.findParentId(workflowId));
+
+      Execution retrievedAction = location.getExecution(actionId);
+      assertNotNull(retrievedAction);
+      assertEquals(actionId, retrievedAction.getId());
+      assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+      assertNull(location.getExecution("non-existent-id"));
+
+      List<Execution> actionChildren = location.findExecutions(actionId);
+      assertEquals(1, actionChildren.size());
+      assertEquals(pipelineId, actionChildren.get(0).getId());
+
+      List<Execution> wfChildren = location.findExecutions(workflowId);
+      assertTrue(wfChildren.stream().anyMatch(e -> 
e.getId().equals(actionId)));
+    } finally {
+      location.close();
+    }
+  }
+
   private static Execution executionAt(String id, Date start) {
     Execution execution = execution(id, null);
     execution.setExecutionStartDate(start);
diff --git a/integration-tests/exec-info/002-mapping-sub.hpl 
b/integration-tests/exec-info/002-mapping-sub.hpl
new file mode 100644
index 0000000000..a1cb79512d
--- /dev/null
+++ b/integration-tests/exec-info/002-mapping-sub.hpl
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>002-mapping-sub</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/10/10 14:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/10/10 14:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>input</from>
+      <to>add_desc</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>add_desc</from>
+      <to>output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>input</name>
+    <type>MappingInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>id</name>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>name</name>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>112</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>add_desc</name>
+    <type>Constant</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>desc</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif>mapped_description</nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>output</name>
+    <type>MappingOutput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>464</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/002-pipeline-with-mapping.hpl 
b/integration-tests/exec-info/002-pipeline-with-mapping.hpl
new file mode 100644
index 0000000000..d29b0d4d4d
--- /dev/null
+++ b/integration-tests/exec-info/002-pipeline-with-mapping.hpl
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>002-pipeline-with-mapping</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/10/10 14:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/10/10 14:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>10-rows</from>
+      <to>id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>id</from>
+      <to>name</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>name</from>
+      <to>simple-mapping</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>simple-mapping</from>
+      <to>output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>10-rows</name>
+    <type>RowGenerator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields/>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field/>
+    <never_ending>N</never_ending>
+    <limit>10</limit>
+    <row_time_field/>
+    <attributes/>
+    <GUI>
+      <xloc>80</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>id</name>
+    <type>Sequence</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <connection/>
+    <counter_name/>
+    <increment_by>1</increment_by>
+    <max_value>999999</max_value>
+    <schema/>
+    <seqname>SEQ_</seqname>
+    <start_at>1</start_at>
+    <use_counter>Y</use_counter>
+    <use_database>N</use_database>
+    <valuename>id</valuename>
+    <attributes/>
+    <GUI>
+      <xloc>208</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>name</name>
+    <type>Constant</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>name</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif>test_name</nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>336</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>simple-mapping</name>
+    <type>SimpleMapping</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <filename>${PROJECT_HOME}/002-mapping-sub.hpl</filename>
+    <mappings>
+      <input>
+        <mapping>
+          <connector>
+            <child>id</child>
+            <parent>id</parent>
+          </connector>
+          <connector>
+            <child>name</child>
+            <parent>name</parent>
+          </connector>
+          <main_path>Y</main_path>
+          <rename_on_output>Y</rename_on_output>
+        </mapping>
+      </input>
+      <output>
+        <mapping>
+          <connector>
+            <child>id</child>
+            <parent>id</parent>
+          </connector>
+          <connector>
+            <child>name</child>
+            <parent>name</parent>
+          </connector>
+          <connector>
+            <child>desc</child>
+            <parent>desc</parent>
+          </connector>
+          <main_path>Y</main_path>
+          <rename_on_output>N</rename_on_output>
+        </mapping>
+      </output>
+      <parameters>
+        <inherit_all_vars>Y</inherit_all_vars>
+      </parameters>
+    </mappings>
+    <runConfiguration>local</runConfiguration>
+    <attributes/>
+    <GUI>
+      <xloc>480</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>output</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>640</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/002-sub-workflow.hwf 
b/integration-tests/exec-info/002-sub-workflow.hwf
new file mode 100644
index 0000000000..7c5d501847
--- /dev/null
+++ b/integration-tests/exec-info/002-sub-workflow.hwf
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>002-sub-workflow</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <created_user>-</created_user>
+  <modified_user>-</modified_user>
+  <created_date>2026/10/10 14:00:00.000</created_date>
+  <modified_date>2026/10/10 14:00:00.000</modified_date>
+  <parameters/>
+  <actions>
+    <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <xloc>80</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <filename>${PROJECT_HOME}/002-pipeline-with-mapping.hpl</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <create_parent_folder>N</create_parent_folder>
+      <set_logfile>N</set_logfile>
+      <set_append_logfile>N</set_append_logfile>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <wait_until_finished>Y</wait_until_finished>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <run_configuration>local-caching-file</run_configuration>
+      <name>run-pipeline-with-mapping</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <xloc>288</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>run-pipeline-with-mapping</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+  </hops>
+  <notepads/>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/exec-info/002-validation.hpl 
b/integration-tests/exec-info/002-validation.hpl
new file mode 100644
index 0000000000..1ad604f812
--- /dev/null
+++ b/integration-tests/exec-info/002-validation.hpl
@@ -0,0 +1,615 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>002-validation</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/10/10 14:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/10/10 14:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>params</from>
+      <to>find-pipeline-execution</to>
+      <enabled>Y</enabled>
+    </hop>
+    <!-- Branch A: Parent-child links -->
+    <hop>
+      <from>find-pipeline-execution</from>
+      <to>verify-pipeline-has-parent</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>verify-pipeline-has-parent</from>
+      <to>find-pipeline-parent-id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>find-pipeline-parent-id</from>
+      <to>verify-pipeline-parent-id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>verify-pipeline-parent-id</from>
+      <to>select-before-action-parent</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>select-before-action-parent</from>
+      <to>find-action-parent-id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>find-action-parent-id</from>
+      <to>verify-action-parent-id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>verify-action-parent-id</from>
+      <to>select-before-find-executions</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>select-before-find-executions</from>
+      <to>find-executions-of-action</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>find-executions-of-action</from>
+      <to>verify-child-pipeline-execution</to>
+      <enabled>Y</enabled>
+    </hop>
+    <!-- Branch B: Mapping output rows in execution data -->
+    <hop>
+      <from>find-pipeline-execution</from>
+      <to>select-for-get-execution-data</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>select-for-get-execution-data</from>
+      <to>get-execution-data</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>get-execution-data</from>
+      <to>filter-simple-mapping</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>filter-simple-mapping</from>
+      <to>count-mapping-rows</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>count-mapping-rows</from>
+      <to>verify-mapping-rows-exist</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>params</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>name</name>
+        <type>String</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>type</name>
+        <type>String</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>ownerId</name>
+        <type>String</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <data>
+      <line>
+        <item>002-pipeline-with-mapping</item>
+        <item>Pipeline</item>
+        <item>all-transforms</item>
+      </line>
+    </data>
+    <attributes/>
+    <GUI>
+      <xloc>80</xloc>
+      <yloc>160</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>find-pipeline-execution</name>
+    <type>ExecInfo</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <location>caching-file</location>
+    <operationType>FindLastExecution</operationType>
+    <idFieldName/>
+    <parentIdFieldName/>
+    <typeFieldName>type</typeFieldName>
+    <nameFieldName>name</nameFieldName>
+    <includeChildrenFieldName/>
+    <limitFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>240</xloc>
+      <yloc>160</yloc>
+    </GUI>
+  </transform>
+  <!-- Branch A Transforms -->
+  <transform>
+    <name>verify-pipeline-has-parent</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+if (executionId == null || parentId == null) {
+  throw "Pipeline execution ID or parent action ID is null!";
+}
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields/>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>find-pipeline-parent-id</name>
+    <type>ExecInfo</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <location>caching-file</location>
+    <operationType>FindParentId</operationType>
+    <idFieldName>executionId</idFieldName>
+    <parentIdFieldName/>
+    <typeFieldName/>
+    <nameFieldName/>
+    <includeChildrenFieldName/>
+    <limitFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>560</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>verify-pipeline-parent-id</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+if (id == null || !id.equals(parentId)) {
+  throw "findParentId(pipelineId) [" + id + "] did not match parent action ID 
[" + parentId + "]!";
+}
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields/>
+    <attributes/>
+    <GUI>
+      <xloc>720</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>select-before-action-parent</name>
+    <type>SelectValues</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <select_unspecified>Y</select_unspecified>
+      <meta>
+        <name>id</name>
+        <rename>resolvedPipelineParentId</rename>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <conversion_mask/>
+        <date_format_lenient>false</date_format_lenient>
+        <date_format_locale/>
+        <date_format_timezone/>
+        <lenient_string_to_number>false</lenient_string_to_number>
+        <encoding/>
+        <decimal_symbol/>
+        <grouping_symbol/>
+        <currency_symbol/>
+        <storage_type/>
+      </meta>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>880</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>find-action-parent-id</name>
+    <type>ExecInfo</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <location>caching-file</location>
+    <operationType>FindParentId</operationType>
+    <idFieldName>parentId</idFieldName>
+    <parentIdFieldName/>
+    <typeFieldName/>
+    <nameFieldName/>
+    <includeChildrenFieldName/>
+    <limitFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>1040</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>verify-action-parent-id</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+if (id == null || id.equals("") || id.equals(parentId)) {
+  throw "findParentId(actionId) [" + id + "] is null or equal to actionId 
itself!";
+}
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields/>
+    <attributes/>
+    <GUI>
+      <xloc>1200</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>select-before-find-executions</name>
+    <type>SelectValues</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>parentId</name>
+      </field>
+      <field>
+        <name>executionId</name>
+        <rename>origPipelineId</rename>
+      </field>
+      <select_unspecified>N</select_unspecified>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>1360</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>find-executions-of-action</name>
+    <type>ExecInfo</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <location>caching-file</location>
+    <operationType>FindExecutions</operationType>
+    <idFieldName/>
+    <parentIdFieldName>parentId</parentIdFieldName>
+    <typeFieldName/>
+    <nameFieldName/>
+    <includeChildrenFieldName/>
+    <limitFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>1520</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>verify-child-pipeline-execution</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+if (executionId == null || !executionId.equals(origPipelineId)) {
+  throw "findExecutions(actionId) did not return expected pipeline execution 
ID [" + origPipelineId + "]! Got: [" + executionId + "]";
+}
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields/>
+    <attributes/>
+    <GUI>
+      <xloc>1680</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+
+  <!-- Branch B Transforms -->
+  <transform>
+    <name>select-for-get-execution-data</name>
+    <type>SelectValues</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>executionId</name>
+        <rename>pipelineExecId</rename>
+      </field>
+      <field>
+        <name>ownerId</name>
+      </field>
+      <select_unspecified>N</select_unspecified>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>get-execution-data</name>
+    <type>ExecInfo</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <location>caching-file</location>
+    <operationType>GetExecutionData</operationType>
+    <idFieldName>ownerId</idFieldName>
+    <parentIdFieldName>pipelineExecId</parentIdFieldName>
+    <typeFieldName/>
+    <nameFieldName/>
+    <includeChildrenFieldName/>
+    <limitFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>560</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>filter-simple-mapping</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <operator>-</operator>
+        <leftvalue>name</leftvalue>
+        <function>=</function>
+        <rightvalue/>
+        <value>
+          <name>constant</name>
+          <type>String</type>
+          <text>simple-mapping</text>
+          <length>-1</length>
+          <precision>-1</precision>
+          <isnull>N</isnull>
+          <mask/>
+        </value>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>720</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>count-mapping-rows</name>
+    <type>GroupBy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <all_rows>N</all_rows>
+    <ignore_aggregate>N</ignore_aggregate>
+    <fields>
+      <field>
+        <aggregate>mappingRowCount</aggregate>
+        <subject>name</subject>
+        <type>COUNT_ANY</type>
+      </field>
+    </fields>
+    <give_back_row>Y</give_back_row>
+    <directory>${java.io.tmpdir}</directory>
+    <prefix>grp</prefix>
+    <add_linenr>N</add_linenr>
+    <attributes/>
+    <GUI>
+      <xloc>880</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>verify-mapping-rows-exist</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+if (mappingRowCount == null || mappingRowCount &lt; 10) {
+  throw "Mapping transform output rows missing or insufficient in execution 
data! Expected &gt;= 10, got: " + mappingRowCount;
+}
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields/>
+    <attributes/>
+    <GUI>
+      <xloc>1040</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf 
b/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf
new file mode 100644
index 0000000000..75c945f736
--- /dev/null
+++ b/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-002-workflow-pipeline-mapping</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <created_user>-</created_user>
+  <modified_user>-</modified_user>
+  <created_date>2026/10/10 14:00:00.000</created_date>
+  <modified_date>2026/10/10 14:00:00.000</modified_date>
+  <parameters/>
+  <actions>
+    <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <xloc>80</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <foldername>${java.io.tmpdir}/caching-file-location/</foldername>
+      <fail_of_folder_exists>N</fail_of_folder_exists>
+      <name>create /tmp/caching-file-location/</name>
+      <description/>
+      <type>CREATE_FOLDER</type>
+      <attributes/>
+      <xloc>272</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <filename>${PROJECT_HOME}/002-sub-workflow.hwf</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <create_parent_folder>N</create_parent_folder>
+      <set_logfile>N</set_logfile>
+      <set_append_logfile>N</set_append_logfile>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <wait_until_finished>Y</wait_until_finished>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <run_configuration>local-caching-file</run_configuration>
+      <name>run-002-sub-workflow</name>
+      <description/>
+      <type>WORKFLOW</type>
+      <attributes/>
+      <xloc>512</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <filename>${PROJECT_HOME}/002-validation.hpl</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <create_parent_folder>N</create_parent_folder>
+      <set_logfile>N</set_logfile>
+      <set_append_logfile>N</set_append_logfile>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <wait_until_finished>Y</wait_until_finished>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <run_configuration>local</run_configuration>
+      <name>run-002-validation</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <xloc>784</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>create /tmp/caching-file-location/</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>create /tmp/caching-file-location/</from>
+      <to>run-002-sub-workflow</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>run-002-sub-workflow</from>
+      <to>run-002-validation</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+  </hops>
+  <notepads/>
+  <attributes/>
+</workflow>
diff --git 
a/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
 
b/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
index 5135e42601..12dc7a1649 100644
--- 
a/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
+++ 
b/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
@@ -719,6 +719,89 @@ public class CachingDatabaseExecutionInfoLocation extends 
BaseCachingExecutionIn
     }
   }
 
+  @Override
+  protected CacheEntry loadCacheEntryWithChild(String childId) throws 
HopException {
+    if (StringUtils.isEmpty(childId)) {
+      return null;
+    }
+    try {
+      String columns = databaseMeta.quoteField(COL_JSON);
+      if (stateJsonColumnAvailable) {
+        columns += ", " + databaseMeta.quoteField(COL_STATE_JSON);
+      }
+      StringBuilder sql = new StringBuilder();
+      sql.append("SELECT ")
+          .append(columns)
+          .append(" FROM ")
+          .append(getQuotedSchemaTable())
+          .append(" WHERE (")
+          .append(databaseMeta.quoteField(COL_JSON))
+          .append(" LIKE ?");
+      if (stateJsonColumnAvailable) {
+        sql.append(" OR 
").append(databaseMeta.quoteField(COL_STATE_JSON)).append(" LIKE ?");
+      }
+      sql.append(") AND ")
+          .append(databaseMeta.quoteField(COL_ID))
+          .append(" != ?")
+          .append(" ORDER BY ")
+          .append(databaseMeta.quoteField(COL_EXECUTION_START_DATE))
+          .append(" DESC")
+          .append(databaseMeta.getLimitClause(20));
+
+      IRowMeta paramMeta = new RowMeta();
+      paramMeta.addValueMeta(new ValueMetaString("p1", 100, -1));
+      List<Object> params = new ArrayList<>();
+      params.add("%" + childId + "%");
+      if (stateJsonColumnAvailable) {
+        paramMeta.addValueMeta(new ValueMetaString("p2", 100, -1));
+        params.add("%" + childId + "%");
+      }
+      paramMeta.addValueMeta(new ValueMetaString("p_not_id", 100, -1));
+      params.add(childId);
+
+      return callWithDatabase(
+          () -> {
+            ResultSet rs = database.openQuery(sql.toString(), paramMeta, 
params.toArray());
+            try {
+              Object[] row = database.getRow(rs);
+              while (row != null) {
+                if (row[0] != null) {
+                  try {
+                    CacheEntry entry = 
JSON_MAPPER.readValue(row[0].toString(), CacheEntry.class);
+                    if (entry != null && !entry.getId().equals(childId)) {
+                      if (stateJsonColumnAvailable
+                          && row.length > 1
+                          && row[1] != null
+                          && StringUtils.isNotEmpty(row[1].toString())) {
+                        applyStateOverlay(
+                            entry, JSON_MAPPER.readValue(row[1].toString(), 
CacheEntry.class));
+                      }
+                      if (entry.peekChildExecution(childId) != null
+                          || (entry.getChildIds() != null
+                              && entry.getChildIds().contains(childId))) {
+                        cache.put(entry.getId(), entry);
+                        enforceMaxCacheSize();
+                        return entry;
+                      }
+                    }
+                  } catch (Exception e) {
+                    // Ignore corrupted candidate and continue checking other 
rows
+                  }
+                }
+                row = database.getRow(rs);
+              }
+              return null;
+            } finally {
+              database.closeQuery(rs);
+            }
+          });
+    } catch (Exception e) {
+      LogChannel.GENERAL.logError(
+          "Error finding cache entry containing child " + childId + " from 
database", e);
+      return null;
+    }
+  }
+
   @Override
   public void deleteCacheEntry(CacheEntry cacheEntry) throws HopException {
     if (cacheEntry == null || StringUtils.isEmpty(cacheEntry.getId())) {
diff --git 
a/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
 
b/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
index bcbfd0732c..34b4ee794f 100644
--- 
a/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
+++ 
b/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
@@ -752,6 +752,72 @@ class CachingDatabaseExecutionInfoLocationTest {
     }
   }
 
+  @Test
+  void testUncachedParentChildLinkResolution() throws Exception {
+    CachingDatabaseExecutionInfoLocation location = new 
CachingDatabaseExecutionInfoLocation();
+    location.setConnectionName("h2-exec");
+    
location.setTableName(CachingDatabaseExecutionInfoLocation.DEFAULT_TABLE_NAME);
+    location.setPersistenceDelay("0");
+    location.setMaxCacheAge("86400000");
+    location.setDatabaseMeta(databaseMeta);
+    location.initialize(variables, metadataProvider);
+    try {
+      String workflowId = UUID.randomUUID().toString();
+      String actionId = UUID.randomUUID().toString();
+      String pipelineId = UUID.randomUUID().toString();
+
+      Execution wf = new Execution();
+      wf.setId(workflowId);
+      wf.setName("Workflow");
+      wf.setExecutionType(ExecutionType.Workflow);
+      wf.setRegistrationDate(new Date());
+
+      Execution action = new Execution();
+      action.setId(actionId);
+      action.setName("ActionPipeline");
+      action.setParentId(workflowId);
+      action.setExecutionType(ExecutionType.Action);
+      action.setRegistrationDate(new Date());
+
+      Execution pipe = new Execution();
+      pipe.setId(pipelineId);
+      pipe.setName("Pipeline");
+      pipe.setParentId(actionId);
+      pipe.setExecutionType(ExecutionType.Pipeline);
+      pipe.setRegistrationDate(new Date());
+
+      location.registerExecution(wf);
+      location.registerExecution(action);
+      location.registerExecution(pipe);
+
+      // Persist to database and clear in-memory cache so nothing is in memory
+      for (CacheEntry entry : location.getCache().values()) {
+        location.persistCacheEntry(entry);
+      }
+      location.clearCaches();
+
+      assertEquals(actionId, location.findParentId(pipelineId));
+      assertEquals(workflowId, location.findParentId(actionId));
+      assertNull(location.findParentId(workflowId));
+
+      Execution retrievedAction = location.getExecution(actionId);
+      assertNotNull(retrievedAction);
+      assertEquals(actionId, retrievedAction.getId());
+      assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+      assertNull(location.getExecution("non-existent-id"));
+
+      List<Execution> actionChildren = location.findExecutions(actionId);
+      assertEquals(1, actionChildren.size());
+      assertEquals(pipelineId, actionChildren.get(0).getId());
+
+      List<Execution> wfChildren = location.findExecutions(workflowId);
+      assertTrue(wfChildren.stream().anyMatch(e -> 
e.getId().equals(actionId)));
+    } finally {
+      location.close();
+    }
+  }
+
   private static final class FailingFlushLocation extends 
CachingDatabaseExecutionInfoLocation {
     private boolean failPersist;
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
index 124a3618f2..f5f0a48885 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
@@ -954,10 +954,6 @@ public class WorkflowExecutionViewer extends 
BaseExecutionViewer
       toolTip = 
"i18n::WorkflowExecutionViewer.ToolbarElement.DrillDown.Tooltip",
       image = "ui/images/down.svg")
   public void drillDown() {
-    if (selectedExecutionData == null) {
-      return;
-    }
-
     ExecutionInfoLocation location = 
perspective.getLocationMap().get(locationName);
     if (location == null) {
       return;
@@ -967,7 +963,26 @@ public class WorkflowExecutionViewer extends 
BaseExecutionViewer
     // We need to look up a pipeline or workflow execution where the parent is 
the ID of the action
     //
     try {
-      String id = selectedExecutionData.getOwnerId();
+      String id = null;
+      if (selectedExecutionData != null) {
+        id = selectedExecutionData.getOwnerId();
+      } else if (selectedAction != null) {
+        List<String> childIds = iLocation.findChildIds(ExecutionType.Workflow, 
execution.getId());
+        if (childIds != null) {
+          for (String childId : childIds) {
+            Execution childExec = iLocation.getExecution(childId);
+            if (childExec != null && 
selectedAction.getName().equals(childExec.getName())) {
+              id = childId;
+              break;
+            }
+          }
+        }
+      }
+
+      if (id == null) {
+        return;
+      }
+
       List<Execution> childExecutions = iLocation.findExecutions(id);
       if (childExecutions.isEmpty()) {
         return;

Reply via email to