v1.39.0 extracts the workflow engine into its own module — Dwarf — and rebuilds the Foreman as a thin Microbus adapter over it. The workflow package moves with the engine: github.com/microbus-io/fabric/workflow becomes github.com/microbus-io/dwarf/workflow, adding a dwarf dependency, while its exported types keep their names. The move brings the engine's current API. workflow.NewGraph gains a leading graph-name argument; flow.Goto now takes a graph node name instead of an endpoint URL; and flow.Subgraph / flow.Interrupt adopt an out-pointer — yield, err := flow.Subgraph(url, input, &out) — that unmarshals the child or resume result into a struct you pass, instead of returning a map. The asynchronous stop notification moves onto the flow itself: the StartNotify endpoint is gone, replaced by FlowOptions.NotifyOnStop set at Create. PascalCase becomes the convention for graph and task node names. Separately and non-breaking, sequel v1.10.2 adds an opt-in OpenTelemetry layer — client spans, sequel_* metrics, and migration logs — that lights up once a SQL microservice attaches the connector's providers.
Highlights
- Workflow engine extracted to the Dwarf module. The engine that defines
Graph,Flow,FlowOptions,FlowOutcome, the reducers, andENDnow lives ingithub.com/microbus-io/dwarf. The Foreman is a thin Microbus host over the embedded engine; the engine owns durable flow execution, scheduling, and recovery and stays transport-agnostic. Every file that importedfabric/workflownow importsdwarf/workflow, and the project gains a direct dwarf dependency. workflow.NewGraphgained a name argument (loud).NewGraph(url)becomesNewGraph(name, url); the new first argument is the graph's display name (its PascalCase feature name). A call left at one argument is a compile error.flow.Gototakes a graph node name, not an endpoint URL (silent). The signature is unchanged (Goto(string)), but the value's meaning changed: pass the node name the task was registered under inAddTask, notsomeapi.Task.URL(). A call left asflow.Goto(api.X.URL())compiles and then fails to route.flow.Subgraph/flow.Interruptadopt an out-pointer (loud). Both now take a trailingout anypointer and return(yield, err); the child's final state and the resume payload are unmarshaled intooutrather than returned as a leading map. The idiom isvar out T; yield, err := flow.Subgraph(url, input, &out). Theinputmay be a typed struct or amap[string]any;outmay be a*struct,*map[string]any, ornil.StartNotifyremoved;FlowOptions.NotifyOnStopreplaces it (loud). To be notified when a flow stops, setNotifyOnStop: trueatCreate. The Foreman records the caller's host at create time and firesOnFlowStoppedto it when the flow terminates — no delivery address is passed.- PascalCase graph and task node names. Newly scaffolded graphs name their graph and nodes in PascalCase (
AddTask("VerifySSN", ...)). Node names are arbitrary strings that only need to be internally consistent, so existing graphs are not required to change. - sequel telemetry (opt-in, non-breaking). sequel v1.10.2 emits OpenTelemetry client spans,
sequel_*metrics, andslogmigration logs once a SQL microservice attaches the connector'sTracerProvider/MeterProvider/Loggerto its*sequel.DB. Without the wiring, behavior is identical.
New Features
The Dwarf Engine
The workflow engine is now the standalone Dwarf module. The Foreman implements the engine's Host interface — loading graphs over the bus, dispatching tasks, classifying transport errors, and delivering stop notifications — while Dwarf owns the durable state machine, the lease-based step execution, the adaptive rate-limit valve and per-task circuit breaker, and the recovery sweeps. Because the engine never inspects HTTP status codes itself, the Foreman maps a task's reply into the engine's two disposition wrappers — workflow.ErrRateLimited (a 429, engaging the valve) and workflow.ErrUnavailable (a 404 ack-timeout, 503, or 529, tripping the breaker). In a Microbus deployment you return the status code as before; the wrappers are the engine-facing translation.
Microbus docs stay at the "how to use" level — the Dwarf repository owns the engine internals.
Out-Pointer Subgraphs and Interrupts
flow.Subgraph and flow.Interrupt deliver their result through a pointer argument and return only (yield, err). Passing a typed In / Out struct keeps the boundary type-safe:
func (svc *Service) RunIdentityVerification(ctx context.Context, flow *workflow.Flow, applicantName string, ssn string) (identityVerified bool, err error) {
var out creditflowapi.IdentityVerificationOut
yield, err := flow.Subgraph(creditflowapi.IdentityVerification.URL(), creditflowapi.IdentityVerificationIn{
ApplicantName: applicantName,
SSN: ssn,
}, &out)
if yield {
return false, nil // first pass: parked, child workflow running
}
if err != nil {
return false, errors.Trace(err) // child failed; retry, route, or propagate
}
return out.IdentityVerified, nil
}flow.Interrupt(payload, &resume) is symmetric: it unmarshals the data passed to foremanapi.Resume into resume. Pass nil for out when you do not need the result inline.
Asynchronous Notification via NotifyOnStop
Opting a flow into a stop notification is now a flow option, not a separate endpoint call:
flowID, err := client.Create(ctx, myserviceapi.CreditApproval.URL(), map[string]any{
"applicant": applicant,
}, &workflow.FlowOptions{NotifyOnStop: true})
// ... handle err ...
err = client.Start(ctx, flowID)The Foreman fires the OnFlowStopped outbound event back to the calling microservice when the flow reaches a terminal status or interrupts. It records the caller's host at Create, so you no longer pass a hostname.
sequel Telemetry
A SQL CRUD microservice lights up sequel's observability by attaching the connector's providers in openDatabase, before the Migrate call so migrations are instrumented too:
svc.db, err = sequel.OpenSingleton(driverName, dataSourceName)
if err != nil {
return errors.Trace(err)
}
// Route sequel's spans, sequel_* metrics, and migration logs through the connector's telemetry pipeline.
svc.db.SetTracerProvider(svc.TracerProvider())
svc.db.SetMeterProvider(svc.MeterProvider())
svc.db.SetLogger(svc.Logger())The accessors return no-op providers when a signal is disabled, so the block is safe in every deployment including TESTING.
Breaking Changes
The upgrade skill handles each of these. Manual migration is not recommended.
github.com/microbus-io/fabric/workflow→github.com/microbus-io/dwarf/workflow(loud). Every file that imports the workflow package — hand-writtenservice.go, the generatedintermediate.go/mock.go/mock_test.go, and the*api/client.goproxy — is repointed, and the project gains a direct dwarf dependency. Exported identifiers are otherwise unchanged.workflow.NewGraph(url)→NewGraph(name, url)(loud). Add the graph's PascalCase name as the leading argument.flow.Goto(url)→flow.Goto(nodeName)(silent). Pass the node name registered inAddTask, not an endpoint URL. A mismatch is caught bygraph.Validate()at startup, not by the compiler.flow.Subgraph/flow.Interruptout-pointer (loud).data, yield, err := flow.Subgraph(url, input)becomesyield, err := flow.Subgraph(url, input, &out); likewise forflow.Interrupt(payload, &out). Both the argument count and the return count change, so old call sites fail to compile.foremanapi.StartNotifyremoved (loud). SetFlowOptions.NotifyOnStopatCreateandStartthe flow as usual; the notify host is recorded automatically.- sequel bumped to v1.10.2. Additive — the telemetry layer is opt-in. No call sites break.
Migration
From inside a Microbus project, ask Claude Code to upgrade Microbus:
{{< prompt >}}
Get the latest version of Microbus.
{{< /prompt >}}
The upgrade skill handles the version bump end-to-end:
- Bump
go.modtov1.39.0andgo mod tidy(which addsgithub.com/microbus-io/dwarfand the sequel bump). - Refresh
.claude/rules/,.claude/skills/, and project-wide framework-managed files. - Relocate the workflow import project-wide (
fabric/workflow→dwarf/workflow). - Add the leading name argument to every
workflow.NewGraphcall. - Change
flow.Gotoarguments from endpoint URLs to the registered node names. - Rewrite
flow.Subgraph/flow.Interruptcall sites to the out-pointer shape, and replaceStartNotifywithFlowOptions.NotifyOnStop. - Wire sequel telemetry into each SQL CRUD microservice's
openDatabase. - Regenerate mocks (
genmock) and manifests (genmanifest), then rungo vet ./... && go test ./....
The load-bearing assertion is the silent one: a workflow test that drives a Goto transition fails to route if a call still passes a .URL() instead of the node name.
Documentation
- Updated: Agentic Workflows and State for the out-pointer subgraph boundary and the relocated workflow package.
- New: Yield and Re-Enter — the park-and-re-execute pattern shared by
Interrupt,Subgraph, andRetry, with the out-pointer idiom. - Updated: Building Agentic Workflows and Building an LLM Workflow for
NewGraph(name, url),flow.Gotonode names, the out-pointer signatures,NotifyOnStop, and PascalCase node names. - Updated: Package
workflowfor the out-pointer park signals and the typedIn/Outsubgraph contracts. - Updated: Foreman for the Dwarf engine adapter and the
NotifyOnStopnotification model. - Updated: Credit Flow example for the typed-struct subgraph call and PascalCase node names.