Skip to content

Commit 1bec59c

Browse files
j15zVikhyath Mondreti
authored andcommitted
feat(copilot): execute workflow run cancellations
1 parent 1404e65 commit 1bec59c

9 files changed

Lines changed: 187 additions & 2 deletions

File tree

‎apps/sim/lib/copilot/generated/tool-catalog-v1.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface ToolCatalogEntry {
3434
| 'browser_type'
3535
| 'browser_wait_for'
3636
| 'call_integration_tool'
37+
| 'cancel_workflow_run'
3738
| 'connect_slack_bot'
3839
| 'cp'
3940
| 'create_empty_file'
@@ -163,6 +164,7 @@ export interface ToolCatalogEntry {
163164
| 'browser_type'
164165
| 'browser_wait_for'
165166
| 'call_integration_tool'
167+
| 'cancel_workflow_run'
166168
| 'connect_slack_bot'
167169
| 'cp'
168170
| 'create_empty_file'
@@ -1614,6 +1616,31 @@ export const CallIntegrationTool: ToolCatalogEntry = {
16141616
requiresApproval: true,
16151617
}
16161618

1619+
export const CancelWorkflowRun: ToolCatalogEntry = {
1620+
id: 'cancel_workflow_run',
1621+
name: 'cancel_workflow_run',
1622+
route: 'sim',
1623+
mode: 'async',
1624+
parameters: {
1625+
type: 'object',
1626+
properties: {
1627+
executionId: {
1628+
type: 'string',
1629+
description:
1630+
'Required workflow execution ID returned by run_workflow with async:true or found with query_logs. This identifies a workflow run, not an agent invocation or chat request.',
1631+
},
1632+
workflowId: {
1633+
type: 'string',
1634+
description:
1635+
'Workflow ID that owns the execution. Optional; omit it to target the current workflow. Pass it when cancelling a run from another workflow.',
1636+
},
1637+
},
1638+
required: ['executionId'],
1639+
},
1640+
requiredPermission: 'write',
1641+
requiresApproval: true,
1642+
}
1643+
16171644
export const ConnectSlackBot: ToolCatalogEntry = {
16181645
id: 'connect_slack_bot',
16191646
name: 'connect_slack_bot',
@@ -4426,7 +4453,11 @@ export const Run: ToolCatalogEntry = {
44264453
description: 'Pre-gathered context: workflow state, block IDs, input requirements.',
44274454
type: 'string',
44284455
},
4429-
request: { description: 'What to run or what logs to check.', type: 'string' },
4456+
request: {
4457+
description:
4458+
'What to run or cancel, or what logs to check. Include a known workflow executionId when cancelling.',
4459+
type: 'string',
4460+
},
44304461
},
44314462
required: ['request'],
44324463
type: 'object',
@@ -7015,6 +7046,7 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
70157046
[BrowserType.id]: BrowserType,
70167047
[BrowserWaitFor.id]: BrowserWaitFor,
70177048
[CallIntegrationTool.id]: CallIntegrationTool,
7049+
[CancelWorkflowRun.id]: CancelWorkflowRun,
70187050
[ConnectSlackBot.id]: ConnectSlackBot,
70197051
[Cp.id]: Cp,
70207052
[CreateEmptyFile.id]: CreateEmptyFile,

‎apps/sim/lib/copilot/generated/tool-schemas-v1.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,25 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
15601560
},
15611561
resultSchema: undefined,
15621562
},
1563+
cancel_workflow_run: {
1564+
parameters: {
1565+
type: 'object',
1566+
properties: {
1567+
executionId: {
1568+
type: 'string',
1569+
description:
1570+
'Required workflow execution ID returned by run_workflow with async:true or found with query_logs. This identifies a workflow run, not an agent invocation or chat request.',
1571+
},
1572+
workflowId: {
1573+
type: 'string',
1574+
description:
1575+
'Workflow ID that owns the execution. Optional; omit it to target the current workflow. Pass it when cancelling a run from another workflow.',
1576+
},
1577+
},
1578+
required: ['executionId'],
1579+
},
1580+
resultSchema: undefined,
1581+
},
15631582
connect_slack_bot: {
15641583
parameters: {
15651584
type: 'object',
@@ -4328,7 +4347,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
43284347
type: 'string',
43294348
},
43304349
request: {
4331-
description: 'What to run or what logs to check.',
4350+
description:
4351+
'What to run or cancel, or what logs to check. Include a known workflow executionId when cancelling.',
43324352
type: 'string',
43334353
},
43344354
},

‎apps/sim/lib/copilot/tool-executor/register-handlers.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import {
3+
CancelWorkflowRun,
34
ConnectSlackBot,
45
Cp as CpTool,
56
CreateWorkflow,
@@ -92,6 +93,7 @@ import {
9293
executeVfsRm,
9394
} from '../tools/handlers/vfs-mutate'
9495
import {
96+
executeCancelWorkflowRun,
9597
executeCreateWorkflow,
9698
executeGenerateApiKey,
9799
executeMoveWorkflow,
@@ -146,6 +148,7 @@ function buildHandlerMap(): Record<string, ToolHandler> {
146148
rename_workflow: h(executeRenameWorkflow),
147149
move_workflow: h(executeMoveWorkflow),
148150
[RunWorkflow.id]: h(executeRunWorkflow),
151+
[CancelWorkflowRun.id]: h(executeCancelWorkflowRun),
149152
[RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock),
150153
[RunFromBlock.id]: h(executeRunFromBlock),
151154
[RunBlock.id]: h(executeRunBlock),
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { describe, expect, it } from 'vitest'
6+
import { hasHandler } from '@/lib/copilot/tool-executor/executor'
7+
import { ensureHandlersRegistered } from '@/lib/copilot/tool-executor/register-handlers'
8+
import {
9+
getToolEntry,
10+
isSimExecuted,
11+
toolRequiresApproval,
12+
} from '@/lib/copilot/tool-executor/router'
13+
14+
describe('workflow-run cancellation tool routing', () => {
15+
it('routes cancellation through Sim with write permission and explicit approval', () => {
16+
expect(getToolEntry('cancel_workflow_run')).toMatchObject({
17+
requiredPermission: 'write',
18+
route: 'sim',
19+
})
20+
expect(isSimExecuted('cancel_workflow_run')).toBe(true)
21+
expect(toolRequiresApproval('cancel_workflow_run')).toBe(true)
22+
})
23+
24+
it('registers the Sim cancellation handler', () => {
25+
ensureHandlersRegistered()
26+
27+
expect(hasHandler('cancel_workflow_run')).toBe(true)
28+
})
29+
})

‎apps/sim/lib/copilot/tools/handlers/param-types.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export interface RunWorkflowParams {
6060
useDeployedState?: boolean
6161
}
6262

63+
export interface CancelWorkflowRunParams {
64+
workflowId?: string
65+
/** The workflow execution ID returned by run_workflow or query_logs. */
66+
executionId?: string
67+
}
68+
6369
export interface RunWorkflowUntilBlockParams {
6470
workflowId?: string
6571
workflow_input?: unknown

‎apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ vi.mock('@/lib/core/telemetry', () => ({
3737
}))
3838

3939
import {
40+
executeCancelWorkflowRun,
4041
executeCreateWorkflow,
4142
executeGenerateApiKey,
4243
executeMoveWorkflow,
@@ -161,6 +162,51 @@ describe('workflow mutation Copilot adapters', () => {
161162
)
162163
})
163164

165+
it('cancels a workflow run through the registered application command', async () => {
166+
mocks.executeWorkflowUseCase.mockResolvedValue({
167+
success: true,
168+
executionId: 'execution-1',
169+
redisAvailable: true,
170+
durablyRecorded: true,
171+
locallyAborted: false,
172+
pausedCancelled: false,
173+
reason: 'recorded',
174+
workflowId: 'workflow-1',
175+
workspaceId: 'workspace-1',
176+
})
177+
178+
const result = await executeCancelWorkflowRun(
179+
{ workflowId: 'workflow-1', executionId: 'execution-1' },
180+
context
181+
)
182+
183+
expect(result).toEqual({
184+
success: true,
185+
output: {
186+
workflowId: 'workflow-1',
187+
executionId: 'execution-1',
188+
durablyRecorded: true,
189+
locallyAborted: false,
190+
pausedCancelled: false,
191+
reason: 'recorded',
192+
},
193+
})
194+
expect(mocks.executeWorkflowUseCase).toHaveBeenCalledWith(
195+
context,
196+
expect.objectContaining({
197+
operation: expect.objectContaining({ id: 'workflows.runs.cancel' }),
198+
}),
199+
{ workflowId: 'workflow-1', runId: 'execution-1' }
200+
)
201+
})
202+
203+
it('requires an execution ID before attempting workflow-run cancellation', async () => {
204+
const result = await executeCancelWorkflowRun({ workflowId: 'workflow-1' }, context)
205+
206+
expect(result).toEqual({ success: false, error: 'executionId is required' })
207+
expect(mocks.executeWorkflowUseCase).not.toHaveBeenCalled()
208+
})
209+
164210
it.each([
165211
{
166212
label: 'until',

‎apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { requireCopilotWorkspace } from '@/lib/copilot/tools/server/workspace-scope'
1616
import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils'
1717
import { PlatformEvents } from '@/lib/core/telemetry'
18+
import { cancelWorkflowRun } from '@/lib/workflows/application/cancel-run'
1819
import { createWorkflow } from '@/lib/workflows/application/create-workflow'
1920
import { moveWorkflowsBulk } from '@/lib/workflows/application/move-workflows-bulk'
2021
import {
@@ -171,6 +172,7 @@ function copilotRunLifecycle(context: ExecutionContext) {
171172
}
172173

173174
import type {
175+
CancelWorkflowRunParams,
174176
CreateWorkflowParams,
175177
GenerateApiKeyParams,
176178
MoveWorkflowParams,
@@ -282,6 +284,49 @@ export async function executeRunWorkflow(
282284
}
283285
}
284286

287+
export async function executeCancelWorkflowRun(
288+
params: CancelWorkflowRunParams,
289+
context: ExecutionContext
290+
): Promise<ToolCallResult> {
291+
try {
292+
const workflowId = params.workflowId || context.workflowId
293+
if (!workflowId) {
294+
return { success: false, error: 'workflowId is required' }
295+
}
296+
const executionId = resolveInputFromExecutionId(params.executionId)
297+
if (!executionId) {
298+
return { success: false, error: 'executionId is required' }
299+
}
300+
301+
assertWorkflowMutationNotAborted(
302+
context,
303+
'Request aborted before workflow run cancellation could be applied.'
304+
)
305+
const result = await executeCopilotWorkflowUseCase(context, cancelWorkflowRun, {
306+
workflowId,
307+
runId: executionId,
308+
})
309+
310+
return {
311+
success: result.success,
312+
output: {
313+
workflowId: result.workflowId,
314+
executionId: result.executionId,
315+
durablyRecorded: result.durablyRecorded,
316+
locallyAborted: result.locallyAborted,
317+
pausedCancelled: result.pausedCancelled,
318+
reason: result.reason,
319+
},
320+
error: result.success ? undefined : 'Workflow run cancellation could not be completed',
321+
}
322+
} catch (error) {
323+
return {
324+
success: false,
325+
error: messageForCopilotWorkflowError(error, 'Failed to cancel workflow run'),
326+
}
327+
}
328+
}
329+
285330
export async function executeSetGlobalWorkflowVariables(
286331
params: SetGlobalWorkflowVariablesParams,
287332
context: ExecutionContext

‎apps/sim/lib/copilot/tools/tool-display.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,9 @@ describe('opaque id suppression', () => {
646646
it('falls back to the generic label instead of printing a workflow id', () => {
647647
expect(getToolDisplayTitle('run_workflow', { workflowName: uuid })).toBe('Running workflow')
648648
expect(getToolDisplayTitle('run_workflow', { name: uuid })).toBe('Running workflow')
649+
expect(getToolDisplayTitle('cancel_workflow_run', { executionId: uuid })).toBe(
650+
'Cancelling workflow run'
651+
)
649652
})
650653

651654
it('suppresses a bare-hex id too', () => {

‎apps/sim/lib/copilot/tools/tool-display.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ const TOOL_TITLES: Record<string, string> = {
550550
prepare_file_edit: 'Editing file',
551551
apply_file_edit: 'Writing changes',
552552
create_workflow: 'Creating workflow',
553+
cancel_workflow_run: 'Cancelling workflow run',
553554
edit_workflow: 'Editing workflow',
554555
manage_knowledge_base: 'Managing knowledge base',
555556
search_knowledge_base: 'Searching knowledge base',

0 commit comments

Comments
 (0)