diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx
index dfeb6b0bd..d28f30d63 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx
@@ -91,6 +91,9 @@ export function BlockMenu({
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
const isSubflow =
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')
+ const isInsideSubflow =
+ isSingleBlock &&
+ (selectedBlocks[0]?.parentType === 'loop' || selectedBlocks[0]?.parentType === 'parallel')
const canRemoveFromSubflow = showRemoveFromSubflow && !hasTriggerBlock
@@ -212,8 +215,8 @@ export function BlockMenu({
)}
- {/* Run from/until block - only for single non-note block selection */}
- {isSingleBlock && !allNoteBlocks && (
+ {/* Run from/until block - only for single non-note block, not inside subflows */}
+ {isSingleBlock && !allNoteBlocks && !isInsideSubflow && (
<>
Run from block
- {
- if (!isExecuting) {
- onRunUntilBlock?.()
- onClose()
- }
- }}
- >
- Run until block
-
+ {/* Hide "Run until" for triggers - they're always at the start */}
+ {!hasTriggerBlock && (
+ {
+ if (!isExecuting) {
+ onRunUntilBlock?.()
+ onClose()
+ }
+ }}
+ >
+ Run until block
+
+ )}
>
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index b1a9ef2df..d27e41815 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -1128,8 +1128,22 @@ const WorkflowContent = React.memo(() => {
const snapshot = getLastExecutionSnapshot(workflowIdParam)
const incomingEdges = edges.filter((edge) => edge.target === block.id)
const isTriggerBlock = incomingEdges.length === 0
+ const isSubflow = block.type === 'loop' || block.type === 'parallel'
+
+ // For subflows, check if the sentinel-end was executed (meaning the subflow completed at least once)
+ // Sentinel IDs follow the pattern: loop-{id}-sentinel-end or parallel-{id}-sentinel-end
+ const subflowWasExecuted =
+ isSubflow &&
+ snapshot &&
+ snapshot.executedBlocks.some(
+ (executedId) =>
+ executedId === `loop-${block.id}-sentinel-end` ||
+ executedId === `parallel-${block.id}-sentinel-end`
+ )
+
const dependenciesSatisfied =
isTriggerBlock ||
+ subflowWasExecuted ||
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
const isNoteBlock = block.type === 'note'
const isInsideSubflow =
diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts
index 0c6b6a1e5..94c7e37a9 100644
--- a/apps/sim/executor/execution/engine.ts
+++ b/apps/sim/executor/execution/engine.ts
@@ -397,9 +397,14 @@ export class ExecutionEngine {
}
if (this.context.stopAfterBlockId === nodeId) {
- logger.info('Stopping execution after target block', { nodeId })
- this.stoppedEarlyFlag = true
- return
+ // For loop/parallel sentinels, only stop if the subflow has fully exited (all iterations done)
+ // shouldContinue: true means more iterations, shouldExit: true means loop is done
+ const shouldContinueLoop = output.shouldContinue === true
+ if (!shouldContinueLoop) {
+ logger.info('Stopping execution after target block', { nodeId })
+ this.stoppedEarlyFlag = true
+ return
+ }
}
const readyNodes = this.edgeManager.processOutgoingEdges(node, output, false)
diff --git a/apps/sim/executor/execution/executor.ts b/apps/sim/executor/execution/executor.ts
index 572ea483b..8cd699d9b 100644
--- a/apps/sim/executor/execution/executor.ts
+++ b/apps/sim/executor/execution/executor.ts
@@ -26,6 +26,10 @@ import {
buildStartBlockOutput,
resolveExecutorStartBlock,
} from '@/executor/utils/start-block'
+import {
+ extractLoopIdFromSentinel,
+ extractParallelIdFromSentinel,
+} from '@/executor/utils/subflow-utils'
import { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedWorkflow } from '@/serializer/types'
@@ -119,19 +123,50 @@ export class DAGExecutor {
const { dirtySet, upstreamSet } = computeExecutionSets(dag, startBlockId)
const effectiveStartBlockId = resolveContainerToSentinelStart(startBlockId, dag) ?? startBlockId
+ // Extract container IDs from sentinel IDs in upstream set
+ const upstreamContainerIds = new Set()
+ for (const nodeId of upstreamSet) {
+ const loopId = extractLoopIdFromSentinel(nodeId)
+ if (loopId) upstreamContainerIds.add(loopId)
+ const parallelId = extractParallelIdFromSentinel(nodeId)
+ if (parallelId) upstreamContainerIds.add(parallelId)
+ }
+
// Filter snapshot to only include upstream blocks - prevents references to non-upstream blocks
const filteredBlockStates: Record = {}
for (const [blockId, state] of Object.entries(sourceSnapshot.blockStates)) {
- if (upstreamSet.has(blockId)) {
+ if (upstreamSet.has(blockId) || upstreamContainerIds.has(blockId)) {
filteredBlockStates[blockId] = state
}
}
- const filteredExecutedBlocks = sourceSnapshot.executedBlocks.filter((id) => upstreamSet.has(id))
+ const filteredExecutedBlocks = sourceSnapshot.executedBlocks.filter(
+ (id) => upstreamSet.has(id) || upstreamContainerIds.has(id)
+ )
+
+ // Filter loop/parallel executions to only include upstream containers
+ const filteredLoopExecutions: Record = {}
+ if (sourceSnapshot.loopExecutions) {
+ for (const [loopId, execution] of Object.entries(sourceSnapshot.loopExecutions)) {
+ if (upstreamContainerIds.has(loopId)) {
+ filteredLoopExecutions[loopId] = execution
+ }
+ }
+ }
+ const filteredParallelExecutions: Record = {}
+ if (sourceSnapshot.parallelExecutions) {
+ for (const [parallelId, execution] of Object.entries(sourceSnapshot.parallelExecutions)) {
+ if (upstreamContainerIds.has(parallelId)) {
+ filteredParallelExecutions[parallelId] = execution
+ }
+ }
+ }
const filteredSnapshot: SerializableExecutionState = {
...sourceSnapshot,
blockStates: filteredBlockStates,
executedBlocks: filteredExecutedBlocks,
+ loopExecutions: filteredLoopExecutions,
+ parallelExecutions: filteredParallelExecutions,
}
logger.info('Executing from block', {