Skip to content

Commit 5928f37

Browse files
committed
feat(copilot): execute workflow run cancellations
1 parent 895d40e commit 5928f37

9 files changed

Lines changed: 223 additions & 2 deletions

File tree

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

Lines changed: 54 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'
@@ -71,6 +72,7 @@ export interface ToolCatalogEntry {
7172
| 'load_deployment'
7273
| 'load_integration_tool'
7374
| 'load_skill'
75+
| 'load_slide_layout'
7476
| 'manage_credential'
7577
| 'manage_custom_tool'
7678
| 'manage_knowledge_base'
@@ -162,6 +164,7 @@ export interface ToolCatalogEntry {
162164
| 'browser_type'
163165
| 'browser_wait_for'
164166
| 'call_integration_tool'
167+
| 'cancel_workflow_run'
165168
| 'connect_slack_bot'
166169
| 'cp'
167170
| 'create_empty_file'
@@ -199,6 +202,7 @@ export interface ToolCatalogEntry {
199202
| 'load_deployment'
200203
| 'load_integration_tool'
201204
| 'load_skill'
205+
| 'load_slide_layout'
202206
| 'manage_credential'
203207
| 'manage_custom_tool'
204208
| 'manage_knowledge_base'
@@ -1612,6 +1616,31 @@ export const CallIntegrationTool: ToolCatalogEntry = {
16121616
requiresApproval: true,
16131617
}
16141618

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+
16151644
export const ConnectSlackBot: ToolCatalogEntry = {
16161645
id: 'connect_slack_bot',
16171646
name: 'connect_slack_bot',
@@ -3143,6 +3172,24 @@ export const LoadSkill: ToolCatalogEntry = {
31433172
},
31443173
}
31453174

3175+
export const LoadSlideLayout: ToolCatalogEntry = {
3176+
id: 'load_slide_layout',
3177+
name: 'load_slide_layout',
3178+
route: 'go',
3179+
mode: 'sync',
3180+
parameters: {
3181+
type: 'object',
3182+
properties: {
3183+
name: {
3184+
type: 'string',
3185+
description:
3186+
"Layout name exactly as it appears in the Layout Library index (e.g. 'metric-cards').",
3187+
},
3188+
},
3189+
required: ['name'],
3190+
},
3191+
}
3192+
31463193
export const ManageCredential: ToolCatalogEntry = {
31473194
id: 'manage_credential',
31483195
name: 'manage_credential',
@@ -4406,7 +4453,11 @@ export const Run: ToolCatalogEntry = {
44064453
description: 'Pre-gathered context: workflow state, block IDs, input requirements.',
44074454
type: 'string',
44084455
},
4409-
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+
},
44104461
},
44114462
required: ['request'],
44124463
type: 'object',
@@ -7004,6 +7055,7 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
70047055
[BrowserType.id]: BrowserType,
70057056
[BrowserWaitFor.id]: BrowserWaitFor,
70067057
[CallIntegrationTool.id]: CallIntegrationTool,
7058+
[CancelWorkflowRun.id]: CancelWorkflowRun,
70077059
[ConnectSlackBot.id]: ConnectSlackBot,
70087060
[Cp.id]: Cp,
70097061
[CreateEmptyFile.id]: CreateEmptyFile,
@@ -7041,6 +7093,7 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
70417093
[LoadDeployment.id]: LoadDeployment,
70427094
[LoadIntegrationTool.id]: LoadIntegrationTool,
70437095
[LoadSkill.id]: LoadSkill,
7096+
[LoadSlideLayout.id]: LoadSlideLayout,
70447097
[ManageCredential.id]: ManageCredential,
70457098
[ManageCustomTool.id]: ManageCustomTool,
70467099
[ManageKnowledgeBase.id]: ManageKnowledgeBase,

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

Lines changed: 35 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',
@@ -3028,6 +3047,20 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
30283047
},
30293048
resultSchema: undefined,
30303049
},
3050+
load_slide_layout: {
3051+
parameters: {
3052+
type: 'object',
3053+
properties: {
3054+
name: {
3055+
type: 'string',
3056+
description:
3057+
"Layout name exactly as it appears in the Layout Library index (e.g. 'metric-cards').",
3058+
},
3059+
},
3060+
required: ['name'],
3061+
},
3062+
resultSchema: undefined,
3063+
},
30313064
manage_credential: {
30323065
parameters: {
30333066
type: 'object',
@@ -4314,7 +4347,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
43144347
type: 'string',
43154348
},
43164349
request: {
4317-
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.',
43184352
type: 'string',
43194353
},
43204354
},

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
@@ -35,6 +35,7 @@ vi.mock('@/lib/core/telemetry', () => ({
3535
}))
3636

3737
import {
38+
executeCancelWorkflowRun,
3839
executeCreateWorkflow,
3940
executeGenerateApiKey,
4041
executeMoveWorkflow,
@@ -158,6 +159,51 @@ describe('workflow mutation Copilot adapters', () => {
158159
)
159160
})
160161

162+
it('cancels a workflow run through the registered application command', async () => {
163+
mocks.executeWorkflowUseCase.mockResolvedValue({
164+
success: true,
165+
executionId: 'execution-1',
166+
redisAvailable: true,
167+
durablyRecorded: true,
168+
locallyAborted: false,
169+
pausedCancelled: false,
170+
reason: 'recorded',
171+
workflowId: 'workflow-1',
172+
workspaceId: 'workspace-1',
173+
})
174+
175+
const result = await executeCancelWorkflowRun(
176+
{ workflowId: 'workflow-1', executionId: 'execution-1' },
177+
context
178+
)
179+
180+
expect(result).toEqual({
181+
success: true,
182+
output: {
183+
workflowId: 'workflow-1',
184+
executionId: 'execution-1',
185+
durablyRecorded: true,
186+
locallyAborted: false,
187+
pausedCancelled: false,
188+
reason: 'recorded',
189+
},
190+
})
191+
expect(mocks.executeWorkflowUseCase).toHaveBeenCalledWith(
192+
context,
193+
expect.objectContaining({
194+
operation: expect.objectContaining({ id: 'workflows.runs.cancel' }),
195+
}),
196+
{ workflowId: 'workflow-1', runId: 'execution-1' }
197+
)
198+
})
199+
200+
it('requires an execution ID before attempting workflow-run cancellation', async () => {
201+
const result = await executeCancelWorkflowRun({ workflowId: 'workflow-1' }, context)
202+
203+
expect(result).toEqual({ success: false, error: 'executionId is required' })
204+
expect(mocks.executeWorkflowUseCase).not.toHaveBeenCalled()
205+
})
206+
161207
it.each([
162208
{
163209
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
@@ -10,6 +10,7 @@ import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/typ
1010
import { requireCopilotWorkspace } from '@/lib/copilot/tools/server/workspace-scope'
1111
import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils'
1212
import { PlatformEvents } from '@/lib/core/telemetry'
13+
import { cancelWorkflowRun } from '@/lib/workflows/application/cancel-run'
1314
import { createWorkflow } from '@/lib/workflows/application/create-workflow'
1415
import { moveWorkflowsBulk } from '@/lib/workflows/application/move-workflows-bulk'
1516
import {
@@ -116,6 +117,7 @@ function copilotRunLifecycle(context: ExecutionContext) {
116117
}
117118

118119
import type {
120+
CancelWorkflowRunParams,
119121
CreateWorkflowParams,
120122
GenerateApiKeyParams,
121123
MoveWorkflowParams,
@@ -227,6 +229,49 @@ export async function executeRunWorkflow(
227229
}
228230
}
229231

232+
export async function executeCancelWorkflowRun(
233+
params: CancelWorkflowRunParams,
234+
context: ExecutionContext
235+
): Promise<ToolCallResult> {
236+
try {
237+
const workflowId = params.workflowId || context.workflowId
238+
if (!workflowId) {
239+
return { success: false, error: 'workflowId is required' }
240+
}
241+
const executionId = resolveInputFromExecutionId(params.executionId)
242+
if (!executionId) {
243+
return { success: false, error: 'executionId is required' }
244+
}
245+
246+
assertWorkflowMutationNotAborted(
247+
context,
248+
'Request aborted before workflow run cancellation could be applied.'
249+
)
250+
const result = await executeCopilotWorkflowUseCase(context, cancelWorkflowRun, {
251+
workflowId,
252+
runId: executionId,
253+
})
254+
255+
return {
256+
success: result.success,
257+
output: {
258+
workflowId: result.workflowId,
259+
executionId: result.executionId,
260+
durablyRecorded: result.durablyRecorded,
261+
locallyAborted: result.locallyAborted,
262+
pausedCancelled: result.pausedCancelled,
263+
reason: result.reason,
264+
},
265+
error: result.success ? undefined : 'Workflow run cancellation could not be completed',
266+
}
267+
} catch (error) {
268+
return {
269+
success: false,
270+
error: messageForCopilotWorkflowError(error, 'Failed to cancel workflow run'),
271+
}
272+
}
273+
}
274+
230275
export async function executeSetGlobalWorkflowVariables(
231276
params: SetGlobalWorkflowVariablesParams,
232277
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ const TOOL_TITLES: Record<string, string> = {
535535
search_integration_tools: 'Finding the right integration',
536536
load_integration_tool: 'Loading integration tools',
537537
load_skill: 'Loading skill',
538+
load_slide_layout: 'Loading slide layout',
538539
read: 'Reading file',
539540
search_library_docs: 'Searching library docs',
540541
user_table: 'Managing table',
@@ -549,6 +550,7 @@ const TOOL_TITLES: Record<string, string> = {
549550
prepare_file_edit: 'Editing file',
550551
apply_file_edit: 'Writing changes',
551552
create_workflow: 'Creating workflow',
553+
cancel_workflow_run: 'Cancelling workflow run',
552554
edit_workflow: 'Editing workflow',
553555
manage_knowledge_base: 'Managing knowledge base',
554556
search_knowledge_base: 'Searching knowledge base',

0 commit comments

Comments
 (0)