Browse Source

feat: add skip propagation to workflow execution engine

When an optional input node has no data, it and all downstream nodes
with a skipped source are silently skipped during execution. Uses
ANY merge policy — if any source is skipped, the downstream node
is also skipped. Skipped status is reset to idle after workflow
completes or errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
d1d36e8d33
  1. 48
      src/store/workflowStore.ts

48
src/store/workflowStore.ts

@ -998,6 +998,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
// Create AbortController for this execution run
const abortController = new AbortController();
const isResuming = startFromNodeId === get().pausedAtNodeId;
const skippedNodeIds = new Set<string>();
set({ isRunning: true, pausedAtNodeId: null, currentNodeIds: [], _abortController: abortController });
// Start logging session
@ -1071,6 +1072,38 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
return; // Skip this node but continue with others
}
// Check 1: Optional input node with no data → skip this node
const nodeData = node.data as Record<string, unknown>;
if (nodeData.isOptional) {
const isEmpty =
(node.type === "imageInput" && !nodeData.image) ||
(node.type === "audioInput" && !nodeData.audioFile) ||
(node.type === "prompt" && !(nodeData.prompt as string)?.trim());
if (isEmpty) {
skippedNodeIds.add(node.id);
logger.info('node.execution', 'Node skipped (optional input empty)', {
nodeId: node.id,
nodeType: node.type,
});
return;
}
}
// Check 2: Any source node is skipped → propagate skip
const incomingEdgesForSkip = edges.filter((e) => e.target === node.id);
const hasSkippedSource = incomingEdgesForSkip.some((e) => skippedNodeIds.has(e.source));
if (hasSkippedSource) {
skippedNodeIds.add(node.id);
if (nodeData.status !== undefined) {
get().updateNodeData(node.id, { status: "skipped" } as Partial<WorkflowNodeData>);
}
logger.info('node.execution', 'Node skipped (upstream source skipped)', {
nodeId: node.id,
nodeType: node.type,
});
return;
}
logger.info('node.execution', `Executing ${node.type} node`, {
nodeId: node.id,
nodeType: node.type,
@ -1214,6 +1247,14 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
logger.info('workflow.end', 'Workflow execution completed successfully');
}
// Reset skipped nodes' status back to idle
for (const skippedId of skippedNodeIds) {
const skippedNode = get().nodes.find((n) => n.id === skippedId);
if (skippedNode && (skippedNode.data as Record<string, unknown>).status !== undefined) {
get().updateNodeData(skippedId, { status: "idle" } as Partial<WorkflowNodeData>);
}
}
set({ isRunning: false, currentNodeIds: [], _abortController: null });
saveLogSession();
@ -1230,6 +1271,13 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
"error"
);
}
// Reset skipped nodes' status back to idle
for (const skippedId of skippedNodeIds) {
const skippedNode = get().nodes.find((n) => n.id === skippedId);
if (skippedNode && (skippedNode.data as Record<string, unknown>).status !== undefined) {
get().updateNodeData(skippedId, { status: "idle" } as Partial<WorkflowNodeData>);
}
}
set({ isRunning: false, currentNodeIds: [], _abortController: null });
saveLogSession();

Loading…
Cancel
Save