Skip to content

Commit a9f271c

Browse files
committed
I think it works??
1 parent 0ead5aa commit a9f271c

File tree

8 files changed

+86
-33
lines changed

8 files changed

+86
-33
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,17 @@ export const ActionBar = memo(
114114
const snapshot = activeWorkflowId ? getLastExecutionSnapshot(activeWorkflowId) : null
115115
const incomingEdges = edges.filter((edge) => edge.target === blockId)
116116
const isTriggerBlock = incomingEdges.length === 0
117+
118+
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
119+
const isSourceSatisfied = (sourceId: string) => {
120+
if (snapshot?.executedBlocks.includes(sourceId)) return true
121+
// Check if source is a trigger (has no incoming edges itself)
122+
const sourceIncomingEdges = edges.filter((edge) => edge.target === sourceId)
123+
return sourceIncomingEdges.length === 0
124+
}
125+
117126
const dependenciesSatisfied =
118-
isTriggerBlock ||
119-
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
127+
isTriggerBlock || incomingEdges.every((edge) => isSourceSatisfied(edge.source))
120128
const canRunFromBlock =
121129
dependenciesSatisfied && !isNoteBlock && !isInsideSubflow && !isExecuting
122130

@@ -149,7 +157,7 @@ export const ActionBar = memo(
149157
'dark:border-transparent dark:bg-[var(--surface-4)]'
150158
)}
151159
>
152-
{!isNoteBlock && (
160+
{!isNoteBlock && !isInsideSubflow && (
153161
<Tooltip.Root>
154162
<Tooltip.Trigger asChild>
155163
<Button
@@ -170,7 +178,6 @@ export const ActionBar = memo(
170178
{(() => {
171179
if (disabled) return getTooltipMessage('Run from block')
172180
if (isExecuting) return 'Execution in progress'
173-
if (isInsideSubflow) return 'Cannot run from inside subflow'
174181
if (!dependenciesSatisfied) return 'Run upstream blocks first'
175182
return 'Run from block'
176183
})()}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export interface BlockMenuProps {
4848
canRunFromBlock?: boolean
4949
disableEdit?: boolean
5050
isExecuting?: boolean
51+
/** Whether the selected block is a trigger (has no incoming edges) */
52+
isPositionalTrigger?: boolean
5153
}
5254

5355
/**
@@ -77,6 +79,7 @@ export function BlockMenu({
7779
canRunFromBlock = false,
7880
disableEdit = false,
7981
isExecuting = false,
82+
isPositionalTrigger = false,
8083
}: BlockMenuProps) {
8184
const isSingleBlock = selectedBlocks.length === 1
8285

@@ -87,7 +90,9 @@ export function BlockMenu({
8790
(b) =>
8891
TriggerUtils.requiresSingleInstance(b.type) || TriggerUtils.isSingleInstanceBlockType(b.type)
8992
)
90-
const hasTriggerBlock = selectedBlocks.some((b) => TriggerUtils.isTriggerBlock(b))
93+
// A block is a trigger if it's explicitly a trigger type OR has no incoming edges (positional trigger)
94+
const hasTriggerBlock =
95+
selectedBlocks.some((b) => TriggerUtils.isTriggerBlock(b)) || isPositionalTrigger
9196
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
9297
const isSubflow =
9398
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,11 @@ export function useWorkflowExecution() {
10721072
logs: accumulatedBlockLogs,
10731073
}
10741074

1075+
// Add trigger block to executed blocks so downstream blocks can use run-from-block
1076+
if (data.success && startBlockId) {
1077+
executedBlockIds.add(startBlockId)
1078+
}
1079+
10751080
if (data.success && activeWorkflowId) {
10761081
if (stopAfterBlockId) {
10771082
const existingSnapshot = getLastExecutionSnapshot(activeWorkflowId)
@@ -1443,14 +1448,25 @@ export function useWorkflowExecution() {
14431448
const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
14441449
const isTriggerBlock = incomingEdges.length === 0
14451450

1446-
if (!snapshot && !isTriggerBlock) {
1451+
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
1452+
const isSourceSatisfied = (sourceId: string) => {
1453+
if (snapshot?.executedBlocks.includes(sourceId)) return true
1454+
// Check if source is a trigger (has no incoming edges itself)
1455+
const sourceIncomingEdges = workflowEdges.filter((edge) => edge.target === sourceId)
1456+
return sourceIncomingEdges.length === 0
1457+
}
1458+
1459+
if (
1460+
!snapshot &&
1461+
!isTriggerBlock &&
1462+
!incomingEdges.every((edge) => isSourceSatisfied(edge.source))
1463+
) {
14471464
logger.error('No execution snapshot available for run-from-block', { workflowId, blockId })
14481465
return
14491466
}
14501467

14511468
const dependenciesSatisfied =
1452-
isTriggerBlock ||
1453-
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
1469+
isTriggerBlock || incomingEdges.every((edge) => isSourceSatisfied(edge.source))
14541470

14551471
if (!dependenciesSatisfied) {
14561472
logger.error('Upstream dependencies not satisfied for run-from-block', {
@@ -1637,6 +1653,9 @@ export function useWorkflowExecution() {
16371653

16381654
onExecutionCompleted: (data) => {
16391655
if (data.success) {
1656+
// Add the start block (trigger) to executed blocks
1657+
executedBlockIds.add(blockId)
1658+
16401659
const mergedBlockStates: Record<string, BlockState> = {
16411660
...effectiveSnapshot.blockStates,
16421661
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,23 +1128,17 @@ const WorkflowContent = React.memo(() => {
11281128
const snapshot = getLastExecutionSnapshot(workflowIdParam)
11291129
const incomingEdges = edges.filter((edge) => edge.target === block.id)
11301130
const isTriggerBlock = incomingEdges.length === 0
1131-
const isSubflow = block.type === 'loop' || block.type === 'parallel'
1132-
1133-
// For subflows, check if the sentinel-end was executed (meaning the subflow completed at least once)
1134-
// Sentinel IDs follow the pattern: loop-{id}-sentinel-end or parallel-{id}-sentinel-end
1135-
const subflowWasExecuted =
1136-
isSubflow &&
1137-
snapshot &&
1138-
snapshot.executedBlocks.some(
1139-
(executedId) =>
1140-
executedId === `loop-${block.id}-sentinel-end` ||
1141-
executedId === `parallel-${block.id}-sentinel-end`
1142-
)
1131+
1132+
// Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
1133+
const isSourceSatisfied = (sourceId: string) => {
1134+
if (snapshot?.executedBlocks.includes(sourceId)) return true
1135+
// Check if source is a trigger (has no incoming edges itself)
1136+
const sourceIncomingEdges = edges.filter((edge) => edge.target === sourceId)
1137+
return sourceIncomingEdges.length === 0
1138+
}
11431139

11441140
const dependenciesSatisfied =
1145-
isTriggerBlock ||
1146-
subflowWasExecuted ||
1147-
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
1141+
isTriggerBlock || incomingEdges.every((edge) => isSourceSatisfied(edge.source))
11481142
const isNoteBlock = block.type === 'note'
11491143
const isInsideSubflow =
11501144
block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel')
@@ -3482,6 +3476,10 @@ const WorkflowContent = React.memo(() => {
34823476
canRunFromBlock={runFromBlockState.canRun}
34833477
disableEdit={!effectivePermissions.canEdit}
34843478
isExecuting={isExecuting}
3479+
isPositionalTrigger={
3480+
contextMenuBlocks.length === 1 &&
3481+
edges.filter((e) => e.target === contextMenuBlocks[0]?.id).length === 0
3482+
}
34853483
/>
34863484

34873485
<CanvasMenu

apps/sim/executor/dag/builder.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,25 @@ export interface DAG {
3333
parallelConfigs: Map<string, SerializedParallel>
3434
}
3535

36+
export interface DAGBuildOptions {
37+
/** Trigger block ID to start path construction from */
38+
triggerBlockId?: string
39+
/** Saved incoming edges from snapshot for resumption */
40+
savedIncomingEdges?: Record<string, string[]>
41+
/** Include all enabled blocks instead of only those reachable from trigger */
42+
includeAllBlocks?: boolean
43+
}
44+
3645
export class DAGBuilder {
3746
private pathConstructor = new PathConstructor()
3847
private loopConstructor = new LoopConstructor()
3948
private parallelConstructor = new ParallelConstructor()
4049
private nodeConstructor = new NodeConstructor()
4150
private edgeConstructor = new EdgeConstructor()
4251

43-
build(
44-
workflow: SerializedWorkflow,
45-
triggerBlockId?: string,
46-
savedIncomingEdges?: Record<string, string[]>
47-
): DAG {
52+
build(workflow: SerializedWorkflow, options: DAGBuildOptions = {}): DAG {
53+
const { triggerBlockId, savedIncomingEdges, includeAllBlocks } = options
54+
4855
const dag: DAG = {
4956
nodes: new Map(),
5057
loopConfigs: new Map(),
@@ -53,7 +60,7 @@ export class DAGBuilder {
5360

5461
this.initializeConfigs(workflow, dag)
5562

56-
const reachableBlocks = this.pathConstructor.execute(workflow, triggerBlockId)
63+
const reachableBlocks = this.pathConstructor.execute(workflow, triggerBlockId, includeAllBlocks)
5764

5865
this.loopConstructor.execute(dag, reachableBlocks)
5966
this.parallelConstructor.execute(dag, reachableBlocks)

apps/sim/executor/dag/construction/paths.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
66
const logger = createLogger('PathConstructor')
77

88
export class PathConstructor {
9-
execute(workflow: SerializedWorkflow, triggerBlockId?: string): Set<string> {
9+
execute(
10+
workflow: SerializedWorkflow,
11+
triggerBlockId?: string,
12+
includeAllBlocks?: boolean
13+
): Set<string> {
14+
// For run-from-block mode, include all enabled blocks regardless of trigger reachability
15+
if (includeAllBlocks) {
16+
return this.getAllEnabledBlocks(workflow)
17+
}
18+
1019
const resolvedTriggerId = this.findTriggerBlock(workflow, triggerBlockId)
1120

1221
if (!resolvedTriggerId) {

apps/sim/executor/execution/executor.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ export class DAGExecutor {
6262

6363
async execute(workflowId: string, triggerBlockId?: string): Promise<ExecutionResult> {
6464
const savedIncomingEdges = this.contextExtensions.dagIncomingEdges
65-
const dag = this.dagBuilder.build(this.workflow, triggerBlockId, savedIncomingEdges)
65+
const dag = this.dagBuilder.build(this.workflow, {
66+
triggerBlockId,
67+
savedIncomingEdges,
68+
})
6669
const { context, state } = this.createExecutionContext(workflowId, triggerBlockId)
6770

6871
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state)
@@ -111,8 +114,9 @@ export class DAGExecutor {
111114
startBlockId: string,
112115
sourceSnapshot: SerializableExecutionState
113116
): Promise<ExecutionResult> {
114-
// Build full DAG to compute upstream set for snapshot filtering
115-
const dag = this.dagBuilder.build(this.workflow)
117+
// Build full DAG with all blocks to compute upstream set for snapshot filtering
118+
// includeAllBlocks is needed because the startBlockId might be a trigger not reachable from the main trigger
119+
const dag = this.dagBuilder.build(this.workflow, { includeAllBlocks: true })
116120

117121
const executedBlocks = new Set(sourceSnapshot.executedBlocks)
118122
const validation = validateRunFromBlock(startBlockId, dag, executedBlocks)

apps/sim/executor/utils/run-from-block.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ export function validateRunFromBlock(
176176
// Skip sentinel nodes - they're internal and not in executedBlocks
177177
if (sourceNode?.metadata.isSentinel) continue
178178

179+
// Skip trigger nodes - they're entry points and don't need prior execution
180+
// A trigger node has no incoming edges
181+
if (sourceNode && sourceNode.incomingEdges.size === 0) continue
182+
179183
if (!executedBlocks.has(sourceId)) {
180184
return {
181185
valid: false,

0 commit comments

Comments
 (0)