Custom Worlds
You can back go-workflow with any storage and queue by implementing worlds.World. The runtime, the client, and the HTTP handler all work against this interface.
The World interface
type World interface {
Storage
Queue
Streamer
// BaseURL returns the absolute public URL where HTTPHandler is mounted,
// including its workflow path prefix and without a trailing slash.
BaseURL() string
// Analytics returns the Analytics interface when analytics are enabled,
// otherwise nil.
Analytics() Analytics
// Capabilities reports the optional behavior this World implements.
Capabilities() Capabilities
// SpecVersion returns the RPC protocol version implemented by this World.
SpecVersion() int
// DeploymentID returns the deployment whose code this World's deliveries
// execute, or "" when the World has no deployments.
DeploymentID() string
}| Part | Responsibility |
|---|---|
Storage | Append-only event logs and the run, step, and hook views derived from them. |
Queue | Durable deliveries with a not-before time. |
Streamer | Named, ordered byte streams with exclusive writer sessions. |
Analytics | Optional reporting queries across runs. |
Key requirements
- Atomic admission.
EventsCreateandEventsCreateBatchmust check the transition rules and append in one atomic operation. A batch is all-or-nothing. This is what makes duplicate deliveries safe. - Idempotent repeats. An event that repeats an admitted transition, such as a second
run_started,step_created, orhook_disposed, returns the earlier event withAllocatedEventCountzero instead of appending. Astep_startedfor a running step is not a repeat: it appends a new attempt. - One copy of each payload. The built-in Worlds keep run, step, and hook payloads only in their events. Their views store the slot of the event that holds each one, and reads decode it with
worlds.EventPayload. A payload then only has to fit in one event, and results are written once. - Step results need a running step. Refuse
step_completed,step_failed, andstep_retryingwithErrStepNotStartableunless the step is running, andhook_disposedwithErrHookNotActiveunless the hook belongs to the run. - Exclusive hook tokens.
EventsCreate(hook_created)must give a token to exactly one active hook, and withForcetake it over and appendhook_disposedwithForceClaimedByto the previous owner's run. - One webhook response. Refuse a
hook_receivedcarrying a response withErrHookNotActiveonce the webhook has completed, including whileTokenRetentionUntilkeeps its token, in the same atomic step as the append. - The in-band fence. Keep a per-run
InBandCount. An append that carriesInBandCount(inEventsCreateOptions, or onEventsCreateBatchInput) is accepted only when it equals the run's count, in the same atomic step as the append, and advances it by the events appended. A mismatch returnsErrInBandSupersededand appends nothing. This is what keeps two flow deliveries of one run from both declaring the same operation. - At-least-once delivery.
Queuemust not deliver beforeNotBefore, and must redeliver failed deliveries. Deduplicating byIdempotencyKeyis optional. Store everyQueueInputfield and hand it back unchanged, includingReplayDivergenceCountandDeploymentMismatchRetryCount: the runtime counts its recovery deliveries in them. - Executing deliveries. Whatever consumes your queue calls
worlds.NewRuntime(world, runners).Execute(ctx, input)for each delivery, acknowledges it whenExecutereturns nil, and retries it otherwise.
Supporting retention is optional. A World that supports it takes Retention and Retentions in its configuration, resolves each run's retention with worlds.RetentionFor from the run's workflow name, and deletes everything recorded for a run, including its index entries and its hooks' tokens, only once the run is terminal, the retention has passed since it completed, and no hook of the run still retains its token.
See the Event Log reference for event types and the worlds reference for every method's contract.
Serving it
handler := worlds.HTTPHandler(world, "") // serves /v1/workflow/rpc and /v1/workflow/webhooks/{token}
rpc := worlds.RPCHandler(world)
webhooks := worlds.WebhookHandler(world)Conformance
The workflowtest package runs the World contract against your implementation. It is the same suite every built-in World passes.
package myworld_test
import (
"testing"
"github.com/eriicafes/go-workflow/workflowtest"
"github.com/eriicafes/go-workflow/worlds"
)
func TestConformance(t *testing.T) {
workflowtest.Conformance(t, func(t *testing.T) worlds.World {
return newIsolatedWorld(t) // fresh state per call
})
}
func TestConformanceShared(t *testing.T) {
workflowtest.ConformanceShared(t, func(t *testing.T) (worlds.World, worlds.World) {
a, b := newWorldPairOnSameBackend(t)
return a, b
})
}Conformancegives each subtest a fresh World fromnewWorld. Worlds backed by shared storage must isolate state per call, for example with a unique table, prefix, schema, or directory.ConformanceSharedcovers behavior across instances: concurrent appends, hook claims, and stream writers.newPairreturns two Worlds backed by the same storage, as two processes would be.
Using it from JavaScript
Any World served by worlds.HTTPHandler can be used by the Workflow SDK through the world-http adapter.