Documentation
¶
Overview ¶
Package workflow defines durable, typed workflows built from retried steps, signals, and sleeps, run by a swappable engine driver. A Workflow is a function that orchestrates Step values, waits for Signal values, sleeps, and starts other workflows; a Driver runs it and decides how it survives restarts. A Client starts runs and talks to them. Workflow signals are unrelated to the payload-less signals of package arkoren.dev/core/event.
var SendWelcome = workflow.NewStep("send-welcome", sendWelcome, func(step *workflow.StepBuilder) {
step.Retry(5)
step.Backoff(time.Second, 2, time.Minute)
})
var EmailConfirmed = workflow.NewSignal[string]("email-confirmed")
var Onboard = workflow.New("onboard", func(c *workflow.Context, user User) (string, error) {
if _, err := c.Step(SendWelcome, user); err != nil {
return "", err
}
token, err := c.Wait(EmailConfirmed, workflow.Timeout(72*time.Hour))
if errors.Is(err, workflow.ErrTimeout) {
return "expired", nil
}
return token, err
}, func(definition *workflow.Builder) {
definition.MaxDuration(96 * time.Hour)
})
A worker process registers the definitions with the driver:
registry, err := workflow.Handlers(Onboard, SendWelcome)
Registry.Workflows and Registry.Steps return eager collection snapshots sorted by name. Their records are detached from registration maps, not live queries; functions and their captures remain shared, not cloned by wrappers.
A server process only needs a client:
client := workflow.NewClient(driver) run, err := client.Start(ctx, Onboard, "user-42", user) err = client.Signal(ctx, Onboard, "user-42", EmailConfirmed, "abc") result, err := run.Result(ctx)
Determinism ¶
A driver may replay a workflow function from the start to rebuild its state. The function must therefore not read the clock, random numbers, the network, or global state directly. Use Context.Now, Context.UUID, and steps, which a driver records. Inputs and outputs must round-trip through the Codec, which is encoding/json/v2 by default.
Signals ¶
Context.Wait returns the first value of a signal and remembers it: a later Wait or Context.Select on the same signal in the same run sees the same value. A signal sent before the workflow waits for it is not lost.
Child ownership ¶
Context.Child waits for a newly created or existing run. Only the caller that creates the child owns its cancellation: parent cancellation or timeout stops newly created children. Joining an existing run ignores the new input and acquires no ownership; cancellation or timeout ends only the joining wait. Drivers must preserve that atomic decision across replay or reject Child with ErrUnsupported before submitting work. Use Context.Spawn for intentionally detached work, not as a substitute for unsupported Child semantics.
Errors ¶
A failure of a run reaches Run.Result as an error. ErrCanceled, ErrTimeout, and any *Error keep their kind across the wire, so errors.Is works in the process that started the run. A step that returns Permanent is not retried.
Failure messages ¶
A failing run stores the text of its error in the engine's history, and Run.Result returns it to every caller, in any process. Error text is persisted and exported, so keep secrets and personal data out of the messages of errors a workflow function or step returns; wrap them with NewError and a generic message when the underlying error may carry values.
Registration ¶
Definitions are closures registered by name in every worker process. New and NewStep publish immutable definitions from Builder and StepBuilder callbacks. Settings return the same draft, and the first error (an invalid name, a nil function, a bad duration) is kept and reported by Workflow.Err and Step.Err and returned by Handlers as ErrInvalidName, ErrNilFunc, or ErrInvalidOptions. Callbacks run directly once, synchronously in order on one fresh draft; supplied nil entries and application panics propagate. Expanded nil or empty callback slices omit edits. Publication detaches the settings, so retained drafts cannot change definitions. Registration captures the name, function, and settings independently of the published pointer. Names are 1-64 lowercase letters, digits, or hyphens with no hyphen at either end, and share one namespace for workflows and steps. Run ids are free-form but not empty and free of ASCII control characters, else ErrInvalidID. A workflow must set Builder.MaxDuration; omission is rejected only at registration. Zero workflows and steps are invalid and report ErrInvalidName. Client.Start, Context.Step, Context.Child, and Context.Spawn reject stored declaration errors before encoding or dispatch, even when a worker registered a valid definition under that same name. Deriving a zero step preserves its declaration error. To change the settings of one step call, use Step.Derive:
singleAttempt := SendWelcome.Derive(func(step *workflow.StepBuilder) {
step.Retry(1)
})
_, err := workflowContext.Step(singleAttempt, user)
Derive preserves the name, function, inherited policy, and first error. Backoff initial zero requests immediate step retries without a deliberate positive delay. An engine unable to express the completed multi-attempt policy returns ErrUnsupported before scheduling or executing the affected step. It must not substitute a positive default or retry the whole workflow instead. Single attempts and positive backoff retain their normal support. Provider duration limits remain provider constraints; this defines no new core maximum. Names, JSON payloads, and an explicit serialized trace carrier cross the driver; context.Context itself never crosses processes.
Events ¶
NewClient uses ClientBuilder callbacks for Codec, Telemetry, and Events, separate from definition and per-start options. Callbacks run once in order on one fresh draft; nil entries panic and nil/empty slice expansion omits edits. Publication detaches retained roots, not shared codecs or event buses. A final nil codec selects JSON and must match the worker registry's encoding.
With ClientBuilder.Events, a Client publishes RunStarted ("workflow.run.started") after Client.Start starts a run, RunSignaled ("workflow.run.signaled") after Client.Signal delivers a signal, and RunCanceled ("workflow.run.canceled") after Run.Cancel on a run from that client. Payloads carry the workflow name, the run ID, and for signals the signal name; never inputs, outputs, metadata, or signal values. Failed calls publish nothing. These are local successful-action notifications, not a distributed engine completion feed. Polling Result or Status does not publish terminal events.
Telemetry ¶
ClientBuilder.Telemetry observes start, signal, cancel, status, and result calls. Result duration is wait latency, not execution time. Configure worker observations separately through NewRegistry and RegistryBuilder.Telemetry:
registry, err := workflow.NewRegistry([]workflow.Definition{Onboard, SendWelcome},
func(worker *workflow.RegistryBuilder) {
worker.Telemetry(observations)
})
if err != nil {
return err
}
_ = registry.Steps()
Nil telemetry follows the current process default per call/attempt. Actual StepHandler.Handle invocations produce workflow step_attempt spans, a count, and a seconds histogram. Each retry is a new attempt; replayed orchestration, suspension, sleeps, and recorded step results produce no attempt observations. The step context supports dependency child spans. No payload, signal value, output, metadata, run ID, or raw error text enters automatic telemetry; metric labels contain only operation, outcome, and classified error type.
StartOptions.Trace carries propagation independently of application metadata. Drivers forward it as deterministic data to children/spawns where supported, then Registry.NewTraceContext extracts it onto each native attempt context. Signals do not replace this start parent. Configure an exporter in every pod and use a common backend for distributed traces; local event buses and memory capture do not aggregate remote pods. Only trusted submitters should be able to supply carriers. No terminal engine subscription exists in this contract.
Index ¶
- Variables
- func DecodeError(data []byte) error
- func DecodeValue(codec Codec, data []byte, v any) error
- func EncodeError(err error) []byte
- func EncodeValue(codec Codec, v any) ([]byte, error)
- func Permanent(err error) error
- type Builder
- type BuilderFunc
- type Case
- type Client
- func (client *Client) Driver() Driver
- func (client *Client) DriverAs[T any]() (T, bool)
- func (client *Client) Run[In, Out any](w *Workflow[In, Out], id string) *Run[Out]
- func (client *Client) Signal[In, Out, T any](ctx context.Context, w *Workflow[In, Out], id string, s Signal[T], v T) (err error)
- func (client *Client) Start[In, Out any](ctx context.Context, w *Workflow[In, Out], id string, in In, ...) (_ *Run[Out], err error)
- type ClientBuilder
- type ClientBuilderFunc
- type Codec
- type Context
- func (workflowContext *Context) Child[In, Out any](w *Workflow[In, Out], id string, in In) (Out, error)
- func (workflowContext *Context) Done() <-chan struct{}
- func (workflowContext *Context) Err() error
- func (workflowContext *Context) ID() string
- func (workflowContext *Context) Logger() *slog.Logger
- func (workflowContext *Context) Name() string
- func (workflowContext *Context) Now() time.Time
- func (workflowContext *Context) Raw() any
- func (workflowContext *Context) Select(cases ...Case) error
- func (workflowContext *Context) Sleep(d time.Duration) error
- func (workflowContext *Context) Spawn[In, Out any](w *Workflow[In, Out], id string, in In) (string, error)
- func (workflowContext *Context) Step[In, Out any](s *Step[In, Out], in In) (Out, error)
- func (workflowContext *Context) UUID() string
- func (workflowContext *Context) Wait[T any](s Signal[T], opts ...WaitOption) (T, error)
- type Definition
- type Driver
- type Error
- type JSON
- type RawCase
- type Registry
- func (registry *Registry) NewTraceContext(ctx context.Context, carrier map[string]string) context.Context
- func (registry *Registry) Step(name string) (StepHandler, bool)
- func (registry *Registry) Steps() collection.Slice[StepHandler]
- func (registry *Registry) Workflow(name string) (WorkflowHandler, bool)
- func (registry *Registry) Workflows() collection.Slice[WorkflowHandler]
- type RegistryBuilder
- type RegistryBuilderFunc
- type Run
- type RunEvent
- type RunSignaledEvent
- type Runtime
- type Signal
- type StartOption
- type StartOptions
- type Status
- type Step
- type StepBuilder
- type StepBuilderFunc
- type StepHandler
- type StepOptions
- type WaitOption
- type Workflow
- type WorkflowHandler
- type WorkflowOptions
Constants ¶
This section is empty.
Variables ¶
var ErrAlreadyStarted = NewError("workflow.already_started", "workflow: already started")
ErrAlreadyStarted reports a run that already exists under the same name and identifier. Client.Start also returns the existing run handle for idempotent callers.
run, err := client.Start(ctx, Onboard, "user-42", user)
if err != nil && !errors.Is(err, workflow.ErrAlreadyStarted) {
return err
}
_ = run.ID()
var ErrCanceled = NewError("workflow.canceled", "workflow: canceled")
ErrCanceled reports a run, step, or wait that was canceled. Its kind survives the driver wire representation.
if errors.Is(err, workflow.ErrCanceled) {
return nil // Cancellation was the requested outcome.
}
var ErrDuplicateName = errors.New("duplicate workflow name")
ErrDuplicateName reports two definitions registered under one name. Workflows and steps share the registry's name namespace.
_, err := workflow.Handlers(Onboard, Onboard)
if errors.Is(err, workflow.ErrDuplicateName) {
return err
}
var ErrInvalidID = errors.New("invalid run id")
ErrInvalidID reports an empty run identifier, one containing ASCII control characters, or a child that uses the identifier of its own run. It rejects unsafe identities before a client submits work.
if _, err := client.Start(ctx, Onboard, "", user); errors.Is(err, workflow.ErrInvalidID) {
return err
}
var ErrInvalidName = errors.New("invalid name")
ErrInvalidName reports a name that is empty or has characters outside [a-z0-9-], or is otherwise rejected by the shared identifier rule. The definition constructors keep it on the value; Handlers returns it.
definition := workflow.New("Invalid", onboard)
if errors.Is(definition.Err(), workflow.ErrInvalidName) {
return definition.Err()
}
var ErrInvalidOptions = errors.New("invalid options")
ErrInvalidOptions reports an out-of-range duration in workflow or step settings. Later declaration edits cannot clear the first invalid setting.
definition := workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(-time.Second)
})
if errors.Is(definition.Err(), workflow.ErrInvalidOptions) {
return definition.Err()
}
var ErrMissingMaxDuration = errors.New("workflow requires MaxDuration")
ErrMissingMaxDuration reports a workflow registered without Builder.MaxDuration. Err deliberately leaves this required worker registration check to Handlers.
_, err := workflow.Handlers(workflow.New("onboard", onboard))
if errors.Is(err, workflow.ErrMissingMaxDuration) {
return err
}
var ErrNilFunc = errors.New("nil function")
ErrNilFunc reports a definition built with a nil function, or a nil definition passed to Handlers. This is a declaration error, not construction-callback misuse.
definition := workflow.NewStep[string, string]("charge", nil)
if errors.Is(definition.Err(), workflow.ErrNilFunc) {
return definition.Err()
}
var ErrNoCases = errors.New("select without cases")
ErrNoCases reports a Context.Select without cases. Supply at least one signal or timer alternative before waiting.
if err := workflowContext.Select(); errors.Is(err, workflow.ErrNoCases) {
return "", err
}
var ErrNotFound = NewError("workflow.not_found", "workflow: not found")
ErrNotFound reports an unknown workflow or run, or a signal sent to a run that is not running.
if errors.Is(err, workflow.ErrNotFound) {
return nil // The run has already disappeared or ended.
}
var ErrPermanent = errors.New("permanent failure")
ErrPermanent matches, through errors.Is, an error marked with Permanent. A step that fails with such an error stops at once even when attempts remain.
if errors.Is(err, workflow.ErrPermanent) {
return err // Do not apply an application-level retry around the step.
}
var ErrTimeout = NewError("workflow.timeout", "workflow: timeout")
ErrTimeout reports an expired wait, step timeout, or workflow Builder.MaxDuration. Match the error rather than its persisted human-readable text.
if errors.Is(err, workflow.ErrTimeout) {
return workflow.NewError("onboarding.expired", "confirmation expired")
}
var ErrUnsupported = NewError("workflow.unsupported", "workflow: unsupported by driver")
ErrUnsupported reports an operation the driver cannot perform. Select a compatible engine rather than retrying the same unsupported call.
if errors.Is(err, workflow.ErrUnsupported) {
return fmt.Errorf("this workflow needs a different engine: %w", err)
}
var RunCanceled = event.NewTopic[RunEvent]("workflow.run.canceled")
RunCanceled is published when Run.Cancel has asked the driver to cancel a run. A failed cancel publishes nothing. Canceling a finished run is not an error and publishes the event as well. The observation is not a distributed subscription to engine terminal states.
unsubscribe, err := bus.Listen(workflow.RunCanceled, func(_ context.Context, notice workflow.RunEvent) error {
fmt.Println(notice.Workflow)
return nil
})
if err != nil {
return err
}
defer unsubscribe()
var RunSignaled = event.NewTopic[RunSignaledEvent]("workflow.run.signaled")
RunSignaled is published when Client.Signal has delivered a signal to a run. A failed signal publishes nothing. It never includes the signal payload or replaces a run's trace parent.
unsubscribe, err := bus.Listen(workflow.RunSignaled, func(_ context.Context, notice workflow.RunSignaledEvent) error {
fmt.Println(notice.Signal)
return nil
})
if err != nil {
return err
}
defer unsubscribe()
var RunStarted = event.NewTopic[RunEvent]("workflow.run.started")
RunStarted is published when Client.Start has started a run. A start that fails, including one that finds the run already started, publishes nothing. Configure ClientBuilder.Events with the same bus before observing commands.
unsubscribe, err := bus.Listen(workflow.RunStarted, func(_ context.Context, notice workflow.RunEvent) error {
fmt.Println(notice.Workflow)
return nil
})
if err != nil {
return err
}
defer unsubscribe()
Functions ¶
func DecodeError ¶
DecodeError restores an error from EncodeError. The result is an *Error that matches the sentinels such as ErrCanceled and ErrTimeout through errors.Is, and is marked with Permanent when the original was. Data that is not an envelope becomes an *Error with the data as message. DecodeError of empty data is nil. Decoded messages remain persisted application data, not safe telemetry text.
failure := workflow.DecodeError(workflow.EncodeError(workflow.ErrTimeout))
if errors.Is(failure, workflow.ErrTimeout) {
return failure
}
func DecodeValue ¶
DecodeValue decodes an envelope made by EncodeValue into v, which is a pointer. Empty data leaves v unchanged. Data that is not an envelope, or has no "v" member, is an error. A nil codec uses JSON. JSON null is a present value, including in whitespace-formatted envelopes.
var greeting string
if err := workflow.DecodeValue(nil, []byte(`{"v":"welcome"}`), &greeting); err != nil {
return err
}
fmt.Println(greeting)
func EncodeError ¶
EncodeError encodes err as {"kind","message","permanent"} so a driver can carry it across the wire. The kind and message come from the first *Error in the chain, so wrapping context is dropped. Without one, context.Canceled maps to the kind of ErrCanceled and context.DeadlineExceeded to the kind of ErrTimeout. EncodeError(nil) is nil. Only the serialized kind and message cross the boundary, not arbitrary Go error identities. Keep secrets out of messages before calling this function.
failure := workflow.NewError("billing.card-declined", "payment declined")
encoded := workflow.EncodeError(workflow.Permanent(failure))
_ = encoded // Store or transport this envelope, not the original cause.
func EncodeValue ¶
EncodeValue encodes v with codec inside the envelope {"v": ...}. A nil codec uses JSON. The output of the codec must be valid JSON; it is not re-validated. Empty codec output is carried as JSON null; the returned envelope owns its bytes independently of the codec's buffer.
data, err := workflow.EncodeValue(nil, "welcome")
if err != nil {
return err
}
_ = data // {"v":"welcome"}
func Permanent ¶
Permanent marks err as non-retryable: a step that returns it fails at once even when attempts remain. Check for the mark with errors.Is(err, ErrPermanent). Permanent(nil) is nil.
Example:
return Receipt{}, workflow.Permanent(errors.New("card declined"))
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder configures one workflow declaration before New publishes its immutable snapshot. Retaining this draft does not permit later changes to the published workflow. Settings preserve the first declaration error.
Example:
workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(24 * time.Hour)
})
func (*Builder) Description ¶
Description sets free text that documents the workflow, replacing any earlier description. Empty text clears it. It returns the same draft without clearing an earlier validation error.
Example:
workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(time.Hour)
definition.Description("Welcomes a new user.")
})
func (*Builder) MaxDuration ¶
MaxDuration bounds the whole run. It is required: Handlers fails for a workflow without it. When it expires the run fails with ErrTimeout. A value of zero or less is an error. It returns the same draft and preserves any earlier declaration error.
Example:
workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(24 * time.Hour)
})
func (*Builder) Retention ¶
Retention sets how long the engine keeps a finished run. Zero uses the engine default and a negative value is an error. It returns the same draft and preserves the first declaration error.
Example:
workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(time.Hour)
definition.Retention(7 * 24 * time.Hour)
})
type BuilderFunc ¶
type BuilderFunc = func(*Builder)
BuilderFunc edits the fresh workflow declaration draft used by New. Callbacks run synchronously once in order, even for invalid declarations; supplied nil callbacks panic. Set MaxDuration before registering the result.
Example:
policy := workflow.BuilderFunc(func(definition *workflow.Builder) {
definition.MaxDuration(time.Minute)
})
onboard := workflow.New("onboard", onboardUser, policy)
if err := onboard.Err(); err != nil {
return err
}
type Case ¶
type Case struct {
// contains filtered or unexported fields
}
Case is one alternative of Context.Select. Create it with On or After. Case callbacks execute synchronously on the workflow's orchestration path and must follow the same replay/determinism restrictions as its body.
approved := workflow.NewSignal[bool]("approved")
var accepted bool
choice := workflow.On(approved, func(value bool) {
accepted = value
})
if err := workflowContext.Select(choice); err != nil {
return "", err
}
_ = accepted
func After ¶
After returns a case that calls fn once d has passed. fn must not be nil; After panics with ErrNilFunc otherwise. A nonpositive delay becomes an immediately eligible timer alternative; the engine still arbitrates it against available signals.
var expired bool
if err := workflowContext.Select(workflow.After(time.Minute, func() {
expired = true
})); err != nil {
return "", err
}
_ = expired
func On ¶
On returns a case that calls fn with the value of signal s. fn must not be nil; On panics with ErrNilFunc otherwise. Decoding errors stop Select before fn is invoked. The first remembered value for the signal is reused by later waits and selections in the same run.
approved := workflow.NewSignal[bool]("approved")
var accepted bool
choice := workflow.On(approved, func(value bool) {
accepted = value
})
if err := workflowContext.Select(choice); err != nil {
return "", err
}
_ = accepted
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client starts workflows and talks to their runs through a Driver. Create it with NewClient. A Client is safe for concurrent use. The driver and codec remain shared; their lifecycle belongs to their owner.
client := workflow.NewClient(driver)
run, err := client.Start(ctx, Onboard, "user-42", user)
if err != nil {
return err
}
_ = run.ID()
func NewClient ¶
func NewClient(driver Driver, configure ...ClientBuilderFunc) *Client
NewClient returns a client using driver without invoking handlers or doing I/O. Callbacks run directly once in order on one fresh draft. Nil entries panic; expanded nil/empty slices omit edits. Retaining a draft cannot change clients. A final nil codec uses JSON; registry compatibility remains the caller's job.
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Codec(workflow.JSON{})
})
func (*Client) Driver ¶
Driver returns the underlying Driver. Raw operations bypass the client's typed encoding and action observations.
backend := client.Driver()
status, err := backend.Status(ctx, "onboard", "user-42")
if err != nil {
return err
}
_ = status
func (*Client) DriverAs ¶
DriverAs returns the first driver of type T in the chain of the underlying driver. A driver that wraps another exposes it with an Unwrap() Driver method. The returned value has the original driver's ownership; lookup does no I/O.
backend, ok := client.DriverAs[*memoryworkflow.Driver]()
if ok {
_ = backend // The composition owner remains responsible for Stop.
}
func (*Client) Run ¶
Run returns the handle of run id of workflow w without any I/O. Its methods fail with ErrInvalidID when id is empty or contains control characters. Obtaining a handle does not establish that the run exists.
run := client.Run(Onboard, "user-42")
status, err := run.Status(ctx)
if err != nil {
return err
}
_ = status
func (*Client) Signal ¶
func (client *Client) Signal[In, Out, T any](ctx context.Context, w *Workflow[In, Out], id string, s Signal[T], v T) (err error)
Signal sends value v of signal s to run id of workflow w. It returns ErrNotFound when the run does not exist or has ended. Telemetry observes delivery acceptance only. A signal does not replace the durable run's start parent, and its value is never recorded.
if err := client.Signal(ctx, Onboard, "user-42", Approved, approval); err != nil {
return err
}
func (*Client) Start ¶
func (client *Client) Start[In, Out any](ctx context.Context, w *Workflow[In, Out], id string, in In, opts ...StartOption) (_ *Run[Out], err error)
Start begins run id of workflow w with input in and returns its handle. When the run already exists it returns the handle of the existing run together with an error that satisfies errors.Is with ErrAlreadyStarted, so callers may ignore that error to start idempotently. One observation includes validation and encoding; ErrAlreadyStarted is an expected telemetry outcome. The start span is injected into a separate carrier, never into application metadata or payload values. Invalid declarations fail with Workflow.Err before encoding or driver I/O.
run, err := client.Start(ctx, Onboard, "user-42", user)
if err != nil && !errors.Is(err, workflow.ErrAlreadyStarted) {
return err
}
_ = run.ID()
type ClientBuilder ¶
type ClientBuilder struct {
// contains filtered or unexported fields
}
ClientBuilder edits client-only settings, not workflow definitions or starts. NewClient copies the final draft while keeping codecs and event buses shared.
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Codec(workflow.JSON{})
})
func (*ClientBuilder) Codec ¶
func (client *ClientBuilder) Codec(value Codec) *ClientBuilder
Codec replaces the payload codec. A final nil selects JSON at construction; use the same codec for the worker Registry. It returns the same draft.
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Codec(workflow.JSON{})
})
func (*ClientBuilder) Events ¶
func (client *ClientBuilder) Events(value *event.Bus) *ClientBuilder
Events selects the shared bus for run outcomes. Nil publishes nothing; it does not affect per-start metadata. It returns the same draft.
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Events(app.Events())
})
func (*ClientBuilder) Telemetry ¶
func (client *ClientBuilder) Telemetry(value *telemetry.Telemetry) *ClientBuilder
Telemetry selects client-call observation and start-carrier injection. It does not configure workers in this or another process. Nil follows the process default on each call. The service remains shared after publication. Panics still propagate unchanged; an interrupted call closes its started observation as workflow.panic without recording the panic value or type.
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Telemetry(observations)
})
type ClientBuilderFunc ¶
type ClientBuilderFunc = func(*ClientBuilder)
ClientBuilderFunc edits the fresh construction draft used by NewClient. Callbacks run synchronously once in order; retaining a draft cannot replace the published client's settings. A supplied nil callback panics.
Example:
policy := workflow.ClientBuilderFunc(func(client *workflow.ClientBuilder) {
client.Codec(workflow.JSON{})
})
client := workflow.NewClient(driver, policy)
type Codec ¶
type Codec interface {
// Marshal encodes v as JSON.
Marshal(v any) ([]byte, error)
// Unmarshal decodes JSON data into v, which is a pointer.
Unmarshal(data []byte, v any) error
}
Codec converts values to and from the bytes that cross a driver. The output of Marshal must be valid JSON, because payloads are wrapped in a JSON object envelope. Implementations remain shared by clients and registered handlers and must support their concurrent use. Use the same encoding in both processes.
var codec workflow.Codec = workflow.JSON{}
client := workflow.NewClient(driver, func(client *workflow.ClientBuilder) {
client.Codec(codec)
})
_ = client.Driver()
type Context ¶
type Context struct {
// contains filtered or unexported fields
}
Context is what a workflow function uses to run steps, wait for signals, sleep, and start other workflows. It wraps the Runtime of the driver. Do not use the clock, random numbers, or the network directly in a workflow function; use Now, UUID, and steps so a driver can replay it. Drivers supply the Context to a registered workflow handler; callers do not construct it directly. Side effects belong in registered steps.
definition := workflow.New("welcome", func(ctx *workflow.Context, user string) (string, error) {
if err := ctx.Sleep(time.Minute); err != nil {
return "", err
}
return user, nil
}, func(definition *workflow.Builder) {
definition.MaxDuration(time.Hour)
})
_ = definition.Err()
func (*Context) Child ¶
func (workflowContext *Context) Child[In, Out any](w *Workflow[In, Out], id string, in In) (Out, error)
Child runs workflow w as run id and waits for its output. If this call creates the run, parent cancellation or MaxDuration also stops that child. When a run with the same id already exists, Child joins it and ignores in: joining acquires no cancellation ownership. Parent cancellation or MaxDuration ends only that wait and must not stop the existing run, even if another parent created it. Creation versus joining must be decided atomically and retained across replay. A driver unable to preserve these guarantees returns ErrUnsupported before submitting child work. An empty id, or the id of the calling run itself, fails with ErrInvalidID. Workflow.Err is checked before encoding or asking the runtime to start/join.
Example:
receipt, err := workflowContext.Child(fulfill, "order-42", order)
if err != nil {
return "", err
}
func (*Context) Done ¶
func (workflowContext *Context) Done() <-chan struct{}
Done is closed when the run is canceled or timed out. Poll it with a non-blocking select; blocking on it is not replay-safe on every engine. Durable calls such as Sleep and Wait return early on cancellation.
Example:
select {
case <-workflowContext.Done():
return "", workflowContext.Err()
default:
}
func (*Context) Err ¶
Err returns why Done is closed, ErrCanceled or ErrTimeout, or nil.
Example:
if err := workflowContext.Err(); err != nil {
return "", err
}
func (*Context) Logger ¶
Logger returns the logger of the run.
Example:
workflowContext.Logger().Info("processing order")
func (*Context) Now ¶
Now returns the time of the run. Use it instead of time.Now.
Example:
startedAt := workflowContext.Now()
func (*Context) Raw ¶
Raw returns the native context of the engine behind the driver, for example a Temporal workflow.Context. Its type depends on the driver.
Example:
nativeContext := workflowContext.Raw()
func (*Context) Select ¶
Select blocks until one case fires and runs its function. A signal case whose signal was already received fires at once, in case order; otherwise the first signal or timer to fire wins. A signal resolves once per run: a signal case records the value, and later Context.Wait or Select calls for that signal return the same value without waiting for a new one. A timer case with a zero or negative delay fires after the shortest delay.
Example:
confirmed := workflow.NewSignal[string]("confirmed")
var result string
err := workflowContext.Select(workflow.On(confirmed, func(token string) {
result = token
}), workflow.After(time.Minute, func() {
result = "expired"
}))
if err != nil {
return "", err
}
return result, nil
func (*Context) Sleep ¶
Sleep pauses the run for d. It returns early with the error of Context.Err when the run ends.
Example:
if err := workflowContext.Sleep(time.Minute); err != nil {
return "", err
}
func (*Context) Spawn ¶
func (workflowContext *Context) Spawn[In, Out any](w *Workflow[In, Out], id string, in In) (string, error)
Spawn starts workflow w as run id without waiting and returns id. The run outlives the parent. A run that already exists is not an error, so a replay of the parent spawns once; use Client.Run with the id to query it. A driver that cannot start runs from a workflow returns ErrUnsupported. An empty id, or the id of the calling run itself, fails with ErrInvalidID. Workflow.Err is checked before encoding or submitting detached work.
Example:
childID, err := workflowContext.Spawn(sendReceipt, "receipt-42", order)
if err != nil {
return "", err
}
func (*Context) Step ¶
Step runs step s with input in and returns its output. To change the settings for one call, use Step.Derive; it keeps the name. A step with a non-nil Step.Err fails without running. A failure that the step marked with Permanent is returned without further retries.
Example:
singleAttempt := charge.Derive(func(step *workflow.StepBuilder) {
step.Retry(1)
})
receipt, err := workflowContext.Step(singleAttempt, order)
if err != nil {
return "", err
}
func (*Context) UUID ¶
UUID returns a new random identifier. Use it instead of random numbers.
Example:
childID := workflowContext.UUID()
func (*Context) Wait ¶
func (workflowContext *Context) Wait[T any](s Signal[T], opts ...WaitOption) (T, error)
Wait blocks until signal s arrives and returns its value. A signal resolves once per run: the first value received per signal name is kept, and every later Wait or Select for the same signal returns that value at once, even if the sender sent more. Timeout bounds the wait and yields ErrTimeout; zero or no Timeout waits without limit.
Example:
confirmed := workflow.NewSignal[string]("confirmed")
token, err := workflowContext.Wait(confirmed, workflow.Timeout(time.Minute))
if err != nil {
return "", err
}
type Definition ¶
type Definition interface {
// contains filtered or unexported methods
}
Definition is a workflow or step that a Driver can run. *Workflow and *Step implement it; pass them to Handlers. The interface is sealed so registry validation and typed decoding remain owned by this package rather than arbitrary caller implementations.
definitions := []workflow.Definition{Onboard, SendWelcome}
registry, err := workflow.NewRegistry(definitions)
if err != nil {
return err
}
_ = registry.Workflows()
type Driver ¶
type Driver interface {
// Start begins run id of the workflow called name with input in. It
// returns [ErrAlreadyStarted] when the run exists, finished or not, until
// the engine forgets it after its retention, and [ErrNotFound] when the
// workflow is not registered. Concurrent starts of one id have exactly one
// winner.
// Trace is a separate, owned propagation carrier: retain a copy, carry it
// through durable child/spawn scheduling, and extract it only at actual
// step attempts. Never merge it with Metadata or retain the caller context.
//
// err := driver.Start(ctx, "onboard", "user-42", input, workflow.StartOptions{})
// if err != nil && !errors.Is(err, workflow.ErrAlreadyStarted) {
// return err
// }
Start(ctx context.Context, name, id string, in []byte, o StartOptions) error
// Result blocks until the run ends, or until ctx is done, and returns the
// encoded output or the failure of the run. [ErrNotFound] for an unknown
// run.
Result(ctx context.Context, name, id string) ([]byte, error)
// Status returns the state of the run: [Running] while it has not ended,
// [Failed] after an error or [ErrTimeout], [Canceled] after a cancel.
// [ErrNotFound] for an unknown run.
Status(ctx context.Context, name, id string) (Status, error)
// Cancel stops the run. Its result becomes [ErrCanceled]. Canceling a
// finished run is not an error; an unknown run is [ErrNotFound].
Cancel(ctx context.Context, name, id string) error
// Signal delivers a named value to a running run. It returns
// [ErrNotFound] for an unknown or finished run.
Signal(ctx context.Context, name, id, signal string, v []byte) error
}
Driver runs workflows. It works on bytes: names, run identifiers, and payloads made by EncodeValue. Failures cross it as errors that EncodeError and DecodeError can carry; a driver returns the sentinels such as ErrNotFound and ErrAlreadyStarted for the matching conditions. Applications normally use Client; raw callers own encoding and engine lifecycle. Driver methods must preserve error identity through wrapping.
client := workflow.NewClient(driver)
run, err := client.Start(ctx, Onboard, "user-42", user)
if err != nil {
return err
}
_ = run.ID()
type Error ¶
type Error struct {
// Kind classifies the failure, for example "billing.card_declined".
Kind string
// Message describes the failure for people.
Message string
}
Error is a failure with a stable Kind that survives the wire. Drivers carry it with EncodeError and DecodeError, and errors.Is matches two errors of the same non-empty Kind. Message text crosses process boundaries; use a stable, nonsensitive message rather than embedding credentials or input values in a failure.
declined := workflow.NewError("billing.card-declined", "payment declined")
if errors.Is(err, declined) {
return declined
}
func NewError ¶
NewError returns an Error with the given kind and message. Applications use it for failures that callers must recognize with errors.Is after the error crossed a process boundary. Kinds that start with "workflow." are reserved for the sentinels of this package; do not use them. Empty kinds do not match other errors by kind. NewError does not retain an underlying cause; it deliberately publishes only the supplied safe message.
declined := workflow.NewError("billing.card-declined", "payment declined")
return Receipt{}, declined
func (*Error) Error ¶
Error returns the message, or the kind when the message is empty. It adds no wrapping context, which keeps the cross-process failure readable.
failure := workflow.NewError("billing.card-declined", "payment declined")
fmt.Println(failure.Error()) // payment declined
func (*Error) Is ¶
Is reports whether target is an *Error with the same non-empty Kind. Use errors.Is to find matching failures through wrapping and wire decoding.
target := workflow.NewError("billing.card-declined", "payment declined")
received := workflow.DecodeError(workflow.EncodeError(target))
fmt.Println(errors.Is(received, target)) // true
type JSON ¶
type JSON struct{}
JSON is the default Codec. It uses encoding/json/v2. Its zero value is ready to use and holds no mutable state.
codec := workflow.JSON{}
data, err := codec.Marshal("welcome")
if err != nil {
return err
}
_ = data
func (JSON) Marshal ¶
Marshal encodes v with encoding/json/v2. Unsupported values return the JSON encoder's error rather than a payload.
data, err := (workflow.JSON{}).Marshal(map[string]string{"greeting": "welcome"})
if err != nil {
return err
}
_ = data
func (JSON) Unmarshal ¶
Unmarshal decodes data with encoding/json/v2. v must be an appropriate nonnil destination pointer; invalid JSON and incompatible destinations return the decoder's error.
var greeting string
if err := (workflow.JSON{}).Unmarshal([]byte(`"welcome"`), &greeting); err != nil {
return err
}
fmt.Println(greeting)
type RawCase ¶
type RawCase struct {
// Signal is the signal to wait for. Empty for a timer case.
Signal string
// After is the delay of a timer case.
After time.Duration
}
RawCase is one alternative of Runtime.Select: a signal name or, when Signal is empty, a timer of After. This raw record is for driver adapters. Application workflows create typed alternatives with On and After rather than decoding signal bytes themselves.
cases := []workflow.RawCase{{Signal: "approved"}, {After: time.Minute}}
index, value, err := runtime.Select(cases)
if err != nil {
return err
}
_, _ = index, value
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the handlers of validated definitions for a driver to register. Use NewRegistry to select worker telemetry or a custom codec; Handlers uses JSON and follows the dynamic process telemetry default. Published handlers snapshot settings; service implementations stay shared.
registry, err := workflow.NewRegistry(definitions, func(worker *workflow.RegistryBuilder) {
worker.Telemetry(observations)
})
if err != nil {
return err
}
_ = registry.Steps()
func Handlers ¶
func Handlers(defs ...Definition) (*Registry, error)
Handlers validates defs and returns their handlers, encoded with JSON. It returns the Workflow.Err or Step.Err of a definition, so an invalid name satisfies ErrInvalidName and a nil function ErrNilFunc, then ErrDuplicateName when two definitions share a name and ErrMissingMaxDuration for a workflow without Builder.MaxDuration. Each handler captures the name, function, and settings independently of the published pointer; subsequent assignment through that pointer does not change the registry. Captured function closures retain their own ownership rules.
Example:
registry, err := workflow.Handlers(Onboard, SendWelcome)
if err != nil {
return err
}
_ = registry.Workflows()
func HandlersWith ¶
func HandlersWith(codec Codec, defs ...Definition) (*Registry, error)
HandlersWith is Handlers with a custom Codec. Use the same codec for the Client. Nil selects JSON. Codec implementations stay shared, while registrations snapshot the validated name, function, and policy of each definition.
registry, err := workflow.HandlersWith(workflow.JSON{}, Onboard, SendWelcome)
if err != nil {
return err
}
_ = registry.Workflows()
func NewRegistry ¶
func NewRegistry(definitions []Definition, configure ...RegistryBuilderFunc) (*Registry, error)
NewRegistry validates definitions and publishes an immutable worker registry. It has the same validation and handler-snapshot semantics as HandlersWith. Configure worker telemetry here even when clients run in a different pod. Without callbacks JSON and the dynamic process telemetry default apply.
registry, err := workflow.NewRegistry([]workflow.Definition{Onboard, SendWelcome},
func(worker *workflow.RegistryBuilder) {
worker.Telemetry(observations)
})
if err != nil {
return err
}
_ = registry.Workflows()
func (*Registry) NewTraceContext ¶
func (registry *Registry) NewTraceContext(ctx context.Context, carrier map[string]string) context.Context
NewTraceContext extracts a serialized start carrier onto a native attempt context, preserving its cancellation/deadline. Drivers call this only at the worker-to-step boundary; extraction must never run in replayed orchestration. The registry's worker telemetry service performs extraction. Nil/empty carriers are valid; no caller context is transported or retained.
ctx = registry.NewTraceContext(ctx, options.Trace)
output, err := handler.Handle(ctx, input)
if err != nil {
return err
}
_ = output
func (*Registry) Step ¶
func (registry *Registry) Step(name string) (StepHandler, bool)
Step returns the handler of the step called name. The boolean is false for an absent name. Drivers use this lookup for actual attempts, with per-call derived policy supplied separately by Runtime.Step.
handler, ok := registry.Step("charge")
if !ok {
return workflow.ErrNotFound
}
_ = handler.Options.Timeout
func (*Registry) Steps ¶
func (registry *Registry) Steps() collection.Slice[StepHandler]
Steps returns the step handlers sorted by name. The slice and records are copies; the registered typed functions are shared. This eager snapshot is not a live query; wrapping it adds collection operations without acquiring ownership of functions or their captures.
retried := registry.Steps().Filter(func(_ int, handler workflow.StepHandler) bool {
return handler.Options.Attempts > 1
})
for _, handler := range retried {
fmt.Println(handler.Name, handler.Options.Attempts)
}
func (*Registry) Workflow ¶
func (registry *Registry) Workflow(name string) (WorkflowHandler, bool)
Workflow returns the handler of the workflow called name. The boolean is false for an absent name. Editing the returned record cannot replace the registry's snapshot or its scalar policy.
handler, ok := registry.Workflow("onboard")
if !ok {
return workflow.ErrNotFound
}
_ = handler.Options.MaxDuration
func (*Registry) Workflows ¶
func (registry *Registry) Workflows() collection.Slice[WorkflowHandler]
Workflows returns the workflow handlers sorted by name. The returned slice is independent of the registry; changing its records cannot replace registered handlers. Handler functions remain shared. This eager snapshot is not a live query; collection operations do not clone functions, their captures, or services used by those functions.
short := registry.Workflows().Filter(func(_ int, handler workflow.WorkflowHandler) bool {
return handler.Options.MaxDuration <= time.Hour
})
for _, handler := range short {
fmt.Println(handler.Name, handler.Options.MaxDuration)
}
type RegistryBuilder ¶
type RegistryBuilder struct {
// contains filtered or unexported fields
}
RegistryBuilder configures worker registration independently of client pods. Its final state is copied before handlers are published; services stay shared.
registry, err := workflow.NewRegistry(definitions, func(worker *workflow.RegistryBuilder) {
worker.Telemetry(observations)
})
if err != nil {
return err
}
_ = registry.Steps()
func (*RegistryBuilder) Codec ¶
func (worker *RegistryBuilder) Codec(value Codec) *RegistryBuilder
Codec replaces the worker payload codec; nil selects JSON. The codec is shared, not cloned, and must match the client. It returns the same draft.
registry, err := workflow.NewRegistry(definitions, func(worker *workflow.RegistryBuilder) {
worker.Codec(workflow.JSON{})
})
if err != nil {
return err
}
_ = registry.Steps()
func (*RegistryBuilder) Telemetry ¶
func (worker *RegistryBuilder) Telemetry(value *telemetry.Telemetry) *RegistryBuilder
Telemetry selects worker attempt observations and carrier extraction. Nil follows the current process default at each attempt, independently of the client's choice. It does not observe orchestration or terminal run outcomes.
registry, err := workflow.NewRegistry(definitions, func(worker *workflow.RegistryBuilder) {
worker.Telemetry(observations)
})
if err != nil {
return err
}
_ = registry.Steps()
type RegistryBuilderFunc ¶
type RegistryBuilderFunc = func(*RegistryBuilder)
RegistryBuilderFunc configures one fresh worker-registration draft. Callbacks run once in order, including for invalid definitions; nil callbacks panic.
var configure workflow.RegistryBuilderFunc = func(worker *workflow.RegistryBuilder) {
worker.Codec(workflow.JSON{})
}
registry, err := workflow.NewRegistry(definitions, configure)
if err != nil {
return err
}
_ = registry
type Run ¶
type Run[Out any] struct { // contains filtered or unexported fields }
Run is a handle to one run of a workflow that produces Out. Get it from Client.Start or Client.Run. A handle stores identity and shared client dependencies, not execution state; each observation queries the driver and can fail if the run no longer exists.
run := client.Run(Onboard, "user-42")
output, err := run.Result(ctx)
if err != nil {
return err
}
_ = output
func (*Run[Out]) Cancel ¶
Cancel stops the run. Canceling a finished run is not an error. Telemetry observes the cancel call, not confirmation of engine termination. The existing RunCanceled topic reports the successful call only.
if err := run.Cancel(ctx); err != nil {
return err
}
func (*Run[Out]) ID ¶
ID returns the run identifier. This is the exact caller-supplied identity, not a new engine execution ID.
run := client.Run(Onboard, "user-42") fmt.Println(run.ID()) // user-42
func (*Run[Out]) Result ¶
Result waits for the run to end and returns its output, or the failure of the run: ErrCanceled after a cancel, ErrTimeout after Builder.MaxDuration, or the error of the workflow. ctx bounds only the wait; when it ends Result returns its error and the run continues. Telemetry measures this wait, including decoding, not workflow execution. Repeated waits are separate observations and never emit terminal events.
output, err := run.Result(ctx)
if err != nil {
return err
}
_ = output
type RunEvent ¶
type RunEvent struct {
// Workflow is the workflow name.
Workflow string
// RunID is the run identifier.
RunID string
}
RunEvent is the payload of RunStarted and RunCanceled. It carries no input, output, or metadata of the run. RunID is application-supplied operational data: restrict subscriptions when those identities are sensitive. These events describe local accepted actions.
unsubscribe, err := bus.Listen(workflow.RunStarted, func(_ context.Context, notice workflow.RunEvent) error {
fmt.Println(notice.Workflow)
return nil
})
if err != nil {
return err
}
defer unsubscribe()
type RunSignaledEvent ¶
type RunSignaledEvent struct {
// Workflow is the workflow name.
Workflow string
// RunID is the run identifier.
RunID string
// Signal is the signal name.
Signal string
}
RunSignaledEvent is the payload of RunSignaled. It omits the signal value. The event reports accepted delivery, not workflow consumption or completion.
unsubscribe, err := bus.Listen(workflow.RunSignaled, func(_ context.Context, notice workflow.RunSignaledEvent) error {
fmt.Println(notice.Signal)
return nil
})
if err != nil {
return err
}
defer unsubscribe()
type Runtime ¶
type Runtime interface {
// ID returns the run identifier.
ID() string
// Name returns the workflow name.
Name() string
// Now returns the current time of the run.
Now() time.Time
// UUID returns a new random identifier.
UUID() string
// Logger returns the logger of the run.
Logger() *slog.Logger
// Sleep pauses the run for d.
Sleep(d time.Duration) error
// Done is closed when the run is canceled or timed out.
Done() <-chan struct{}
// Err returns why Done is closed, or nil.
Err() error
// Step runs the registered step called name with completed per-call options o,
// not the registered handler's default policy. Initial zero means no deliberate
// positive backoff. An engine unable to express Attempts > 1 with Initial == 0
// returns ErrUnsupported before scheduling or executing the affected step;
// it must not substitute a positive delay or retry the whole workflow instead.
// This policy rejection does not disable single attempts or positive backoff.
//
// policy := workflow.StepOptions{Attempts: 3, Initial: 0, Factor: 1}
// output, err := runtime.Step("charge", input, policy)
// if err != nil {
// return err
// }
// _ = output
Step(name string, in []byte, o StepOptions) ([]byte, error)
// Child atomically creates or joins run id of workflow name and waits for
// its result. Only creation acquires cancellation ownership: cancellation
// or timeout of the caller also stops a newly created child. Joining ignores
// in and never acquires ownership, including when another parent created
// the existing run. Cancellation or timeout ends the joining wait without
// stopping that run. Replay must retain the original ownership decision;
// a lookup followed by a separate create is not sufficient. A driver unable
// to guarantee this returns [ErrUnsupported] before submission, not a
// detached child or a join that can cancel another owner's run.
//
// output, err := runtime.Child("fulfill", "order-42", input)
// if err != nil {
// return err
// }
// _ = output // An existing run keeps its original input and owner.
Child(name, id string, in []byte) ([]byte, error)
// Spawn starts run id of workflow name without waiting; the run outlives
// the caller. An existing run is not an error: [Context.Spawn] treats
// [ErrAlreadyStarted] as success so a replay is idempotent. A driver that
// cannot do it returns [ErrUnsupported].
Spawn(name, id string, in []byte) error
// Wait blocks for the next value of signal. A zero timeout waits without
// limit. It returns [ErrTimeout] when the timeout expires.
Wait(signal string, timeout time.Duration) ([]byte, error)
// Select blocks until one case fires and returns its index and, for a
// signal case, the value.
Select(cases []RawCase) (index int, value []byte, err error)
// Raw returns the native context of the engine, such as a
// workflow.Context from Temporal.
Raw() any
}
Runtime is the execution environment a driver gives to a workflow function. A Runtime may also implement Repanic(v any) bool; the workflow handler raises a recovered panic again when it returns true, so engine control-flow panics are not turned into failures. Every call that blocks returns early with an error when the run is canceled or timed out. A driver that replays workflows must make these calls deterministic and durable, including the ownership decision of Child. Engine implementations receive one from their own execution adapter; an application uses Context rather than constructing a native runtime.
handler, ok := registry.Workflow("onboard")
if !ok {
return workflow.ErrNotFound
}
out, err := handler.Handle(runtime, input) // runtime is supplied by the engine.
if err != nil {
return err
}
_ = out
type Signal ¶
type Signal[T any] struct { // contains filtered or unexported fields }
Signal names an external message of type T that a running workflow waits for. Create it with NewSignal; send it with Client.Signal. Definitions bind a shared name to the same payload type on both sides of the wire; using the same name with incompatible types is an application error.
approved := workflow.NewSignal[bool]("approved")
if err := client.Signal(ctx, Onboard, "user-42", approved, true); err != nil {
return err
}
func NewSignal ¶
NewSignal returns the signal called identity that carries values of type T. The name must satisfy the shared identifier rule of 1-64 lowercase letters, digits, or hyphens with no hyphen at either end; NewSignal panics with ErrInvalidName otherwise. The constructor does no I/O; use a shared definition for senders and waiters.
approved := workflow.NewSignal[bool]("approved")
value, err := workflowContext.Wait(approved, workflow.Timeout(time.Minute))
if err != nil {
return "", err
}
_ = value
type StartOption ¶
type StartOption func(*StartOptions)
StartOption changes one StartOptions setting of Client.Start. Options run in call order before trace injection and do not reconfigure the client. Custom options must not retain the per-call settings pointer.
var option workflow.StartOption = workflow.Delay(time.Minute)
run, err := client.Start(ctx, Onboard, "user-42", user, option)
if err != nil {
return err
}
_ = run.ID()
func Delay ¶
func Delay(d time.Duration) StartOption
Delay postpones the start of the run by d. The driver owns scheduling; this does not sleep in Client.Start.
run, err := client.Start(ctx, Onboard, "user-42", user, workflow.Delay(time.Minute))
if err != nil {
return err
}
_ = run.ID()
func Metadata ¶
func Metadata(data map[string]string) StartOption
Metadata attaches free-form data to the run. Applying the option copies the outer map. Keep sensitive data out of metadata that the selected engine exposes in its operational interfaces.
run, err := client.Start(ctx, Onboard, "user-42", user,
workflow.Metadata(map[string]string{"source": "signup"}))
if err != nil {
return err
}
_ = run.ID()
type StartOptions ¶
type StartOptions struct {
// Trace carries injected trace fields separately from application metadata.
// Drivers copy retained maps and forward fields without invoking telemetry
// in replayed orchestration. Nil means no propagated parent. This is not an
// authorization boundary; only trusted submitters should reach workers.
//
// options := workflow.StartOptions{Trace: map[string]string{"traceparent": parent}}
Trace map[string]string
// Delay postpones the start of the run.
Delay time.Duration
// Metadata is free-form data kept with the run.
Metadata map[string]string
}
StartOptions are the settings of one Driver.Start. Trace is transport propagation state, not application metadata; Client.Start supplies it after applying start options. Drivers retain copies rather than caller contexts.
options := workflow.StartOptions{Metadata: map[string]string{"source": "signup"}}
err := driver.Start(ctx, "onboard", "user-42", input, options)
if err != nil && !errors.Is(err, workflow.ErrAlreadyStarted) {
return err
}
type Status ¶
type Status string
Status is the persisted, lowercase state of a run. Its empty zero value is invalid and does not identify a running workflow.
Example: _ = workflow.Status("running") == workflow.Running
const ( // Running means the run has not ended. // // Example: _ = workflow.Status("running") == workflow.Running Running Status = "running" // Completed means the run returned an output. // // Example: _ = workflow.Completed.String() // "completed" Completed Status = "completed" // Failed means the run returned an error or timed out. // // Example: _ = workflow.Failed.String() // "failed" Failed Status = "failed" // Canceled means the run was canceled. // // Example: _ = workflow.Canceled.String() // "canceled" Canceled Status = "canceled" )
The states of a run.
type Step ¶
type Step[In, Out any] struct { // contains filtered or unexported fields }
Step is a named, typed unit of side-effecting work that a workflow runs through Context.Step. A driver may retry and record it. Create it with NewStep and configure it through StepBuilder.
A Step is an immutable definition: Step.Derive returns a modified copy that keeps the name, and the first error found is kept and reported by Step.Err. Handlers and Context.Step reject a step whose Err is not nil. To change the settings of one call, derive a step and pass that instead. Always create steps with NewStep; the zero value has no name and no function.
Example:
var Charge = workflow.NewStep("charge-card", charge, func(step *workflow.StepBuilder) {
step.Retry(5)
step.Backoff(time.Second, 2, time.Minute)
step.Timeout(30 * time.Second)
})
// Inside a workflow, for this call only:
singleAttempt := Charge.Derive(func(step *workflow.StepBuilder) {
step.Retry(1)
})
receipt, err := workflowContext.Step(singleAttempt, order)
func NewStep ¶
func NewStep[In, Out any](identity string, fn func(context.Context, In) (Out, error), configure ...StepBuilderFunc) *Step[In, Out]
NewStep returns the step called identity that runs fn. The name must satisfy the shared identifier rule of 1-64 lowercase letters, digits, or hyphens with no hyphen at either end, and fn must not be nil. Violations are kept on the value and reported by Step.Err, Handlers, and Context.Step as ErrInvalidName or ErrNilFunc. Steps run once by default with a one minute timeout. Return Permanent from fn to stop retries. The default retry backoff starts at one second, multiplies by two, and caps at 30 seconds; it is used only when the configured attempts allow retries. Callbacks run directly once, synchronously in order on one fresh, nonnil draft, including invalid declarations. Nil entries and callback panics propagate; expanded nil or empty slices supply no edits. Publication copies the settings so a retained draft cannot change the step.
Example:
charge := workflow.NewStep("charge", chargeCard, func(step *workflow.StepBuilder) {
step.Retry(3)
})
registry, err := workflow.Handlers(charge)
if err != nil {
return err
}
_ = registry
func (*Step[In, Out]) Derive ¶
func (step *Step[In, Out]) Derive(configure ...StepBuilderFunc) *Step[In, Out]
Derive returns a detached step with the same name, function, policy, and first error, followed by the supplied edits. Callbacks run directly once, synchronously in order on one fresh draft. Nil entries panic; expanded nil or empty slices supply no edits. The base is never mutated, and defaults are not reapplied. Use the result for one Context.Step call. The base is captured before callbacks run, including its typed function.
Example:
singleAttempt := charge.Derive(func(step *workflow.StepBuilder) {
step.Retry(1)
})
receipt, err := workflowContext.Step(singleAttempt, order)
func (*Step[In, Out]) Err ¶
Err returns the first declaration error, or nil for a valid step. Derive retains this error; later settings cannot repair an invalid declaration. The zero Step reports ErrInvalidName because it has no declaration identity.
Example:
step := workflow.NewStep("Bad", charge)
if errors.Is(step.Err(), workflow.ErrInvalidName) {
return step.Err()
}
type StepBuilder ¶
type StepBuilder struct {
// contains filtered or unexported fields
}
StepBuilder configures the retry policy of a step before NewStep or Step.Derive publishes it. Each setting returns the same draft; publication copies its scalar settings so a retained draft cannot change the step.
Example:
workflow.NewStep("charge", charge, func(step *workflow.StepBuilder) {
step.Retry(3)
})
func (*StepBuilder) Backoff ¶
func (step *StepBuilder) Backoff(initial time.Duration, factor float64, maximum time.Duration) *StepBuilder
Backoff sets the delay before the first retry, the factor that multiplies it after each retry, and the cap on the delay. A zero maximum means no cap. A factor below one counts as one (constant delay). A negative initial or maximum is an error. Initial zero requests no deliberate positive delay, not an engine default. Engines unable to express it with multiple attempts return ErrUnsupported before scheduling or executing that call; the declaration itself remains valid. Defaults are an initial delay of one second, factor two, and a 30-second cap. Backoff returns the same draft and preserves the first error. NaN and infinite factors are normalized to one, like a factor below one, so the published policy remains usable by native engines and the memory driver.
Example:
workflow.NewStep("charge", charge, func(step *workflow.StepBuilder) {
step.Retry(3)
step.Backoff(time.Second, 2, time.Minute)
})
func (*StepBuilder) Retry ¶
func (step *StepBuilder) Retry(attempts int) *StepBuilder
Retry sets the maximum number of executions of a step. One means no retry, which is the default. A smaller value counts as one. Retry returns the same draft and does not clear earlier errors.
Example:
workflow.NewStep("charge", charge, func(step *workflow.StepBuilder) {
step.Retry(3)
})
func (*StepBuilder) Timeout ¶
func (step *StepBuilder) Timeout(duration time.Duration) *StepBuilder
Timeout bounds one execution of a step. The default is one minute. Zero means no limit and a negative value is an error. Timeout returns the same draft and preserves the first declaration error.
Example:
workflow.NewStep("charge", charge, func(step *workflow.StepBuilder) {
step.Timeout(30 * time.Second)
})
type StepBuilderFunc ¶
type StepBuilderFunc = func(*StepBuilder)
StepBuilderFunc edits a step draft during NewStep or Step.Derive. Callbacks run synchronously once in order on one independent draft; supplied nil callbacks panic. Edits preserve an inherited declaration error.
Example:
policy := workflow.StepBuilderFunc(func(step *workflow.StepBuilder) {
step.Retry(3)
})
charge := workflow.NewStep("charge", chargeCard, policy)
if err := charge.Err(); err != nil {
return err
}
type StepHandler ¶
type StepHandler struct {
// Name is the step name.
Name string
// Options are the default settings of the step.
Options StepOptions
// Handle decodes in, runs the function, and returns the encoded output.
// An error that satisfies [ErrPermanent] must not be retried.
// Registered handlers observe each actual invocation with the registry's
// telemetry service. Drivers must not invoke this callback for replayed
// results; extract the carrier with Registry.NewTraceContext beforehand.
//
// ctx = registry.NewTraceContext(ctx, options.Trace)
// out, err := handler.Handle(ctx, input)
// if err != nil {
// return err
// }
// _ = out
Handle func(ctx context.Context, in []byte) ([]byte, error)
}
StepHandler runs one step definition on bytes. Call Handle only for actual attempts, not for replayed recorded results. Its context carries cancellation and worker trace-parent extraction.
handler, ok := registry.Step("charge")
if !ok {
return workflow.ErrNotFound
}
ctx = registry.NewTraceContext(ctx, carrier)
output, err := handler.Handle(ctx, input)
if err != nil {
return err
}
_ = output
type StepOptions ¶
type StepOptions struct {
// Attempts is the maximum number of executions. One means no retry.
Attempts int
// Initial is the delay before the first retry. Zero means no deliberate
// positive backoff, not an engine default. A driver unable to express this
// with multiple attempts rejects the completed policy with ErrUnsupported
// before affected step scheduling or execution; see Runtime.Step.
//
// policy := workflow.StepOptions{Attempts: 3, Initial: 0, Factor: 1}
// fmt.Println(policy.Backoff(1)) // 0s
Initial time.Duration
// Factor multiplies the delay after every retry.
Factor float64
// Max caps the delay between retries. Zero means no cap.
Max time.Duration
// Timeout bounds one execution.
Timeout time.Duration
}
StepOptions are the settings of one step execution. NewStep publishes completed defaults. Raw adapters retain finite factors; Backoff normalizes nonfinite factors to one before calculating a delay.
handler, ok := registry.Step("charge")
if !ok {
return workflow.ErrNotFound
}
delay := handler.Options.Backoff(1)
_ = delay
func (StepOptions) Backoff ¶
func (options StepOptions) Backoff(n int) time.Duration
Backoff returns the delay before retry n, counting from 1. It never overflows: the result is capped by Max, or by the largest duration.
Example:
policy := workflow.StepOptions{
Initial: time.Second,
Factor: 2,
Max: time.Minute,
}
delay := policy.Backoff(2)
type WaitOption ¶
type WaitOption func(*waitConfig)
WaitOption changes one setting of Context.Wait. It affects only this wait and cannot change the workflow's MaxDuration.
var option workflow.WaitOption = workflow.Timeout(time.Minute)
value, err := workflowContext.Wait(Approved, option)
if err != nil {
return "", err
}
_ = value
func Timeout ¶
func Timeout(d time.Duration) WaitOption
Timeout limits Context.Wait. When it expires Wait returns ErrTimeout. Without it Wait blocks until the signal arrives or the run ends. Zero or a negative duration disables the per-wait timeout; neither disables cancellation or the workflow's MaxDuration. An already remembered signal value returns immediately without scheduling another timer.
value, err := workflowContext.Wait(Approved, workflow.Timeout(time.Minute))
if errors.Is(err, workflow.ErrTimeout) {
return "expired", nil
}
if err != nil {
return "", err
}
_ = value
type Workflow ¶
type Workflow[In, Out any] struct { // contains filtered or unexported fields }
Workflow is a named, typed, durable function together with the run settings that are application code rather than transport data. Create it with New, configure it through Builder, pass it to Handlers on the worker, and start it with Client.Start.
A Workflow is an immutable published definition: callbacks edit a draft, and the first error found is kept and reported by Workflow.Err. Handlers rejects a workflow whose Err is not nil. Always create workflows with New; the zero value has no name and no function.
Example:
var Onboard = workflow.New("onboard", onboard, func(definition *workflow.Builder) {
definition.MaxDuration(24 * time.Hour)
definition.Retention(7 * 24 * time.Hour)
definition.Description("Welcomes a new user and waits for confirmation.")
})
func New ¶
func New[In, Out any](identity string, fn func(*Context, In) (Out, error), configure ...BuilderFunc) *Workflow[In, Out]
New returns the workflow called identity that runs fn. The name must satisfy the shared identifier rule of 1-64 lowercase letters, digits, or hyphens with no hyphen at either end, and fn must not be nil. Violations are kept on the value and reported by Workflow.Err and Handlers as ErrInvalidName or ErrNilFunc. fn must use the Context for time, randomness, and side effects, so a driver can replay it. Set Builder.MaxDuration before registering: it is required. Callbacks run directly once, synchronously in order on one fresh, nonnil draft, even when name or fn is invalid. Nil entries and callback panics propagate; expanded nil or empty slices supply no edits. Publication copies the completed settings; retained drafts cannot change the result.
Example:
onboard := workflow.New("onboard", onboardUser, func(definition *workflow.Builder) {
definition.MaxDuration(24 * time.Hour)
})
registry, err := workflow.Handlers(onboard)
if err != nil {
return err
}
_ = registry
func (*Workflow[In, Out]) Err ¶
Err returns the first declaration error, or nil for valid construction. Missing MaxDuration is checked only by Handlers, not by Err. A zero Workflow reports ErrInvalidName before registration-only checks.
Example:
definition := workflow.New("Bad", onboard)
if errors.Is(definition.Err(), workflow.ErrInvalidName) {
return definition.Err()
}
type WorkflowHandler ¶
type WorkflowHandler struct {
// Name is the workflow name.
Name string
// Options are the settings of the definition.
Options WorkflowOptions
// Handle decodes in, runs the function with a [Context] over rt, and
// returns the encoded output. A failure is returned as the error of the
// function; use [EncodeError] to carry it.
Handle func(rt Runtime, in []byte) ([]byte, error)
}
WorkflowHandler runs one workflow definition on bytes. Registration snapshots the declaration; adapters must supply a replay-safe Runtime and preserve failures using EncodeError at process boundaries.
handler, ok := registry.Workflow("onboard")
if !ok {
return workflow.ErrNotFound
}
output, err := handler.Handle(runtime, input)
if err != nil {
return err
}
_ = output
type WorkflowOptions ¶
type WorkflowOptions struct {
// MaxDuration bounds the whole run. Required.
MaxDuration time.Duration
// Retention is how long the engine keeps a finished run. Zero uses the
// engine default.
Retention time.Duration
// Description documents the workflow.
Description string
}
WorkflowOptions are the settings of a workflow definition. Configure definitions through Builder; engine adapters read this completed reporting record from registered WorkflowHandler values.
handler, ok := registry.Workflow("onboard")
if !ok {
return workflow.ErrNotFound
}
limit := handler.Options.MaxDuration
_ = limit
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
envelope
Package envelope owns workflow payload framing and failure wire records.
|
Package envelope owns workflow payload framing and failure wire records. |
|
execution
Package execution protects registered workflow and step invocations.
|
Package execution protects registered workflow and step invocations. |
|
Package memoryworkflow provides an in-process workflow.Driver for local development and single-replica deployments.
|
Package memoryworkflow provides an in-process workflow.Driver for local development and single-replica deployments. |
|
internal/engine
Package engine owns in-process workflow runs, worker lifetime, and retention.
|
Package engine owns in-process workflow runs, worker lifetime, and retention. |
|
Package workflowtest helps test code that starts workflows and checks drivers of the workflow package.
|
Package workflowtest helps test code that starts workflows and checks drivers of the workflow package. |