Workflows
Create and manage Cloudflare Workflows instances from Go.
Cloudflare Workflows run durable, multi-step tasks. The exp/cloudflare/workflows package covers both sides: the Workflow/WorkflowInstance binding for creating, getting, and controlling instances from a Go worker, and hosting a Go type’s run() as the Workflow’s own WorkflowEntrypoint.
Implement a Workflow in Go
A Workflow class is a Runner — a function of (ctx, event, step) — registered by name with workflows.Register, before workers.Serve (or any other blocking call). Unlike a Durable Object, a WorkflowEntrypoint doesn’t need to keep in-memory state alive between invocations: Workflows persists step results and durably replays run() on its own, so a Runner should not rely on Go variables surviving from one run() invocation to the next. (The shim still boots the Wasm module once per WorkflowEntrypoint instance and reuses it if the runtime calls run() on the same instance again.)
import (
"context"
"syscall/js"
"time"
"github.com/syumai/workers-go/exp/cloudflare/workflows"
)
// step1Result is step "generate-message"'s JSON result.
type step1Result struct {
Message string `json:"message"`
}
func runMyWorkflow(ctx context.Context, event *workflows.Event, step *workflows.Step) (js.Value, error) {
step1, err := workflows.DoJSON(step, "generate-message", func(ctx context.Context) (step1Result, error) {
return step1Result{Message: "hello from step 1"}, nil
})
if err != nil {
return js.Value{}, err
}
if err := step.Sleep("wait-a-bit", time.Second); err != nil {
return js.Value{}, err
}
return workflows.ResultJSON(map[string]any{"step1": step1})
}
func main() {
workflows.Register("MyWorkflow", runMyWorkflow)
http.HandleFunc("/", handleIndex)
workers.Serve(nil)
}
event.Payload is a raw, structured-clonable js.Value; event.PayloadJSON(&v) decodes it into a typed Go value the same way WorkflowInstance.SendEvent encodes one on the caller side.
Steps
*workflows.Step wraps the JS WorkflowStep passed into run():
Do(name, fn)/DoWithConfig(name, cfg, fn)runfnunder stepname;fnonly runs if that step hasn’t already completed for this Workflow instance — on a retry/replay, Workflows returns the saved result instead of calling it again.DoJSON/DoJSONWithConfigare generic helpers that JSON-encode/decode a step’s result instead of working with a rawjs.Value, the same waydurableobjects.DurableObjectStorage.GetJSON/PutJSONdo.Sleep(name, d)/SleepUntil(name, t)pause the Workflow durably — it survives a restart/eviction of the underlying Wasm instance.WaitForEvent(name, eventType, timeout)waits for an event sent to this instance (e.g. viaWorkflowInstance.SendEvent) and returns a*StepEvent.
StepConfig (passed to DoWithConfig/DoJSONWithConfig) sets Retries (a *RetryConfig — Limit, Delay, Backoff), Timeout, and Sensitive. Wrap a step’s (or the Runner’s) returned error with workflows.NonRetryable(err) to fail the Workflow instance permanently instead of letting Workflows retry it.
Build and wiring
Pass -workflows to workers-assets-gen (comma-separate multiple classes):
go run github.com/syumai/workers-go/cmd/workers-assets-gen -mode=go -workflows=MyWorkflow
This appends one subclass per name to the generated worker.mjs:
export class MyWorkflow extends GoWorkflowEntrypoint { static goClassName = "MyWorkflow"; }
The name passed here, the class_name in wrangler.toml’s [[workflows]] (below), and the className given to workflows.Register must all match exactly. Hosting a Workflow and calling into one (below) are independent — a Worker can do either, both, or neither.
Declare the binding
[[workflows]]
name = "my-workflow"
binding = "MY_WORKFLOW"
class_name = "MyWorkflow"
Create and get instances
workflows.NewWorkflow resolves the binding by name. Create starts a new instance (an existing id is an error); Get returns a handle to a running one.
wf, err := workflows.NewWorkflow("MY_WORKFLOW")
if err != nil {
return err
}
inst, err := wf.Create(workflows.WorkflowInstanceCreateOptions{
ID: "order-123",
Params: js.ValueOf(map[string]any{"orderId": 123}),
})
if err != nil {
return err
}
fmt.Println(inst.ID())
same, err := wf.Get("order-123")
WorkflowInstanceCreateOptions also accepts a *WorkflowInstanceCreateOptionsRetention and a LocationHint (one of the WorkflowInstanceLocationHint* constants). Params is passed through as a js.Value.
Control an instance
status, err := inst.Status() // InstanceStatus{Status, Error, Output}
err = inst.SendEvent(workflows.WorkflowInstanceSendEventEvent{
Type: "approval",
Payload: js.ValueOf(map[string]any{"approved": true}),
})
err = inst.Pause()
err = inst.Resume()
err = inst.Restart(workflows.WorkflowInstanceRestartOptions{
From: &workflows.WorkflowInstanceRestartOptionsFrom{Name: "step-name"},
})
err = inst.Terminate(workflows.WorkflowInstanceTerminateOptions{Rollback: true})
err = inst.Delete()
Batch operations
CreateBatch starts up to 100 instances at once; DeleteBatch deletes up to 100 and reports per-instance results.
instances, err := wf.CreateBatch([]workflows.WorkflowInstanceCreateOptions{
{ID: "a"},
{ID: "b"},
})
res, err := wf.DeleteBatch([]string{"a", "b"})
for _, d := range res.Deleted {
fmt.Println("deleted", d.ID)
}
for _, e := range res.Errors {
fmt.Println("failed", e.ID, e.Message)
}
Example project
_examples/workflow-go hosts a MyWorkflow Workflow with two steps and a sleep, plus a regular HTTP handler that creates and inspects instances of it.