2026-06-30

Vihren v0.2.0: typed async workflow runs

Vihren v0.2.0 tightens the generated codegen client surface with typed async workflow run handles.

Vihren’s first codegen path made the simple case pleasant: start a workflow, wait for it to finish, and get a typed Go value back. That is still the right shape when the caller wants the workflow result immediately.

Temporal also supports a different mode: start the workflow now, keep a handle to the in-flight execution, and wait for the result later. In the Go SDK, that async start path returns a client.WorkflowRun, which is Temporal’s control handle for the workflow execution.

Vihren v0.2.0 generates the typed version of that handle. Generated async workflow clients now return values such as HelloWorkflowRun instead of raw Temporal client.WorkflowRun values, so the handle still exposes workflow identity while completion stays typed.

The synchronous generated client path is unchanged:

out, err := codegenhello.NewClient(c).HelloWorkflow(ctx,
	client.StartWorkflowOptions{
		ID:        "hello-ada",
		TaskQueue: codegenhello.DefaultTaskQueue,
	},
	codegenhello.GreetingInput{Name: "Ada"},
)

The async path now keeps the result type on the handle:

run, err := codegenhello.NewClient(c).HelloWorkflowAsync(ctx,
	client.StartWorkflowOptions{
		ID:        "hello-ada",
		TaskQueue: codegenhello.DefaultTaskQueue,
	},
	codegenhello.GreetingInput{Name: "Ada"},
)
if err != nil {
	return err
}
out, err := run.Get(ctx)

That replaces the old raw Temporal shape:

var out codegenhello.GreetingOutput
err := run.Get(ctx, &out)

The generated handle still exposes Temporal run identity:

workflowID := run.GetID()
runID := run.GetRunID()

Generated shape

For examples/codegenhello, the relevant generated code now looks like this:

type activityRegistry interface {
	RegisterActivityWithOptions(activityFunc any, options activity.RegisterOptions)
}

type workflowRegistry interface {
	RegisterWorkflowWithOptions(workflowFunc any, options workflow.RegisterOptions)
}

func Register(r worker.Registry, greetingActivities *GreetingActivities) {
	RegisterActivities(r, greetingActivities)
	RegisterWorkflows(r)
}

type HelloWorkflowRun struct {
	run client.WorkflowRun
}

func (run HelloWorkflowRun) GetID() string {
	return run.run.GetID()
}

func (run HelloWorkflowRun) GetRunID() string {
	return run.run.GetRunID()
}

func (run HelloWorkflowRun) Get(ctx context.Context) (GreetingOutput, error) {
	var out GreetingOutput
	if err := run.run.Get(ctx, &out); err != nil {
		return GreetingOutput{}, err
	}
	return out, nil
}

func (cl Client) HelloWorkflow(
	ctx context.Context,
	opts client.StartWorkflowOptions,
	in GreetingInput,
) (GreetingOutput, error) {
	run, err := cl.HelloWorkflowAsync(ctx, opts, in)
	if err != nil {
		return GreetingOutput{}, err
	}
	return run.Get(ctx)
}

There are two compatibility notes:

  • Client.<Workflow> remains the simple start-and-await API.
  • Combined generated Register still accepts Temporal’s worker.Registry.

The breaking change is limited to code that calls <Workflow>Async and expects client.WorkflowRun directly. Change those call sites to use the generated run handle and call out, err := run.Get(ctx).

Install

go get github.com/vihren-dev/[email protected]
go install github.com/vihren-dev/vihren/cmd/[email protected]

Then regenerate:

go generate ./...

The compact example remains the fastest way to try the release:

go run ./examples/codegenhello/cmd/codegenhello-embedded

Back to writing