Context
The engine implements workflow cancellation but no user-facing surface exists. WorkflowOrchestrationService.cancel(workflowId) recursively cancels children and fires the parent callback with Canceled status, and WorkflowRunnerService.cancel(workflowId, userId) is an auth-aware wrapper — but nothing in @loopstack/api, the Studio frontend, or BaseWorkflow exposes it. Users have no way to cancel a run, and there is zero documentation for cancellation as a general feature (only a passing mention in agent-workflows.md).
Originally surfaced as Q59 in context/how-to-2-workflows.md ("Can a sub-workflow be cancelled? How does recursive cancellation work?"), currently tagged [not answered]. Decision was to implement the missing surface area first, then document.
What to do
- Add a REST endpoint —
POST /workflows/:id/cancel (or DELETE /workflows/:id) wired to WorkflowRunnerService.cancel().
- Add a Studio UI button — cancel button on the workbench/run view that calls the REST endpoint, with a
ConfirmDialog to avoid misclicks on long-running runs. Frontend already renders canceled status colors.
- Add a programmatic API on
BaseWorkflow — this.cancel(workflowId) and/or this.cancelChildren() so a workflow can cancel sub-workflows from transition code. Recommendation: cancelling a child fires the parent callback with Canceled status (matches existing callback path).
- Confirm the auth model — is
WorkflowRunnerService.cancel user-scoping correct for the REST surface? (owner-only? workspace-wide? admin override?)
- Decide idempotency / race handling — cancel of an already-terminal workflow currently logs and skips. Surface this predictably from REST + programmatic paths (200 idempotent vs 409 conflict).
Once shipped, document:
loopstack/docs/build/patterns/sub-workflows.md — cancelling a parent recursively cancels children; callback fires with Canceled status.
loopstack/docs/learn/workflow-engine.md — engine-internals view: Canceled terminal state, callback semantics.
- Swagger docs for the new endpoint.
- Update Q59 coverage tag in
context/how-to-2-workflows.md from [not answered] to [sufficiently answered].
Affected area
- Package(s):
@loopstack/api, @loopstack/core, @loopstack/loopstack-studio
- File(s):
loopstack/packages/core/src/workflow-processor/services/workflow-orchestration.service.ts (existing cancel())
loopstack/packages/core/src/scheduler/services/workflow-runner.service.ts (existing auth-aware cancel())
loopstack/packages/api/src/... (new endpoint)
loopstack/frontend/studio/... (new button)
Acceptance criteria
Open questions
- Should cancel fire any cleanup hooks (e.g. a
@OnCancel transition), or is "terminal status + callback" the whole feature?
- Do we need partial-cancel (cancel one child, parent continues)? Current
cancel() recursively cancels children of the target — fine. No API to cancel a sibling without cancelling the parent. Probably not needed.
Additional context
Source: todo-cancel.md in the project root.
Context
The engine implements workflow cancellation but no user-facing surface exists.
WorkflowOrchestrationService.cancel(workflowId)recursively cancels children and fires the parent callback withCanceledstatus, andWorkflowRunnerService.cancel(workflowId, userId)is an auth-aware wrapper — but nothing in@loopstack/api, the Studio frontend, orBaseWorkflowexposes it. Users have no way to cancel a run, and there is zero documentation for cancellation as a general feature (only a passing mention inagent-workflows.md).Originally surfaced as Q59 in
context/how-to-2-workflows.md("Can a sub-workflow be cancelled? How does recursive cancellation work?"), currently tagged[not answered]. Decision was to implement the missing surface area first, then document.What to do
POST /workflows/:id/cancel(orDELETE /workflows/:id) wired toWorkflowRunnerService.cancel().ConfirmDialogto avoid misclicks on long-running runs. Frontend already renderscanceledstatus colors.BaseWorkflow—this.cancel(workflowId)and/orthis.cancelChildren()so a workflow can cancel sub-workflows from transition code. Recommendation: cancelling a child fires the parent callback withCanceledstatus (matches existing callback path).WorkflowRunnerService.canceluser-scoping correct for the REST surface? (owner-only? workspace-wide? admin override?)Once shipped, document:
loopstack/docs/build/patterns/sub-workflows.md— cancelling a parent recursively cancels children; callback fires withCanceledstatus.loopstack/docs/learn/workflow-engine.md— engine-internals view:Canceledterminal state, callback semantics.context/how-to-2-workflows.mdfrom[not answered]to[sufficiently answered].Affected area
@loopstack/api,@loopstack/core,@loopstack/loopstack-studioloopstack/packages/core/src/workflow-processor/services/workflow-orchestration.service.ts(existingcancel())loopstack/packages/core/src/scheduler/services/workflow-runner.service.ts(existing auth-awarecancel())loopstack/packages/api/src/...(new endpoint)loopstack/frontend/studio/...(new button)Acceptance criteria
WorkflowRunnerService.cancel()BaseWorkflowexposes a programmatic cancel API for sub-workflowssub-workflows.md,workflow-engine.md, Swagger)context/how-to-2-workflows.mdre-tagged[sufficiently answered]Open questions
@OnCanceltransition), or is "terminal status + callback" the whole feature?cancel()recursively cancels children of the target — fine. No API to cancel a sibling without cancelling the parent. Probably not needed.Additional context
Source:
todo-cancel.mdin the project root.