v1.48.0 — Contributions Everywhere, Every Output Applied, Priority Bands That Spill
Released 2026-10-23. GitHub release.
v1.48.0 moves the embedded workflow engine to dwarf v0.12.0, which gives reduced state fields one meaning wherever a task writes them. A write to a field with a reducer is now a contribution, folded through the reducer at every hand-off rather than only at a fan-in, so a task writes only what it adds and the result no longer depends on the graph’s shape. Fabric’s generated task code follows suit: every value a task returns without an error is applied to the state, zero values included. Priorities become five bands that spill, the engine sizes each shard’s connections itself, and an agent loop’s conversation is stored once instead of at every step. The release changes the engine’s schema, its API and its wire format, so it needs a maintenance window.
Highlights
- Reducers fold at every hand-off. A write to a reduced field adds to it at the next step, the fan-in, the final state and
Continue.flow.Overwritereplaces a reduced field andflow.Delclears it. (Behavior change - see below.) - Every returned output is applied. The generated task wrapper writes each declared output with its own
flow.Set, so returningfalse,0or nil resets a field with no reducer instead of being silently skipped. - Stricter task and workflow arguments. genservice rejects an In/Out field the state cannot carry as written: unexported or
json:"-"fields, embedded structs, the,stringoption, and names ending in!or+. - Priority bands that spill. Priorities run from 1 to the foreman’s new
MaxPriorityconfig (5 by default), an unset priority is the midpoint, and capacity a better band leaves unused goes to the next one. (Breaking - see below.) - Engine-derived connection budgets. Each shard’s connections are a budget the engine derives and shares between the shard’s two dispatcher replicas;
maxConnectionson aShardsentry overrides it, andMaxOpenConnsis gone. (Breaking - see below.) - An agent loop stores each message once.
llm.core’sChatLoopappends its conversation through a reducer, and the engine keeps an appended list as references rather than a full copy per step. - agentstudio shows what a subgraph returned, on a new Subgraph tab of the step page.
- Workflow diagrams render again.
genworkflowmmd, which housekeeping runs to draw each workflow, had been failing since dwarf v0.10.0; it is fixed and now covered by a test.
Reducers Fold at Every Hand-Off
Before this release, a step’s write to a reduced field outside a fan-out branch was the field’s whole value, and the reducer combined writes only where parallel branches met. Now every write to a field wired with graph.SetReducer is a contribution, folded wherever the state passes from one step to the next. A read returns the combined value.
For task authors this comes down to four rules:
- Write your contribution, not the value you read. With
messagesonReducerAppend, returning[]llmapi.Item{reply}adds the reply. Returningappend(messages, reply)adds the whole history again. Overwriteto trim or reset.flow.Overwrite("messages", messages[max(0, len(messages)-20):])keeps the last 20.Overwrite,DelandClearof a reduced field are refused inside a fan-out branch, where a reset has no defined meaning for the other branches.- Contribute after
InterruptorSubgraph. A task runs again from the top after a park or aRetry, and each run’s contributions add to the earlier runs’. A contribution made before the park is made twice. - A subgraph returns only what it adds. Writing a child’s result into a reduced field of the parent is a contribution like any other.
The Out-suffixed pair (items in, itemsOut out) invites the read-modify-write the first rule forbids. On a reduced field, return only the new elements, and return nil on a path that adds nothing, including a park’s yield path. Fields with no reducer behave as before.
Every Returned Output Is Applied
dwarf v0.12.0 changes Flow.SetChanges to skip empty omitempty/omitzero fields. Every generated Out struct is omitzero, so a task that returned false after an earlier step set true would have left true in the state. The generated task wrapper and workflow mock now write each declared output with its own flow.Set:
err = flow.Set("approved", &out.Approved)
if err != nil {
return errors.Trace(err)
}Any value a task returns without an error is applied, through the field’s reducer or the default replace, on every successful return, including a park’s yield path. A zero value is applied like any other; under some reducers it simply changes nothing. An error return still applies nothing. Returned values are applied after the task body, so a returned value overwrites an earlier flow.Set of the same field: set a field or return it, not both. The value is passed by pointer, so a MarshalJSON method with a pointer receiver is honored.
Because each argument is read and written as its own state field, genservice now rejects what would silently misbehave in a task’s or workflow’s In and Out structs: a field that does not marshal to JSON (unexported or json:"-"), an embedded struct, the ,string option, and a JSON name ending in ! or +, which the engine refuses to write.
Priorities and the Foreman’s Configuration
Priorities now run from 1 (served first) to a maximum, 5 by default and at most 100, and a flow that sets no priority gets the midpoint, 3. The scheduler fills the best due band first and gives whatever capacity remains to the next, so a sparse but steady stream of urgent work no longer starves everything behind it while workers sit idle. Priority stays strict over capacity: worse work gets only what better work leaves unused.
The foreman’s configuration follows the engine:
MaxPriorityreplacesDefaultPriority. It sets the number of priority levels and must be the same on every replica.CreateandRunreject a priority above it with a 400.MaxOpenConnsis removed. The engine derives each shard’s connection budget (250 to 500 by round trip, clamped by the server’smax_connections) and splits it between the two replicas that dispatch the shard. SetmaxConnectionson aShardsentry for a database shared with other workloads, or when several shards are databases on one server.virtualCPUsis gone fromShardsentries, since pools no longer follow vCPUs. A newweightsets a shard’s relative share of new flows; any weight other than 1 switches the fleet to weighted placement.
The engine’s state cache now defaults to a tenth of the memory limit (GOMEMLIMIT, else the cgroup limit, else 1 GiB), so a foreman’s resident memory grows after the upgrade. Setting GOMEMLIMIT bounds it.
llm.core
ChatLoop wires its items conversation to ReducerAppend, and CallLLM contributes only the folded tool results and the new turn. Reading items still returns the whole conversation, a subgraph caller still receives it whole, and the engine stores each message once. One consequence: a Continue of a finished ChatLoop thread now appends the items it is given, so pass only the new turn.
agentstudio
The flow list’s task column and the dashboard’s error timeline read StoppedTaskName, the task a flow stopped at, which is empty while the flow runs. A step that called a subgraph gains a Subgraph tab showing the child flow’s status, a link to it, and its final state, which is what the subgraph returned.
Observability
dwarf_flows_terminatedis nowdwarf_flows_ended. The bundled Workflow Overview dashboard is updated; dashboards and alerts of your own must change.dwarf_flows_startedanddwarf_flows_endedgain aprioritylabel.dwarf_steps_pendinganddwarf_steps_oldest_pending_age_secondsare reported every 30 seconds by one leading replica per shard. Aggregate pending steps withmaxper shard and priority, thensumacross shards; the dashboard now does.- New panels for dispatch lag by shard, API operation latency by shard, dispatching replicas per shard, and state cache memory.
Other Changes
- Workflow rules and skills. The workflow rules carry the reducer contract and the returned-values rule, drop their copy of the workflow API in favor of the semantics godoc does not state, and correct several descriptions: the Interrupt-and-Resume example (resume data arrives through the
&resumepointer, not the state),forEachbranch state, merge order, and fairness weights. A subgraph’s result is described as the calling task’s to merge, and the review skills check reduced-field writes. genworkflowmmd. The program it generates is compiled only when the tool runs, so it had silently failed sinceGraphRenderer.Renderstopped returning an error in dwarf v0.10.0. It is fixed, and a golden test now runs it against a fixture. Rendered diagrams no longer carry a title.- Dependencies. dwarf v0.12.0, sequel v1.15.0, OpenTelemetry v1.47.0, modernc.org/sqlite v1.60.1, and a general refresh.
Breaking Changes
- Writes to reduced fields are contributions. A task that returns or sets a reduced field’s current value plus its addition now adds the history twice. Return only the new part; use
flow.Overwriteto reset. - dwarf API renames and removals that reach Fabric code:
workflow.ENDisworkflow.End;Flow.ParseStateisParse;Graph.SetEndpointisSetURL;Flow.Getreturns(ok bool, err error);GotoRequested,RetryRequested,SleepRequested,InterruptRequestedandSubgraphRequestedmoved fromFlowtoRawFlow;workflow.NodeisTask;Transition’sWithGoto/OnError/Switchbooleans are oneKind;FlowStep.SubWorkflowURL/SubWorkflowNameareSubgraphURL/SubgraphName, and its edges are step keys;workflow.TerminalStatusesis gone in favor ofIsTerminalStatus;engine.TestProxyishosts.Mux. FlowSummary.TaskNameandQuery.TaskNameareStoppedTaskName, and the meaning changed: empty while a flow runs, so a filter on it matches no running flow.foremanapi.ShardSummary:ShardisIndex,LatencyMs intisLatency time.Duration, andStepsandFlowsare gone.- Foreman configuration:
DefaultPriorityandMaxOpenConnsare removed,MaxPriorityis new, andvirtualCPUsis gone fromShardsentries. - Priorities above the range are rejected. An explicit priority above
MaxPriorityfailsCreateandRunwith a 400. An unset priority now runs at 3, ahead of explicit 4s and 5s, where it previously ran atDefaultPriority(5). - Changed status codes:
Terminateof an ended flow returns nil (was 409); anAwaitorRuntimeout is a 408 that also satisfieserrors.Is(err, context.DeadlineExceeded); aTerminateorCancelreason over 4 KB is a 400; any value over 256 MB is a 413. - Stricter genservice. A task or workflow In/Out field that is unexported, tagged
json:"-", embedded, uses,string, or has a name ending in!or+now fails generation. - The engine’s schema and wire format changed. See Operational Prerequisites.
Operational Prerequisites
- Recreate or migrate workflow databases. dwarf v0.12.0 revised its migrations in place, so a database migrated by an earlier version does not receive the changes and the new engine fails on it. In
LOCALdevelopment, deleteshard_1.local.sqlite. For deployed shards, plan a maintenance window: drain or terminate in-flight flows, then use a fresh database or apply the schema changes by hand, as described under “Upgrading from v0.11.0” in the dwarf v0.12.0 release notes. Every shard needs the newdwarf_peers.ordering_seqcolumn. - Deploy everything together. Every microservice decodes and re-encodes the flow, whose JSON changed, so task microservices and foreman replicas must all run v1.48.0. Old and new replicas also disagree on how a shard’s dispatchers are chosen, and must not run side by side.
- Bring old priorities into range with
UPDATE dwarf_flows SET priority=3 WHERE priority>5and the same ondwarf_steps, unlessMaxPrioritycovers them. Threads continued, forked or subgraphed from a flow created at the old default keep its priority of 5. - Budget the foreman’s memory with
GOMEMLIMIT, which bounds the engine’s state cache. - PostgreSQL sessions run in UTC. sequel v1.15.0 starts every PostgreSQL and CockroachDB session in UTC unless the data source name sets a time zone. SQL that relied on a non-UTC server or role time zone, such as
NOW()into aTIMESTAMPcolumn, now computes in UTC; add atimezoneparameter to keep the old zone.
Migration
From inside a Microbus project, ask Claude Code to upgrade Microbus:
The upgrade-microbus skill applies the dwarf renames, walks through the signature changes go vet reports, and reviews every task that writes a reduced field, including Out pairs and park paths, with you where the intended total is a design choice. It asks how to bring explicit priorities into the new range, what to do with a pinned MaxOpenConns, and flags ChatLoop Continue callers, assertions on changed status codes, dashboard queries and PostgreSQL time zones. It offers to delete the local SQLite shard file; deployed databases are out of its reach, so it reminds you to migrate them.
Documentation
- Reducers is rewritten around contributions: write what you add,
Overwriteto reset, contribute after a park, and return only the contribution from anOutpair on a reduced field. State covers typed arguments, every returned value being applied, and the In/Out fields genservice rejects, and correctsforEachbranch state and the empty-forEachfan-in. workflowfollows dwarf v0.12.0: theFlowaccessors,Get,Parse,SetChangesandOverwrite, theTransitionkinds,Task,NewGraphRenderer, the fullFlowOptions, andRawFlow.foreman, Production Readiness and Priority and Fairness documentMaxPriorityand bands that spill,Shardsentries withmaxConnectionsandweight, the derived connection budget, the state cache andGOMEMLIMIT, database settings, and the new metrics and dashboard panels. Reliability and Recovery covers crashed replicas andStoppedTaskName.llm, LLM Integration and Building an LLM Workflow describeChatLoop’s append-reduceditemsand passing only the new turn toContinue. The example service pages show the current generated diagrams.- Python Integration shows the
pyvenvcalls as they are:CallAndAwaitin an endpoint, andCallthenAwaitin a workflow task. - agentstudio has its own page, including the step page’s Subgraph tab, and
cmddocumentsgenworkflowmmdandgeninit.