Skip to content

Package worlds ​

go
import "github.com/eriicafes/go-workflow/worlds"

Package worlds defines the World contract that runs are recorded in, the runtime that executes deliveries against it, the Client, and the HTTP server that exposes a World to clients. Full godoc is on pkg.go.dev.

Most applications only touch worlds.Runners. This page is for operators, tool builders, and people implementing a World.

Runners and runtime ​

go
func Runners(values ...Runner) RunnerSet
func NewRunner(name string, run func(context.Context, json.RawMessage) (json.RawMessage, error)) Runner

func NewRuntime(world World, runners RunnerSet) *Runtime
func (*Runtime) Execute(ctx context.Context, input QueueInput) error

workflow.Use builds a Runner from a typed runner function. NewRunner builds one from a JSON function, for runners written without the workflow package.

Runtime.Execute handles one delivery. With a nil StepID it executes a flow. With StepID set it executes that step. A delivery for a terminal run returns nil without running code, and one for a terminal step queues the flow that consumes its result instead of running the step. A non-nil error means the delivery should be retried. Execute filters managed cancellation bindings and removes any Client put in the context with NewClientContext, so a run never uses a Client it inherited.

Set QueueInput.MessageID to the ID Queue returned for the message before calling Execute, and keep it the same when the message is redelivered. A flow records it as the owner of each step it starts inline, so the redelivery of a flow that stopped runs those steps again at once.

A replay that diverges from the event log ends the delivery with an error that runner code never sees. Execute queues an immediate recovery delivery of the same flow or step with ReplayDivergenceCount incremented, and once three recovery deliveries have diverged, fails the run with an error matching ErrCorruptedEventLog, without running compensations.

When the World reports DeploymentAffinity, a delivery for a run pinned to another deployment is re-routed to the run's deployment instead of replayed. After three re-routes, or when the World returns ErrDeploymentUnavailable, the run fails with an error matching ErrDeploymentMismatch. See Pinning runs to a deployment.

A flow whose write another flow superseded (ErrInBandSuperseded) queues one follow-up flow delivery right away and acknowledges its own. On a World that deduplicates, flows refused by the same write share that follow-up. See Concurrent flows.

A storage error while declaring a step, timer, or hook, or while cancelling a context or disposing a hook, ends the delivery so the queue retries it, instead of reaching runner code.

When a flow replays past a step's declaration, it queues the step's delivery again if the step hasn't started since it was declared or last scheduled for a retry, in case a crash or queue error lost the first one. This covers steps started with Background that nothing awaits. The re-queue uses the step ID as its idempotency key, or the retry's key for a step scheduled for a retry. A step waiting for a retry that isn't due yet is left alone until its queued retry is due. Once that time has passed without a start, it is re-queued like any unstarted step. Steps declared in the same replay are skipped, and inline steps never need it. If the re-queue fails, the flow delivery fails so the queue retries it. Local and Postgres drop the duplicate while the original delivery is still queued. AWS and Vercel don't deduplicate, so a step waiting to start may be delivered twice, which its at-least-once contract already allows. The Workflow SDK re-ensures pending steps on replay the same way.

Timers settle the same way: a flow that replays past a timer's declaration once it is due completes it. WithTimeout and WithDeadline use normal timers and continuations to cancel contexts. Cancelling those contexts affects the steps and timers that use them, not the run.

World ​

go
type World interface {
	Storage
	Queue
	Streamer
	BaseURL() string
	Analytics() Analytics
	Capabilities() Capabilities
	SpecVersion() int
	DeploymentID() string
}

type Capabilities struct {
	Analytics          bool
	SetAttributes      bool
	DeploymentAffinity bool
}

A capability being false does not remove a method. Unsupported attribute writes are successful no-ops, and Analytics returns nil.

DeploymentID returns the deployment whose code the World's deliveries execute, or "". Runs created through the World's Client record it on Run.DeploymentID. With DeploymentAffinity, the Runtime also enforces it: deliveries are addressed to the run's deployment through QueueInput.DeploymentID, and misdelivered ones are re-routed. Of the built-in Worlds, only Vercel reports affinity, and only when it has a deployment ID.

A World with affinity can also implement LatestDeployments, whose LatestDeploymentID(ctx) returns its newest deployment, or "" when it knows none. workflow.Start pins new runs of workflows marked workflow.StartOnLatest to it through StartDeploymentID(ctx, world, latest): child runs directly, and other runs by setting ClientRunsCreateInput.Latest, which CreateRun resolves on the World's side of a local or HTTP Client.

A World can choose a delivery's queue from QueueInput.WorkflowName, which the Runtime and Client set on every delivery. ResumeHook takes it from the hook's ResumeContext, and other code that queues for a run it hasn't loaded, such as wake-ups and cancellation, reads the run for it. RunDelivery(ctx, world, runID) returns a delivery addressed that way.

Storage ​

Storage is StorageRuns, StorageSteps, StorageEvents, and StorageHooks.

MethodDescription
RunsGetA run view. ErrRunNotFound if missing.
RunsListRuns filtered by WorkflowName and Status, paginated.
RunsWaitForTerminalStatusBlocks until the run is terminal, Timeout elapses, or ctx is done.
RunsSetAttributesApplies attribute changes as one attr_set event.
StepsGet, StepsListStep views of a run.
EventsCreateRunCreates a run and its run_created event. ErrRunAlreadyExists if it exists.
EventsCreateAppends one event, enforcing transition rules. Hook creation atomically claims or records conflict, returning committed event data and the created Hook.
EventsCreateBatchAppends events atomically. Rejecting one rejects the batch.
EventsGet, EventsList, EventsListByCorrelationIDRead the event log.
HooksGet, HooksGetByToken, HooksListHook views.

In-band fence. An append whose EventsCreateOptions.InBandCount (or EventsCreateBatchInput.InBandCount) is set is an in-band write by the run's orchestrator. The World appends it only when Run.InBandCount still equals that count, advances the count by the events appended, and returns it as EventResult.InBandCount. Otherwise it appends nothing and returns ErrInBandSuperseded. Appends without a count are out-of-band: never refused, and they leave the count alone. See Concurrent flows.

Hook token ownership is acquired through EventsCreate(hook_created), with conflict and takeover metadata in the same commit. There is no separate public claim operation. A hook's TokenRetentionUntil comes from its hook_created data. Explicit disposal releases immediately. Run, step, and hook views retain their timestamps. Step StartedAt is the first attempt. Step metadata describes the current attempt.

Lists take a Pagination with Cursor, Limit, and SortOrder, and return a Page with Data, Cursor, and HasMore. ResolveData selects whether payloads (inputs, outputs, hook metadata) are returned.

Views ​

go
type Run struct {
	RunID        RunID
	WorkflowName string
	Status       Status
	Input        []byte // JSON workflow input
	Output       []byte // JSON workflow output
	Error        []byte // JSON Error; DecodeError decodes it
	Attributes   map[string]string
	DeploymentID string // the deployment the run is pinned to
	InBandCount  int    // events appended by the run's orchestrator, which fence its writes
	CreatedAt    time.Time
	UpdatedAt    time.Time
	StartedAt    *time.Time
	CompletedAt  *time.Time
}

type Step struct {
	RunID       RunID
	StepID      StepID
	StepName    string
	Status      Status // pending before the first attempt and while a retry is scheduled
	Input       []byte
	Output      []byte
	Error       []byte // last failed attempt
	Attempt     int
	CreatedAt   time.Time
	UpdatedAt   time.Time
	StartedAt   *time.Time
	CompletedAt *time.Time
	RetryAfter  *time.Time
}

type Hook struct {
	RunID               RunID
	ResumeContext       HookResumeContext // copied from the run when the hook is created
	HookID              HookID
	Token               string
	Metadata            []byte
	Webhook             *Webhook // set for webhooks
	WebhookEventID      EventID
	CreatedAt           time.Time
	UpdatedAt           time.Time
	ReceivedAt          *time.Time
	TokenRetentionUntil *time.Time
	ClaimedFrom         *HookForceClaim
	ForceClaimedBy      *HookForceClaim
	DisposedAt          *time.Time
}

type HookResumeContext struct {
	DeploymentID string
	WorkflowName string
}

Run attributes $parentRunId (ParentRunIDAttribute) and $rootRunId (RootRunIDAttribute) are set on child runs.

Queue ​

go
type Queue interface {
	Queue(context.Context, QueueInput) (QueueResult, error)
	QueueBatch(context.Context, QueueBatchInput) ([]QueueResult, error)
	QueueCancel(context.Context, QueueCancelInput) error
}

type QueueInput struct {
	QueueName      string // empty selects the default queue
	WorkflowName   string // workflow of the run, for Worlds that route by it
	RunID          RunID
	StepID         *StepID    // nil makes the delivery a flow
	NotBefore      *time.Time // nil allows immediate delivery
	IdempotencyKey string     // for implementations that deduplicate
	DeploymentID   string     // deployment to deliver to; empty uses the World's own

	DeploymentMismatchRetryCount int    // re-routes so far, set by the Runtime
	ReplayDivergenceCount        int    // recovery deliveries after divergence, set by the Runtime
	MessageID                    string // the delivered message's ID, set by the World before Execute; Queue ignores it
}

Delivery is at least once. QueueCancel returns ErrQueueMessageNotFound for an unknown message. A World with deployment affinity returns an error wrapping ErrDeploymentUnavailable when the addressed deployment no longer exists.

Routing ​

go
type Workflow[Out any] interface{ Workflow(Out) }

func WorkflowName[In Workflow[Out], Out any]() string
func Route[In Workflow[Out], Out any](queue string) QueueRoute
func Routes(routes ...QueueRoute) map[string]string

Workflow is satisfied by a workflow type, such as one embedding workflow.Struct. WorkflowName returns the name its runs are recorded under, its package path and type name. Routes(Route[W](queue), ...) builds the Config.Routes of a World that routes deliveries by workflow.

Retention ​

go
type WorkflowRetention struct {
	Workflow  string
	Retention time.Duration
}

func Retain[In Workflow[Out], Out any](retention time.Duration) WorkflowRetention
func Retentions(retentions ...WorkflowRetention) map[string]time.Duration
func RetentionFor(fallback time.Duration, retentions map[string]time.Duration, workflow string) time.Duration
func MaxRetention(fallback time.Duration, retentions map[string]time.Duration) (longest time.Duration, negative bool)

Retentions(Retain[W](retention), ...) builds the Config.Retentions of a World that supports retention. RetentionFor returns how long finished runs of a workflow are kept, its entry or else the fallback, where zero keeps them. MaxRetention returns the longest retention and whether any is negative, for validating a configuration.

Streamer ​

MethodDescription
StreamsCreateWriteSessionExclusive writer session for a stream. ErrStreamLocked if another holds it.
StreamsWriteSessionAppends chunks for a session at a sequence number.
StreamsCloseWriteSessionReleases a session, optionally closing the stream.
StreamsWrite, StreamsWriteMultiAppend when no session owns the stream.
StreamsCloseMarks a stream done when no session owns it.
StreamsGetChunksChunks after a cursor, or from StartIndex when the cursor is empty, waiting up to Wait for more. A negative StartIndex counts back from the end; worlds.StreamsStartIndex resolves it.
StreamsGetInfoTail index and whether the stream is done.
StreamsListStream names of a run.

Analytics ​

Analytics is AnalyticsRuns (RunsGet, RunsList of run summaries), AnalyticsAttributes (AttributesList, one aggregate per attribute key), and AnalyticsWaits (WaitsGet, WaitsList). A World returns it only when Capabilities().Analytics is true.

Client ​

Client is the API outside a run. workflow.Client is an alias. Its first methods are shared with the World:

MethodDescription
RunsGet, RunsList, RunsWaitForTerminalStatus, RunsSetAttributesRead runs and change their attributes.
StepsGet, StepsListRead steps.
StreamsGetChunks, StreamsGetInfo, StreamsListRead streams.
StreamsCreateWriteSessionOpen an exclusive writer session.
HooksGet, HooksGetByTokenLook up hook metadata by ID, or by the token of an active or retained hook.
AnalyticsAs on World.

The rest are the Client's own:

MethodDescription
RunsCreateStarts a run and queues its first delivery. Returns the run ID.
RunsCancelCancels a run. Returns ErrRunTerminal if finished.
RunsWakeSettles pending waits early.
StreamsWrite, StreamsWriteMulti, StreamsCloseWrite streams without a session.
HooksResumeResolves the token owner once, records a payload, and queues the run. Ownership races reach the caller.
SpecThe World's protocol version and capabilities, as a ClientSpec.
go
type ClientSpec struct {
	SpecVersion  int
	Capabilities Capabilities
}

Spec takes a context and returns an error, because the HTTP client asks the World for it over RPC. The HTTP client caches a successful result.

go
func NewClient(world World) Client
func NewHTTPClient(endpoint string, clients ...*http.Client) Client
func NewClientContext(ctx context.Context, client Client) context.Context
func ClientFromContext(ctx context.Context) (Client, bool)

HTTP ​

go
func HTTPHandler(world World, prefix string) http.Handler // empty prefix is /v1/workflow
func RPCHandler(world World) http.Handler
func WebhookHandler(world World) http.Handler
func NormalizeBaseURL(value string) (string, error)

See HTTP Endpoints.

Durable primitives ​

The workflow package is built on these functions, which read and append to the Execution of the current delivery. They are exported for advanced integrations. Operations normally take the runner's next position with Execution.NextPosition. Lazy hooks reserve a position at creation and pass it to DurableHook when receiving, selecting, or explicitly checking ownership.

FunctionDescription
DurableStepDeclares a step at the next position, or executes it in its step delivery.
DurableInlineStepRuns an inline step's attempt in the flow and records its result, or queues the retry, records it, and suspends.
DurableWaitDeclares a durable timer. Context cancellation can end its wait early.
DurableHookCreates a hook and claims its token, or matches its recorded declaration. Takes options only and uses the execution’s delivery context.
Execution.Settled, Execution.SettledEventWhether an operation's result has been delivered, and the event that delivered it.
Execution.AdvanceDeliver the next recorded event and run continuations. Returns false at the end of the loaded log.
Execution.IsSuspendedWhether the runtime stopped replay before the workflow body finished. Deferred cleanup skips work in this case.
Execution.CreateEventAppend one of this replay's writes, fenced for flows, and add it with any events appended since. A superseded write ends the replay with a follow-up queued. Other World errors retry the delivery. A non-nil mapError sees the error first and can accept it.
StepOutcome, HookOutcomeDecode a delivered step or hook result.
Execution.InBandCountThe in-band count a flow's writes are fenced with.
WakeRunCompletes pending waits in log order and queues a delivery.
PendingWaitsThe IDs of a run's pending waits, in creation order.
ResumeHookAppends a hook's payload and queues its run, addressed to the run's deployment.
RunDeploymentIDThe deployment a run is pinned to, for addressing deliveries queued from outside it.
NewRunHookTokenDerives a stable token from a run and hook declaration position.
ExecutionFromContextThe replay state of the current delivery.

Errors ​

VariableMeaning
ErrWorkflowNotFoundNo runner is registered for the run's workflow.
ErrRunNotFoundNo run has the ID.
ErrRunAlreadyExistsEventsCreateRun with an existing ID.
ErrRunTerminalAppending to or writing a stream of a finished run.
ErrRunCancelledThe run was cancelled.
ErrStepNotFoundNo step has the ID.
ErrStepNotStartableStep transition not allowed.
ErrEventNotFoundNo event (or wait) has the ID.
ErrEventInvalidMalformed event, ID, or cursor, or an item over a backend limit.
ErrHookNotFoundNo hook has the ID, or no hook owns the token.
ErrHookNotActiveThe hook was resolved, disposed, or lost its token.
ErrHookConflictAnother active hook owns the token.
ErrHookForceClaimedAnother hook took the token with Force.
ErrResumeNotQueuedA hook payload was recorded but its run could not be queued.
ErrStreamLockedStream write conflicts with its writer session.
ErrQueueMessageNotFoundQueueCancel with an unknown message.
ErrCorruptedEventLogThe run's replay kept diverging from its event log.
ErrDeploymentMismatchThe run's deliveries kept reaching other deployments, or its deployment is gone.
ErrDeploymentUnavailableQueue addressed a deployment that no longer exists.
ErrInBandSupersededAn in-band write's count is stale because another flow wrote to the run first.
ErrWebhookRequestTooLargeWebhook body over 128 KiB (413).
go
type Error struct {
	// Error chain fields omitted.
}

func NewError(err error) *Error
func DecodeError(data []byte) *Error
func DeliveryError(cause error) error

DeliveryError marks a World error that a step function hit while doing the runtime's own work, such as recording a hook's disposal. The delivery is retried, instead of the step recording a failed attempt.

Error keeps the type, message, and JSON encoding of every error in the chain so errors.Is and errors.As work after replay.

Released under the MIT License.