Skip to content

Workflow Analyzer ​

The check package provides a static analyzer that reports runner code that breaks deterministic replay and calls that misuse the workflow API. The workflowcheck command runs it as a go vet tool.

Install and run ​

sh
go install github.com/eriicafes/go-workflow/cmd/workflowcheck@latest
go vet -vettool=$(which workflowcheck) ./...

Add it to CI next to go vet and your tests. With go-dev, workflowdev.Check runs it before every start or reload.

What it reports ​

Registration ​

Every function passed to workflow.Use must be a valid runner: a named function, or a method value such as f.Report whose receiver holds the runner's dependencies. Method runners' bodies are checked like functions'.

DiagnosticCause
workflow.Use requires one workflow functionUse called with the wrong number of arguments.
workflow.Use requires a named workflow function or methodA closure or function variable passed to Use.
workflow function must be func(context.Context, NamedInput) (Output, error)Wrong runner signature.
workflow input must be a named package typeInput is not a named type.
<Input> must declare Workflow(<Output>)Input type does not embed workflow.Struct[<Output>] or have a matching Workflow method.

Runner bodies ​

DiagnosticFix
workflow body must not start a goroutine, use workflow.Background insteadStart concurrent work with Background.
workflow body must not select on channelsUse the Select functions on promises.
workflow body must not range over a mapIterate a sorted slice of keys.
workflow body calls time.Now, use workflow.Now insteadUse workflow.Now. Also reported for time.Since and time.Until.
workflow body calls time.Sleep, use workflow.Sleep insteadUse workflow.Sleep. Also reported for time.After, time.AfterFunc, time.Tick, time.NewTimer, and time.NewTicker.
workflow body calls <pkg.Func>, use workflow.Step or workflow.Background insteadMove I/O and randomness into a step. Reported for calls into os, io, net/http, database/sql, crypto/rand, math/rand, math/rand/v2, and packages whose path contains uuid.
workflow stream access and webhook responses require a durable stepMove stream access and RespondWith calls into a step.

The analyzer also rejects the context package's cancellation and deadline constructors in workflow bodies. Use durable cancellation instead. Standard context constructors remain valid inside steps. Function literals in a runner, such as Then callbacks and local helpers, follow the same workflow restrictions. Only step closures passed to Step, Background, and the other durable operations may do I/O.

Steps ​

DiagnosticFix
durable step must be a named function or inline closure, not a wrapper or factoryPass a named function (or a literal closure) to Step, Background, and friends, not the result of a call.
durable closure captures <name>, pass workflow-relevant state through its explicit input insteadPass the value as the step's input.

Example ​

go
func Bad(ctx context.Context, input workflows.Report) (workflows.ReportResult, error) {
	started := time.Now()       // use workflow.Now
	go notify(input)            // use workflow.Background
	for k := range input.Tags { // map iteration
		_ = k
	}

	body, _ := os.ReadFile("template.txt") // I/O outside a step
	out, err := workflow.Step(ctx, func(ctx context.Context, _ struct{}) (string, error) {
		return render(body, started), nil // captures body and started
	}, struct{}{})
	return workflows.ReportResult{Text: out}, err
}

Using the analyzer as a library ​

check.Analyzer is a standard *analysis.Analyzer, so it plugs into multichecker, singlechecker, or analysistest:

go
import (
	"github.com/eriicafes/go-workflow/check"
	"golang.org/x/tools/go/analysis/singlechecker"
)

func main() { singlechecker.Main(check.Analyzer) }

Released under the MIT License.