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 when Cancel is called finishes, then receives the cancellation on its node’s onError transition. 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 Terminate endpoint. foremanapi.Terminate(flowKey, reason) unconditionally stops a running or interrupted flow and its whole subgraph tree, abandoning in-flight work.
  • New terminated status. A terminated flow ends terminated with its reason in FlowOutcome.TerminateReason and FlowSummary.TerminateReason, distinct from a gracefully cancelled flow and its CancelReason.
  • agentstudio shows both. The flow page offers Cancel while a flow runs and Terminate while it runs or is interrupted, and terminated appears 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:

  • Cancel returns once the request is recorded. Await the 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.Sleep or flow.Retry backoff stays running until then.
  • An interrupted flow stays interrupted until it is resumed or terminated.
  • Cancellation crosses subgraphs. If a child flow was cancelled, the error flow.Subgraph returns to the parent task satisfies workflow.IsCancelled, and returning it keeps the parent’s loss a cancellation.
  • A flow is cancelled once. A repeat Cancel does nothing, so a call whose response was lost is safe to retry, even after an onError handler 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

  • Cancel no longer stops a flow outright. Code that cancelled an interrupted flow, or one sleeping or backed off for a long time, and awaited cancelled now waits until the step wakes, or indefinitely for an interrupted flow. Switch such call sites to Terminate and check for workflow.StatusTerminated and TerminateReason.
  • A new terminal status, terminated. Code that enumerates terminal or error statuses by hand should add workflow.StatusTerminated, or use workflow.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 LOCAL development, delete the shard_1.local.sqlite file. Existing flows do not carry over.

Migration

From inside a Microbus project, ask Claude Code to upgrade Microbus:

Get the latest version of 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.