v1.47.0 - Graceful Cancel, Forceful Terminate
Released 2026-09-27. GitHub release.
v1.47.0 moves the embedded workflow engine to dwarf v0.11.0, which splits stopping a flow into two distinct operations. Cancel becomes graceful: the workflow is told it is being cancelled and gets to react, compensating in an onError handler or carrying on. Terminate is the forceful stop Cancel used to be, now exposed on the foreman in its own right and ending the flow in a new terminated status. The agentstudio dev UI learns the new status and offers both actions.
Highlights
- Graceful
Cancel. Every step in progress whenCancelis called finishes, then receives the cancellation on its node’sonErrortransition.workflow.IsCancelled(onErr)tells a cancellation apart from a real failure, so a workflow can compensate and continue, or stop. (Behavior change - see below.) - New
Terminateendpoint.foremanapi.Terminate(flowKey, reason)unconditionally stops a running or interrupted flow and its whole subgraph tree, abandoning in-flight work. - New
terminatedstatus. A terminated flow endsterminatedwith its reason inFlowOutcome.TerminateReasonandFlowSummary.TerminateReason, distinct from a gracefullycancelledflow and itsCancelReason. - agentstudio shows both. The flow page offers Cancel while a flow runs and Terminate while it runs or is interrupted, and
terminatedappears in the status chips, list filter and dashboard charts.
Cancel and Terminate
Before this release, Cancel stopped a flow outright. That is still available, now named Terminate, and Cancel means something gentler.
Cancel(flowKey, reason) covers every step in progress when it is called. A step that has not started does not run. A step that is already running finishes, and its next transition is not taken. The covered step receives the cancellation as an error on its node’s onError transition, like any task error except that the task’s own changes are kept, because it ran. An error the task itself returns wins over the cancellation. With no onError transition the flow stops, ending cancelled if every unrecovered loss in it was a cancellation and failed if a real error is mixed in.
// A compensating handler reached via AddTransitionOnError
func (svc *Service) Compensate(ctx context.Context, flow *workflow.Flow, onErr *errors.TracedError) (err error) {
if workflow.IsCancelled(onErr) {
// Undo the side effects of the steps that ran, then let the flow end
}
// ...
}A few consequences are worth knowing:
Cancelreturns once the request is recorded.Awaitthe flow to see it stop.- A sleeping or backed-off step is reached only when it next wakes, so a flow parked on a long
flow.Sleeporflow.Retrybackoff stays running until then. - An interrupted flow stays
interrupteduntil it is resumed or terminated. - Cancellation crosses subgraphs. If a child flow was cancelled, the error
flow.Subgraphreturns to the parent task satisfiesworkflow.IsCancelled, and returning it keeps the parent’s loss a cancellation. - A flow is cancelled once. A repeat
Canceldoes nothing, so a call whose response was lost is safe to retry, even after anonErrorhandler caught the first cancellation and the flow carried on. To stop a flow that recovered, terminate it.
Terminate(flowKey, reason) is unconditional. It stops a running or interrupted flow and every flow in its subgraph tree, abandoning in-flight work rather than awaiting it. Both operations are addressed by the root flow key.
agentstudio
The flow detail page’s actions follow the new split. Cancel is offered only while a flow is running, since a graceful cancel does nothing visible on an interrupted flow. A new Terminate action is offered while a flow is running or interrupted, and Continue and Fork appear once the flow has stopped, terminated included. The terminated status gets its own color in the status chips, the list filter, and the dashboard’s stacked status chart and error-events timeline, and the flow’s reason field shows whichever of TerminateReason or CancelReason applies.
Other Changes
- CI enforces
gofmt. A new job fails the build when any tracked Go file is unformatted, and the files that had drifted are reformatted. - Dependencies. dwarf v0.11.0, boolexp v1.3.0, nats.go v1.54.0, OpenTelemetry v1.46.0, gRPC v1.84.0, and a general refresh.
Breaking Changes
Cancelno longer stops a flow outright. Code that cancelled an interrupted flow, or one sleeping or backed off for a long time, and awaitedcancellednow waits until the step wakes, or indefinitely for an interrupted flow. Switch such call sites toTerminateand check forworkflow.StatusTerminatedandTerminateReason.- A new terminal status,
terminated. Code that enumerates terminal or error statuses by hand should addworkflow.StatusTerminated, or useworkflow.IsTerminalStatus. - Workflow databases must be recreated. dwarf v0.11.0 changes the engine’s schema by revising its existing migrations, so a database already migrated by an earlier version does not pick up the new columns. Drop and recreate each shard database, or point the foreman at a fresh one; in
LOCALdevelopment, delete theshard_1.local.sqlitefile. Existing flows do not carry over.
Migration
From inside a Microbus project, ask Claude Code to upgrade Microbus:
The upgrade-microbus skill finds each Cancel call and works through with you whether it should stay graceful or become Terminate, adds the terminated status wherever statuses are enumerated, and offers to delete the local SQLite shard file. A project that neither defines nor drives workflows skips all of it. The skill cannot reach deployed databases, so it reminds you to recreate them as part of the upgrade.
Documentation
foreman gains a section on cancelling and terminating, and lists terminated among the flow statuses. workflow documents TerminateReason, StatusTerminated, IsTerminalStatus and IsCancelled, and State shows how an onError handler recognizes a cancellation.