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
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'.
| Diagnostic | Cause |
|---|---|
workflow.Use requires one workflow function | Use called with the wrong number of arguments. |
workflow.Use requires a named workflow function or method | A 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 type | Input 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
| Diagnostic | Fix |
|---|---|
workflow body must not start a goroutine, use workflow.Background instead | Start concurrent work with Background. |
workflow body must not select on channels | Use the Select functions on promises. |
workflow body must not range over a map | Iterate a sorted slice of keys. |
workflow body calls time.Now, use workflow.Now instead | Use workflow.Now. Also reported for time.Since and time.Until. |
workflow body calls time.Sleep, use workflow.Sleep instead | Use 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 instead | Move 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 step | Move 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
| Diagnostic | Fix |
|---|---|
durable step must be a named function or inline closure, not a wrapper or factory | Pass 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 instead | Pass the value as the step's input. |
Example
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:
import (
"github.com/eriicafes/go-workflow/check"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() { singlechecker.Main(check.Analyzer) }