Skip to content

Commit 677f5cb

Browse files
author
Waleed Latif
committed
fix(workflows): refuse a disabled stop block, or one reached only through one
The executor omits disabled blocks from its graph, so a disabled stop target, or one whose only path runs through a disabled block, is never reached and the run would finish everything after the entry.
1 parent cde4976 commit 677f5cb

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎apps/sim/lib/workflows/application/execute-manual-workflow.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,37 @@ describe('manual workflow execution application operations', () => {
377377
expect(mocks.executeService).not.toHaveBeenCalled()
378378
})
379379

380+
it('rejects a disabled stop block, or one reached only through a disabled block', async () => {
381+
mockLoadManualState.mockResolvedValue({
382+
blocks: {
383+
'trigger-1': {},
384+
'agent-1': { enabled: false },
385+
'agent-2': {},
386+
},
387+
edges: [
388+
{ source: 'trigger-1', target: 'agent-1' },
389+
{ source: 'agent-1', target: 'agent-2' },
390+
],
391+
})
392+
393+
await expect(
394+
executeManualWorkflowOperation.execute({
395+
principal,
396+
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'agent-1' },
397+
})
398+
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('is disabled') })
399+
await expect(
400+
executeManualWorkflowOperation.execute({
401+
principal,
402+
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'agent-2' },
403+
})
404+
).rejects.toMatchObject({
405+
code: 'validation',
406+
message: expect.stringContaining('not reachable'),
407+
})
408+
expect(mocks.executeService).not.toHaveBeenCalled()
409+
})
410+
380411
it('rejects a stop block named by an inherited object key', async () => {
381412
await expect(
382413
executeManualWorkflowOperation.execute({

‎apps/sim/lib/workflows/application/execute-manual-workflow.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async function loadManualState(workflowId: string) {
5050

5151
type ManualWorkflowState = Awaited<ReturnType<typeof loadManualState>>
5252

53+
/** Blocks a run entering at `entryBlockId` can reach; the executor skips disabled blocks. */
5354
function reachableFrom(state: ManualWorkflowState, entryBlockId: string): Set<string> {
5455
const targetsBySource = new Map<string, string[]>()
5556
for (const edge of state.edges) {
@@ -61,7 +62,7 @@ function reachableFrom(state: ManualWorkflowState, entryBlockId: string): Set<st
6162
const queue = [entryBlockId]
6263
for (let next = queue.pop(); next !== undefined; next = queue.pop()) {
6364
for (const target of targetsBySource.get(next) ?? []) {
64-
if (reached.has(target)) continue
65+
if (reached.has(target) || state.blocks[target]?.enabled === false) continue
6566
reached.add(target)
6667
queue.push(target)
6768
}
@@ -72,10 +73,10 @@ function reachableFrom(state: ManualWorkflowState, entryBlockId: string): Set<st
7273
/**
7374
* The engine stops only when it completes a node whose id equals the target, so
7475
* a target the run cannot reach would silently run everything after the entry:
75-
* an unknown id, a block upstream of the entry, or a block inside a loop or
76-
* parallel (which would stop after its first iteration or never). All are
77-
* refused, matching the editor, which offers "Run until block" only outside
78-
* subflows.
76+
* an unknown or disabled block, a block upstream of the entry or only behind a
77+
* disabled one, or a block inside a loop or parallel (which would stop after its
78+
* first iteration or never). All are refused, matching the editor, which offers
79+
* "Run until block" only outside subflows.
7980
*/
8081
function assertStopAfterBlock(
8182
state: ManualWorkflowState,
@@ -90,6 +91,12 @@ function assertStopAfterBlock(
9091
`run.stopAfterBlockId "${blockId}" is not a block in the current saved workflow.`
9192
)
9293
}
94+
if (block.enabled === false) {
95+
throw new OrchestrationError(
96+
'validation',
97+
`run.stopAfterBlockId "${blockId}" is disabled, so the run never executes it.`
98+
)
99+
}
93100
if (block.data?.parentId) {
94101
throw new OrchestrationError(
95102
'validation',

0 commit comments

Comments
 (0)