Skip to content

Typed Subflow Client, Names over URLs, Slimmer Scaffold

Latest

Choose a tag to compare

@bw19 bw19 released this 18 Jun 14:57

v1.40.0 follows the Dwarf engine extracted in v1.39.0 with a graph/task API cleanup (dwarf v0.4.1v0.5.0) that prefers node names over endpoint URLs, and a new typed Subflow client for invoking one microservice's tasks and workflows from inside another task body. workflow.NewGraph(name, url) drops its URL argument to become NewGraph(name), and graph builders register a node's endpoint with graph.SetEndpoint(name, url) instead of graph.AddTask. Every generated *api/client.go now ships a Subflow type and a NewSubflow(flow) constructor — the blessed, state-isolated way for a task to call another unit of work — backed by the new flow.Subtask(name, taskURL, in, out) primitive, the task-level sibling of flow.Subgraph. The Foreman's CreateTask gains a required leading name argument, and the OnFlowStopped event now carries the flowKey directly (the FlowOutcome.FlowKey field is gone). Separately, the act package is no longer scaffolded into new projects: imperative claim checks go through frame.Of(ctx) directly, with the typed Actor struct demoted to an optional, solution-owned pattern.

Highlights

  • workflow.NewGraph(name, url)NewGraph(name) (loud). A graph is now named at construction and gets its node endpoints separately; the URL argument is gone. A call left at two arguments is a compile error.
  • graph.AddTask(name, url)graph.SetEndpoint(name, url) (loud). Registering a node's endpoint is renamed to read as what it does — binding an already-named node to a URL. The old method name no longer exists.
  • Typed Subflow client on every microservice (additive). Each generated *api/client.go exposes a Subflow type and NewSubflow(flow) constructor. From inside a task body, someapi.NewSubflow(flow).SomeTask(ctx, …) runs another service's task or workflow as an isolated child flow — only the declared inputs cross in and the declared outputs cross back. This is the blessed call path; the Executor remains test-only.
  • flow.Subtask(name, taskURL, in, out) (additive). The task-level sibling of flow.Subgraph: it launches a single task endpoint as an isolated child flow by synthesizing a one-node graph named name, with the same park / re-enter / out-pointer semantics. No graph definition is required.
  • Foreman CreateTask gains a required name argument (loud). CreateTask(ctx, taskURL, …) becomes CreateTask(ctx, name, taskURL, …); name is the node's display name in diagrams and history.
  • OnFlowStopped carries flowKey; FlowOutcome.FlowKey removed (loud). Subscribers read the flow key from the event argument (OnFlowStopped(ctx, flowKey, outcome)) instead of the removed outcome.FlowKey field.
  • act package no longer scaffolded (loud if used). New projects no longer generate the act package. Use frame.Of(ctx).IfActor / ParseActor for imperative claim checks, or keep a typed Actor struct as an optional pattern you own.

New Features

Names over URLs in Graph Construction

A graph is now named when it is built, and its nodes are bound to endpoints separately:

g := workflow.NewGraph("CreditApproval")
g.SetEndpoint("SubmitCreditApplication", creditflowapi.SubmitCreditApplication.URL())
g.SetEndpoint("VerifyCredit", creditflowapi.VerifyCredit.URL())
g.AddTransitionChain("SubmitCreditApplication", "VerifyCredit")
g.SetEntryPoint("SubmitCreditApplication")

Transitions, Goto, and fan-in all reference nodes by the names registered with SetEndpoint, completing the move (begun in v1.39.0 with flow.Goto node names) away from threading endpoint URLs through the graph topology.

The Typed Subflow Client

Every microservice's *api/client.go now carries a Subflow type alongside the Client, MulticastClient, and the test-only Executor. NewSubflow(flow) binds it to the calling task's *workflow.Flow, and each method parks the calling step and re-enters it when the child terminates — returning the endpoint's normal outputs with a yield bool inserted before err:

func (svc *Service) RunIdentityVerification(ctx context.Context, flow *workflow.Flow, applicant Applicant) (identityVerified bool, err error) {
    identityVerified, yield, err := creditflowapi.NewSubflow(flow).IdentityVerification(
        ctx, applicant.Name, applicant.SSN, applicant.Address, applicant.Phone,
    )
    if yield {
        return false, nil // parked, child running
    }
    if err != nil {
        return false, errors.Trace(err)
    }
    return identityVerified, nil
}

A method whose endpoint is a task maps to flow.Subtask; one whose endpoint is a workflow maps to flow.Subgraph. Only the declared inputs cross into the child and only the declared outputs cross back — the caller's flow state is never shared. This is the way one task invokes another unit of work with state isolation; calling the Executor or foremanapi from a task body is not. The boilerplate (the Subflow type, NewSubflow, and the marshalSubflow helper) ships in every client, even services with no tasks or workflows yet, so the typed methods are already there when an endpoint is added.

The flow.Subtask Primitive

flow.Subtask(name, taskURL, in, out) launches a single task endpoint as an isolated child flow — the task-level sibling of flow.Subgraph. The engine synthesizes a trivial one-node graph named name around taskURL, so any task endpoint can be invoked without a graph definition; parking, re-entry, the out-pointer result, and cancel/interrupt propagation are identical to Subgraph:

var out VerifySSNOut
yield, err := flow.Subtask("VerifySSN", creditflowapi.VerifySSN.URL(), VerifySSNIn{SSN: ssn}, &out)

Pass a task URL (not a graph URL); the typed Subflow client makes the distinction automatic, and the raw primitive is the dynamic-only escape hatch. The subflow boundary reads inout throughout, matching Subgraph's in/out symmetry.

Breaking Changes

The upgrade skill handles each of these. Manual migration is not recommended.

  • workflow.NewGraph(name, url)NewGraph(name) (loud). Drop the URL argument from every graph constructor.
  • graph.AddTask(name, url)graph.SetEndpoint(name, url) (loud). Rename the call in every graph builder; arguments are unchanged.
  • foremanapi CreateTask gains a leading name (loud). CreateTask(ctx, taskURL, initialState, opts) becomes CreateTask(ctx, name, taskURL, initialState, opts). The new CreateTaskIn.Name flows through the endpoint, service, generated code, and manifest.
  • OnFlowStopped(ctx, flowKey, outcome); FlowOutcome.FlowKey removed (loud). Subscribers take the flow key from the event argument; the field on the outcome no longer exists.
  • act package no longer generated (loud if referenced). Projects that imported the scaffolded act package switch to frame.Of(ctx).IfActor / ParseActor, or keep a hand-owned typed Actor struct.
  • dwarf bumped v0.4.1v0.5.0. Pulled in by go mod tidy; the graph/task API cleanup above rides this bump.

Migration

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

Get the latest version of Microbus.

The upgrade skill handles the version bump end-to-end:

  1. Bump go.mod to v1.40.0 and go mod tidy (which bumps github.com/microbus-io/dwarf to v0.5.0).
  2. Refresh .claude/rules/, .claude/skills/, and project-wide framework-managed files.
  3. Drop the URL argument from every workflow.NewGraph call and rename graph.AddTask to graph.SetEndpoint.
  4. Thread the name argument through every foremanapi CreateTask call site.
  5. Update OnFlowStopped subscribers to read flowKey from the event argument instead of FlowOutcome.FlowKey.
  6. Retrofit the Subflow client (Subflow type, NewSubflow, marshalSubflow) into every *api/client.go, with per-endpoint methods for services that have task or workflow endpoints.
  7. Remove the scaffolded act package and repoint imperative claim checks at frame.Of(ctx).
  8. Regenerate mocks (genmock) and manifests (genmanifest), then run go vet ./... && go test ./....

Documentation