Documentation
¶
Overview ¶
Package duro is a durable dataflow DSL: reactive pipelines whose every stage runs as a checkpointed DBOS step. Pipelines are powered by samber/ro internally, but the public API only accepts duro stages, making it a compile error to insert a raw ro operator that could break durability.
A workflow body is a pipe of typed stages:
func OrderWorkflow(ctx duro.Context, o Order) (Confirmation, error) {
return duro.Run(ctx, o, duro.Pipe4(
duro.Step("validate", validateOrder),
duro.Step("reserve", reserveInventory),
duro.Step("charge", chargePayment, duro.WithMaxRetries(3)),
duro.Step("notify", sendConfirmation),
))
}
Durability relies on DBOS replay determinism: on recovery the workflow function re-executes and the Nth step call must be the same logical operation as in the original run. duro enforces this with three layers:
- Compile time: PipeN only accepts Stage values, which can only be built by this package's constructors. Concurrent or time-based ro operators cannot be expressed. Sources cannot be swapped either — Run feeds the workflow input through ro.Of internally.
- Construction time: Run checkpoints the pipeline's shape (ordered stage kinds and names) as a hidden first step named "duro.shape". If a replay constructs a different shape — non-deterministic pipeline construction, changed code — Run fails immediately instead of letting stages read misaligned checkpoints.
- Execution time: every stage asserts it runs on the goroutine the pipeline was subscribed on, failing fast if an operator smuggled in concurrency; and once any stage fails, a shared abort flag prevents items behind the failure from executing further stages (fail-fast, like sequential workflow code).
Escape hatches: Pure wraps a deterministic, side-effect-free transform that is NOT checkpointed (it re-executes on every replay — it must be pure), and UnsafeOperator admits an arbitrary ro operator with no safety guarantees beyond the runtime guards. Both participate in the shape fingerprint.
For parallelism, use FanOut (child workflows on a DBOS queue — bounded, distributed, per-child durability) or Parallel (concurrent steps in-process via dbos.Go — lightweight, no queue). Both preserve replay determinism: work is spawned and awaited in stream order on the workflow goroutine.
The rest of DBOS's workflow toolkit is available as stages: Delay (durable sleep), Send/Recv (durable mailbox messaging — external signals and human-in-the-loop pauses), SetEvent/GetEvent (progress events published and read durably), and ToStream/FromStream (durable streams written and drained durably). Messaging goes through typed channels — Topic, Event, Stream — declared once and referenced by both sides, so keys and payload types cannot drift; declare a channel with Portable() to serialize its payloads in DBOS's cross-language format.
Control flow is durable too: Branch and Switch route each item through embedded pipelines by a checkpointed decision, Loop repeats a pipeline until a checkpointed verdict says done, Sub embeds a pipeline as one named stage, and Collect folds the stream into a slice. Embedded pipelines are part of the shape fingerprint.
Pipelines are also registrable as first-class workflows: Register names a pipeline as a DBOS workflow, RegisterScheduled runs one on a cron schedule (typed Pipeline[time.Time, R]), RegisterDebounced collapses bursts of triggers into a single run, and RegisterWorkflow covers hand-written workflow functions. Runs are tracked by workflow ID from any process: Status/StatusAll reconcile a persisted ID against the engine, Attach reconnects to a live handle, and ForkFromStage restarts a completed or failed run from a named stage — optionally onto a different application version.
Example ¶
Example shows a DBOS workflow written as a durable pipeline: each stage runs as a checkpointed DBOS step, so a crashed workflow resumes after the last completed stage. Register the workflow with dbos.RegisterWorkflow and start it with dbos.RunWorkflow as usual.
package main
import (
"context"
"github.com/dbos-inc/dbos-transact-golang/dbos"
"github.com/lemonberrylabs/duro"
)
func main() {
type Order struct {
ID string
AmountCents int
}
type Receipt struct {
OrderID string
PaymentID string
}
chargeOrder := func(ctx dbos.DBOSContext, o Order) (Receipt, error) {
return duro.Run(ctx, o, duro.Pipe2(
duro.Step("charge", func(_ context.Context, o Order) (string, error) {
return "pay-" + o.ID, nil // call your payment provider here
}, duro.WithMaxRetries(3)),
duro.Step("receipt", func(_ context.Context, paymentID string) (Receipt, error) {
return Receipt{OrderID: o.ID, PaymentID: paymentID}, nil
}),
))
}
_ = chargeOrder
}
Output:
Index ¶
- Constants
- Variables
- func RegisterQueues(ctx Context, queues ...Queue) error
- func Run[P, R any](ctx Context, in P, p Pipeline[P, R]) (R, error)
- func RunAll[P, R any](ctx Context, in P, p Pipeline[P, R]) ([]R, error)
- func WithWorkflowID(id string) dbos.WorkflowOption
- type App
- type Case
- type ChannelOption
- type ChildOption
- func WithChildAppVersion(version string) ChildOption
- func WithChildAssumedRole(role string) ChildOption
- func WithChildAuthenticatedRoles(roles ...string) ChildOption
- func WithChildAuthenticatedUser(user string) ChildOption
- func WithChildDeduplicationID[T any](fn func(in T) string) ChildOption
- func WithChildDeduplicationPolicy(policy DeduplicationPolicy) ChildOption
- func WithChildDelay(d time.Duration) ChildOption
- func WithChildID[T any](fn func(in T) string) ChildOption
- func WithChildPartitionKey[T any](fn func(in T) string) ChildOption
- func WithChildPriority(priority uint) ChildOption
- func WithChildTimeout(d time.Duration) ChildOption
- func WithPortableChildren() ChildOption
- type Config
- type Context
- type Debouncer
- type DeduplicationPolicy
- type Event
- type Fork
- type Handle
- type Pipeline
- func Pipe1[A, B any](s1 Stage[A, B]) Pipeline[A, B]
- func Pipe2[A, B, C any](s1 Stage[A, B], s2 Stage[B, C]) Pipeline[A, C]
- func Pipe3[A, B, C, D any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D]) Pipeline[A, D]
- func Pipe4[A, B, C, D, E any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E]) Pipeline[A, E]
- func Pipe5[A, B, C, D, E, F any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F]) Pipeline[A, F]
- func Pipe6[A, B, C, D, E, F, G any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F], ...) Pipeline[A, G]
- func Pipe7[A, B, C, D, E, F, G, H any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F], ...) Pipeline[A, H]
- func Pipe8[A, B, C, D, E, F, G, H, I any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F], ...) Pipeline[A, I]
- type PipelineWorkflow
- type Queue
- type QueueOption
- type RegisteredWorkflow
- type RunStatus
- type Stage
- func Branch[T, R any](name string, pred func(ctx context.Context, in T) (bool, error), ...) Stage[T, R]
- func Collect[T any](name string, opts ...StepOption) Stage[T, []T]
- func Delay[T any](name string, d time.Duration) Stage[T, T]
- func Expand[T, R any](name string, fn func(ctx context.Context, in T) ([]R, error), ...) Stage[T, R]
- func FanOut[T, R any](name string, queue Queue, wf WorkflowRef[T, R], opts ...ChildOption) Stage[T, R]
- func Filter[T any](name string, pred func(ctx context.Context, in T) (bool, error), ...) Stage[T, T]
- func FromStream[T, V any](name string, stream Stream[V], fn func(in T) (workflowID string), ...) Stage[T, V]
- func GetEvent[T, V any](name string, event Event[V], fn func(in T) (workflowID string), ...) Stage[T, V]
- func Loop[T any](name string, body Pipeline[T, T], ...) Stage[T, T]
- func Parallel[T, R any](name string, maxConcurrent int, fn func(ctx context.Context, in T) (R, error), ...) Stage[T, R]
- func Pure[T, R any](name string, fn func(in T) R) Stage[T, R]
- func Recv[T, M any](name string, topic Topic[M], timeout time.Duration) Stage[T, M]
- func Reduce[T, A any](name string, fn func(ctx context.Context, acc A, in T) (A, error), seed A, ...) Stage[T, A]
- func Send[T, M any](name string, topic Topic[M], ...) Stage[T, T]
- func SetEvent[T, V any](name string, event Event[V], fn func(in T) V) Stage[T, T]
- func Step[T, R any](name string, fn func(ctx context.Context, in T) (R, error), opts ...StepOption) Stage[T, R]
- func Sub[T, R any](name string, p Pipeline[T, R]) Stage[T, R]
- func Switch[T, R any](name string, route func(ctx context.Context, in T) (string, error), ...) Stage[T, R]
- func Tap[T any](name string, fn func(ctx context.Context, in T) error, opts ...StepOption) Stage[T, T]
- func ToStream[T any](name string, stream Stream[T]) Stage[T, T]
- func UnsafeOperator[T, R any](name string, op func(ro.Observable[T]) ro.Observable[R]) Stage[T, R]
- type State
- type StepOption
- type Stream
- type Topic
- type WorkflowFunc
- type WorkflowRef
Examples ¶
Constants ¶
const ( // DeduplicationReject (the default) fails the enqueue of a child whose // deduplication ID is already held by an active child. DeduplicationReject = dbos.DeduplicationPolicyReject // DeduplicationReturnExisting returns the existing child's handle // instead, so both items observe the first child's result. DeduplicationReturnExisting = dbos.DeduplicationPolicyReturnExisting )
const ShapeStepName = "duro.shape"
ShapeStepName is the name of the hidden bookkeeping step Run records as the pipeline's first checkpoint. It holds the pipeline's shape fingerprint, which Run compares on replay to fail fast on non-deterministic pipeline construction.
Variables ¶
var ErrAborted = errors.New("duro: pipeline aborted by an earlier stage failure")
ErrAborted marks stage executions skipped because an earlier stage already failed. It never surfaces from Run: the first failure is the pipeline's error, and ErrAborted only travels through already-terminated downstream observers.
var ErrNoValue = errors.New("duro: pipeline completed without emitting a value")
ErrNoValue is returned by Run when the pipeline completes without emitting any value (for example, when a Filter stage drops every item).
var ErrRunNotFound = errors.New("duro: run not found")
ErrRunNotFound is returned by Status and Attach for an unknown workflow ID.
Functions ¶
func RegisterQueues ¶ added in v0.2.0
RegisterQueues registers declared queues with DBOS. Register does this automatically for every queue its pipeline references; call RegisterQueues yourself only for pipelines run directly with Run/RunAll inside hand-written workflows.
func Run ¶
Run executes the pipeline durably inside a DBOS workflow, feeding it the input value and blocking until completion. It returns the last emitted value, the first stage error, or ErrNoValue if the pipeline emits nothing. Call it as the body of a registered DBOS workflow function.
func RunAll ¶
RunAll is Run for pipelines whose final stage legitimately emits multiple items: it returns every emitted value.
func WithWorkflowID ¶ added in v0.2.0
func WithWorkflowID(id string) dbos.WorkflowOption
WithWorkflowID assigns a run's workflow ID — the standard idempotency key: starting the same ID twice re-attaches to the first run instead of running again. Every other dbos.WorkflowOption passes through Start unchanged.
Types ¶
type App ¶ added in v0.2.0
type App struct {
dbos.DBOSContext
// contains filtered or unexported fields
}
App owns the DBOS lifecycle so applications never touch it directly:
app, err := duro.New(ctx, duro.Config{Name: "orders", DatabaseURL: url})
wf := duro.Register(app, "invoice", invoicePipeline) // register everything...
err = app.Launch() // ...then launch
defer app.Shutdown(5 * time.Second)
handle, err := wf.Start(app, batch)
Launch also checks for stranded runs: in-flight workflows recorded under names no longer registered (a renamed pipeline) are reported as warnings instead of silently never recovering.
*App satisfies Context, so it can be passed wherever duro expects one. Calling raw dbos package functions directly is different: several inspect the concrete context type, so hand them Context() rather than the App itself.
func New ¶ added in v0.2.0
New initializes the application. Register pipelines and queues after New and before Launch.
func (*App) Context ¶ added in v0.2.0
Context returns the underlying DBOS context — for calling raw dbos package functions directly. Everything in duro accepts the App itself.
type Case ¶ added in v0.4.0
type Case[T, R any] struct { // contains filtered or unexported fields }
Case pairs a route key with the pipeline that handles it; see Switch.
type ChannelOption ¶ added in v0.2.0
type ChannelOption func(*channelConfig)
ChannelOption configures a declared channel.
func Portable ¶ added in v0.2.0
func Portable() ChannelOption
Portable makes every payload written through the channel serialize in DBOS's cross-language portable JSON format, so non-Go DBOS applications (Python, TypeScript) can consume it. Readers need nothing special — DBOS decodes by each value's recorded serialization.
type ChildOption ¶ added in v0.2.0
type ChildOption func(*childConfig)
ChildOption configures the child workflows a FanOut stage enqueues. Policy options (priority, delay, timeout, auth, serialization, version) apply uniformly to every child; identity options (workflow ID, deduplication ID, partition key) derive a per-child value from the stream item. Item-derived options are typed by the stage's item type — FanOut panics at construction time if they were built for a different type.
func WithChildAppVersion ¶ added in v0.2.0
func WithChildAppVersion(version string) ChildOption
WithChildAppVersion pins children to a specific application version, overriding the parent's. This affects which executors recover them.
func WithChildAssumedRole ¶ added in v0.2.0
func WithChildAssumedRole(role string) ChildOption
WithChildAssumedRole records the assumed role on every child workflow's status.
func WithChildAuthenticatedRoles ¶ added in v0.2.0
func WithChildAuthenticatedRoles(roles ...string) ChildOption
WithChildAuthenticatedRoles records the authenticated roles on every child workflow's status.
func WithChildAuthenticatedUser ¶ added in v0.2.0
func WithChildAuthenticatedUser(user string) ChildOption
WithChildAuthenticatedUser records the authenticated user on every child workflow's status.
func WithChildDeduplicationID ¶ added in v0.2.0
func WithChildDeduplicationID[T any](fn func(in T) string) ChildOption
WithChildDeduplicationID derives a queue deduplication ID from each item. While a child holding the ID is active on the queue, enqueueing another with the same ID is rejected — or returns the existing child's handle under dbos.DeduplicationPolicyReturnExisting (see WithChildDeduplicationPolicy).
func WithChildDeduplicationPolicy ¶ added in v0.2.0
func WithChildDeduplicationPolicy(policy DeduplicationPolicy) ChildOption
WithChildDeduplicationPolicy sets how a colliding deduplication ID is handled (default DeduplicationReject).
func WithChildDelay ¶ added in v0.2.0
func WithChildDelay(d time.Duration) ChildOption
WithChildDelay delays each child's dequeue by d: children start in the DELAYED status and become runnable once the delay expires.
func WithChildID ¶ added in v0.2.0
func WithChildID[T any](fn func(in T) string) ChildOption
WithChildID derives each child's workflow ID from its item, making child runs idempotent under an application-level key (e.g. an order ID): starting the same pipeline twice re-attaches to the same children instead of spawning duplicates. Without it, child IDs derive from the parent's step counter, which is idempotent per parent run but not across runs.
func WithChildPartitionKey ¶ added in v0.2.0
func WithChildPartitionKey[T any](fn func(in T) string) ChildOption
WithChildPartitionKey derives each child's queue partition key from its item. The queue must be registered with dbos.WithPartitionQueue; each partition then gets its own concurrency limits.
func WithChildPriority ¶ added in v0.2.0
func WithChildPriority(priority uint) ChildOption
WithChildPriority sets every child's queue priority (lower runs first). The queue must be registered with dbos.WithPriorityEnabled.
func WithChildTimeout ¶ added in v0.2.0
func WithChildTimeout(d time.Duration) ChildOption
WithChildTimeout gives every child a durable workflow deadline of d from its enqueue time: the deadline is stored with the child's status, survives recovery, and cancels the child when it expires. A timed-out child fails the pipeline when its result is awaited.
func WithPortableChildren ¶ added in v0.2.0
func WithPortableChildren() ChildOption
WithPortableChildren stores each child's inputs, step outputs, events, messages, and streams in DBOS's cross-language portable JSON format, so non-Go DBOS applications can read them.
type Config ¶ added in v0.2.0
type Config struct {
// Name identifies the application in the system database.
Name string
// DatabaseURL is the Postgres URL of the DBOS system database.
DatabaseURL string
// Logger receives duro and DBOS logs; slog.Default() when nil.
Logger *slog.Logger
}
Config configures a duro application.
type Context ¶ added in v0.2.0
type Context = dbos.DBOSContext
Context is the durable execution context every workflow runs under: it carries the checkpoint state duro's stages record to. It is DBOS's context type under a duro name (a type alias), so it satisfies context.Context and remains directly usable with any dbos API — but declaring and running workflows never requires importing dbos:
func Process(ctx duro.Context, job Job) (Result, error)
type Debouncer ¶ added in v0.2.0
type Debouncer[P, R any] struct { // contains filtered or unexported fields }
Debouncer collapses bursts of pipeline starts into a single run; see RegisterDebounced.
func RegisterDebounced ¶ added in v0.2.0
func RegisterDebounced[P, R any](ctx Context, name string, p Pipeline[P, R], opts ...dbos.DebouncerOption) *Debouncer[P, R]
RegisterDebounced registers the pipeline as a workflow and returns its debouncer. Cap the total postponement with dbos.WithDebouncerTimeout. Like Register, call it after New and before Launch.
func (*Debouncer[P, R]) Debounce ¶ added in v0.2.0
func (d *Debouncer[P, R]) Debounce(ctx Context, key string, delay time.Duration, input P) (Handle[R], error)
Debounce postpones the pipeline's start by delay. Every further call with the same key pushes the start back and replaces the input; when the delay lapses, the pipeline runs once with the last input. Different keys debounce independently. Every call returns a handle to the same eventual run.
type DeduplicationPolicy ¶ added in v0.2.0
type DeduplicationPolicy = dbos.DeduplicationPolicy
DeduplicationPolicy controls how a colliding child deduplication ID is handled; see WithChildDeduplicationPolicy.
type Event ¶ added in v0.2.0
type Event[V any] struct { // contains filtered or unexported fields }
Event is a typed key-value event channel: a value of type V published on a workflow under one key. Write with a SetEvent stage; read with a GetEvent stage or Event.Get.
var Progress = duro.NewEvent[int]("last-item")
func NewEvent ¶ added in v0.2.0
func NewEvent[V any](key string, opts ...ChannelOption) Event[V]
NewEvent declares a typed event key.
type Fork ¶ added in v0.2.0
type Fork struct {
WorkflowID string // the pipeline run to fork
Stage string // the stage to restart from (its first execution, for stages that ran per item)
// ForkedID names the forked run; auto-generated when empty.
ForkedID string
// ApplicationVersion pins the forked run to a different code version —
// the recovery tool for rerunning a workflow on fixed code after a bad
// deploy.
ApplicationVersion string
// Queue enqueues the forked run on the named queue instead of running it
// on the internal one; QueuePartitionKey partitions it there.
Queue string
QueuePartitionKey string
}
Fork describes where to restart an existing pipeline run. WorkflowID and Stage are required; every other field is optional and its zero value means "inherit from the original run".
type Handle ¶ added in v0.2.0
type Handle[R any] struct { // contains filtered or unexported fields }
Handle tracks one durable pipeline run. It is returned by Start and ForkFromStage; Result blocks until the run completes.
func Attach ¶ added in v0.4.0
Attach reconnects to an existing run by workflow ID and returns its handle — how a restarted process awaits a result instead of just polling Status. R must match the workflow's result type.
func ForkFromStage ¶ added in v0.2.0
ForkFromStage restarts an existing pipeline run from a named stage: stages before it replay from the original run's checkpoints, the named stage and everything after re-execute. The pipeline's shape guard replays too, so a fork onto changed pipeline code fails fast instead of misreading checkpoints.
type Pipeline ¶
type Pipeline[P, R any] struct { // contains filtered or unexported fields }
Pipeline is a composed chain of stages from input P to result R. Pipelines are immutable and stateless: build them once (package level is fine) and run them from any workflow with Run or RunAll.
func Pipe4 ¶
func Pipe4[A, B, C, D, E any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E]) Pipeline[A, E]
Pipe4 composes a pipeline from 4 stages.
func Pipe5 ¶
func Pipe5[A, B, C, D, E, F any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F]) Pipeline[A, F]
Pipe5 composes a pipeline from 5 stages.
func Pipe6 ¶
func Pipe6[A, B, C, D, E, F, G any](s1 Stage[A, B], s2 Stage[B, C], s3 Stage[C, D], s4 Stage[D, E], s5 Stage[E, F], s6 Stage[F, G]) Pipeline[A, G]
Pipe6 composes a pipeline from 6 stages.
type PipelineWorkflow ¶ added in v0.2.0
type PipelineWorkflow[P, R any] struct { // contains filtered or unexported fields }
PipelineWorkflow is a pipeline registered as a DBOS workflow. Every registration shares one generic runner method, so DBOS's configured instance mechanism keys each registration by the pipeline's name — that is what ConfigName returns, and why runs must go through Start (which selects this instance) rather than a bare dbos.RunWorkflow.
func Register ¶ added in v0.2.0
func Register[P, R any](ctx Context, name string, p Pipeline[P, R], opts ...dbos.WorkflowRegistrationOption) *PipelineWorkflow[P, R]
Register turns a pipeline into a registered DBOS workflow under the given name, and registers every queue the pipeline references. Call it after New and before Launch; run the result with Start. Workflow-level registration options (recovery attempts via dbos.WithMaxRetries, ...) pass through opts.
The name is the pipeline's durable identity: in-flight runs are recovered by looking it up, so it must be registered on every process start. Launch warns about runs whose name is no longer registered.
func RegisterScheduled ¶ added in v0.2.0
func RegisterScheduled[R any](ctx Context, name, cronSchedule string, p Pipeline[time.Time, R], opts ...dbos.WorkflowRegistrationOption) *PipelineWorkflow[time.Time, R]
RegisterScheduled registers the pipeline as a scheduled (cron) workflow: every tick starts a durable run whose input is the scheduled time. The schedule uses cron syntax with seconds precision ("*/30 * * * * *" = every 30 seconds). Requiring Pipeline[time.Time, R] makes the DBOS rule that scheduled workflows take a time.Time input a compile-time guarantee.
func (*PipelineWorkflow[P, R]) ConfigName ¶ added in v0.2.0
func (w *PipelineWorkflow[P, R]) ConfigName() string
ConfigName implements dbos.ConfiguredInstance: the workflow name uniquely keys this pipeline's registration.
func (*PipelineWorkflow[P, R]) Start ¶ added in v0.2.0
func (w *PipelineWorkflow[P, R]) Start(ctx Context, in P, opts ...dbos.WorkflowOption) (Handle[R], error)
Start runs (or, with dbos.WithQueue, enqueues) the pipeline as a durable workflow and returns its handle. It accepts any dbos.WorkflowOption — workflow ID, queue, priority, deduplication, auth. For a durable deadline, pass a context derived with dbos.WithTimeout.
type Queue ¶ added in v0.2.0
type Queue struct {
// contains filtered or unexported fields
}
Queue is a declared DBOS workflow queue. Declare it once (package level is fine) and reference the value everywhere it is used — the queue's name lives in exactly one place, so writer and reader can never drift:
var Jobs = duro.NewQueue("jobs", duro.WithConcurrency(4))
...
duro.FanOut("process", Jobs, duro.Workflow(ProcessJob))
Queues referenced by a pipeline are registered automatically when the pipeline is registered with Register; pipelines run directly with Run/RunAll (no Register) need RegisterQueues before workflows start enqueueing.
func NewQueue ¶ added in v0.2.0
func NewQueue(name string, opts ...QueueOption) Queue
NewQueue declares a queue. Declaring is side-effect free; registration happens through Register (automatic for the pipeline's queues) or RegisterQueues.
type QueueOption ¶ added in v0.2.0
type QueueOption func(*queueConfig)
QueueOption configures a declared queue.
func WithConcurrency ¶ added in v0.2.0
func WithConcurrency(n int) QueueOption
WithConcurrency caps how many workflows from the queue run concurrently across all executors.
func WithPartitions ¶ added in v0.2.0
func WithPartitions() QueueOption
WithPartitions makes the queue partitioned: children enqueued with WithChildPartitionKey get per-partition concurrency limits.
func WithPriorities ¶ added in v0.2.0
func WithPriorities() QueueOption
WithPriorities enables priority scheduling: children enqueued with WithChildPriority run lowest-number-first.
func WithRateLimit ¶ added in v0.2.0
func WithRateLimit(limit int, period time.Duration) QueueOption
WithRateLimit caps how many workflows may start within each period — backpressure for external services.
func WithWorkerConcurrency ¶ added in v0.2.0
func WithWorkerConcurrency(n int) QueueOption
WithWorkerConcurrency caps how many workflows from the queue a single executor runs concurrently.
type RegisteredWorkflow ¶ added in v0.3.0
type RegisteredWorkflow[P, R any] struct { // contains filtered or unexported fields }
RegisteredWorkflow is a hand-written workflow function registered under a durable name; see RegisterWorkflow. It is a WorkflowRef, so it passes directly as a FanOut child.
func RegisterWorkflow ¶ added in v0.3.0
func RegisterWorkflow[P, R any](ctx Context, name string, fn WorkflowFunc[P, R], opts ...dbos.WorkflowRegistrationOption) *RegisteredWorkflow[P, R]
RegisterWorkflow registers a hand-written workflow function under the given name. Reach for it only when a workflow needs imperative control flow around its pipelines — branching between them, looping, post-processing a RunAll — since Register covers pipelines themselves. Call it before Launch; the name is the workflow's durable identity, so register the same function under the same name on every process start. Workflow-level registration options pass through opts.
func (*RegisteredWorkflow[P, R]) Start ¶ added in v0.3.0
func (w *RegisteredWorkflow[P, R]) Start(ctx Context, in P, opts ...dbos.WorkflowOption) (Handle[R], error)
Start runs (or, with dbos.WithQueue, enqueues) the workflow and returns its handle. It accepts any dbos.WorkflowOption, like PipelineWorkflow's Start.
type RunStatus ¶ added in v0.4.0
type RunStatus struct {
ID string
Name string // registered workflow (pipeline) name
State State
// Err is the recorded failure for failed runs — nil otherwise. Cancelled
// and retries-exceeded runs that recorded no error get a synthesized one,
// so Err is always non-nil when State.Failed().
Err error
CreatedAt time.Time
UpdatedAt time.Time
CompletedAt time.Time // zero until terminal
ApplicationVersion string
ForkedFrom string // original run's ID when this run was forked
}
RunStatus is the cheap status view of a run: no input or output payloads are loaded or deserialized, making it safe for polling paths.
func Status ¶ added in v0.4.0
Status fetches a run's current status by workflow ID — the reconcile primitive for consumers that persist run IDs and check on them later. It works from any process attached to the same system database; no Handle needed.
func StatusAll ¶ added in v0.4.0
StatusAll is the batch form of Status: it returns the status of every listed run that exists, in the requested order, silently omitting unknown IDs (compare lengths to detect them).
The common case is one payload-free query. DBOS stores a run's failure message alongside its output, so when the batch contains failed runs their recorded errors are fetched in a second query scoped to just those runs — healthy polling stays cheap, failure reasons still surface.
type Stage ¶
type Stage[T, R any] struct { // contains filtered or unexported fields }
Stage is one typed pipeline segment. Stages are nominal: only this package's constructors can build them, which is what keeps arbitrary ro operators out of durable pipelines at compile time.
func Branch ¶ added in v0.4.0
func Branch[T, R any](name string, pred func(ctx context.Context, in T) (bool, error), then, els Pipeline[T, R], opts ...StepOption) Stage[T, R]
Branch is durable two-way dispatch: the predicate runs as a checkpointed step and each item flows through then or els accordingly. Both arms must produce the same output type — the compiler holds routing honest.
func Collect ¶ added in v0.4.0
func Collect[T any](name string, opts ...StepOption) Stage[T, []T]
Collect folds the stream into a slice of every item, in order — the standard final stage for a registered pipeline that should return all values rather than the last one. An empty stream yields an empty slice.
func Delay ¶ added in v0.2.0
Delay is a durable pause: each item passing through sleeps for d via dbos.Sleep, which checkpoints the wake-up deadline — a workflow recovered mid-sleep resumes sleeping only for the remaining time, and a replayed sleep completes instantly. Use it for pacing between durable stages; remember it applies per item on multi-item streams.
func Expand ¶
func Expand[T, R any](name string, fn func(ctx context.Context, in T) ([]R, error), opts ...StepOption) Stage[T, R]
Expand is a durable one-to-many transform (a flattening FlatMap): fn runs as a checkpointed DBOS step and each element of its result is emitted downstream in order.
func FanOut ¶ added in v0.2.0
func FanOut[T, R any](name string, queue Queue, wf WorkflowRef[T, R], opts ...ChildOption) Stage[T, R]
FanOut is a durable parallel map: each item starts the referenced workflow as a child on the queue, and once the stream completes, results are awaited and emitted downstream in input order. Parallelism, rate limits, and distribution across processes are governed entirely by the queue's declaration:
var Jobs = duro.NewQueue("jobs", duro.WithConcurrency(4))
...
duro.Pipe3(
duro.Expand("explode", split),
duro.FanOut("process", Jobs, duro.Workflow(ProcessJob)),
duro.Reduce("merge", merge, seed),
)
The child can be a hand-written DBOS workflow (wrap it with Workflow) or a registered pipeline (pass the *PipelineWorkflow directly). Child workflows are configured with ChildOptions — identity (WithChildID, WithChildDeduplicationID, WithChildPartitionKey), scheduling (WithChildPriority, WithChildDelay, WithChildTimeout), and metadata (WithChildAuthenticatedUser, WithChildAppVersion, WithPortableChildren).
FanOut is the sanctioned form of concurrency inside a duro pipeline: it is deterministic because children are enqueued in stream order (child workflow IDs derive from the parent's step counter, so a recovered parent re-attaches to its children instead of spawning duplicates) and awaited in that same order (each result is checkpointed in the parent). Every child is itself a durable workflow.
On the first child failure, FanOut fails the pipeline with that child's error. Children queued behind it are independent durable workflows and run to completion in the background; cancel them with dbos.CancelWorkflows if that is not what you want.
func Filter ¶
func Filter[T any](name string, pred func(ctx context.Context, in T) (bool, error), opts ...StepOption) Stage[T, T]
Filter is a durable filter: the predicate runs as a checkpointed DBOS step, so effectful or non-deterministic predicates still replay consistently on recovery. Items for which the predicate returns false are dropped.
func FromStream ¶ added in v0.2.0
func FromStream[T, V any](name string, stream Stream[V], fn func(in T) (workflowID string), opts ...StepOption) Stage[T, V]
FromStream durably drains the stream written by another workflow: fn derives the source workflow ID from the item, the stream is read to its close (blocking while the writer is still active), and each value is emitted downstream in order — the item itself is discarded, like Recv. The whole read runs as one checkpointed step, so a recovered workflow replays the values it already collected instead of re-reading a stream that may have changed. Pair it with WithTimeout to bound how long the stage waits for the writer to finish.
func GetEvent ¶ added in v0.2.0
func GetEvent[T, V any](name string, event Event[V], fn func(in T) (workflowID string), timeout time.Duration) Stage[T, V]
GetEvent durably reads the event published by another workflow: fn derives the source workflow ID from the item, and the event's value is emitted downstream in place of the item (reshape beforehand if you need both). The read blocks until the event is set or the timeout elapses, and is checkpointed — a recovered workflow replays the value it already observed instead of re-reading.
func Loop ¶ added in v0.4.0
func Loop[T any](name string, body Pipeline[T, T], until func(ctx context.Context, in T) (bool, error), opts ...StepOption) Stage[T, T]
Loop durably repeats the body pipeline until the until predicate — a checkpointed step — reports done, then emits the final value. Each iteration feeds the body's last emitted value back in (a body that emits nothing drops the item, like Filter). On replay the recorded predicate verdicts reproduce the exact iteration count. Pair the body with Delay for durable polling. Applied per item on multi-item streams.
Iterations are unbounded, and a durable loop is more durable than a bug deserves: a predicate that can never report done keeps checkpointing and resumes across restarts. Give the loop a natural bound (track attempts in T and fail past a limit), or stop a runaway run with dbos.CancelWorkflow.
func Parallel ¶ added in v0.2.0
func Parallel[T, R any](name string, maxConcurrent int, fn func(ctx context.Context, in T) (R, error), opts ...StepOption) Stage[T, R]
Parallel is a durable parallel Step: items execute fn concurrently as DBOS steps within the workflow process, at most maxConcurrent at a time (unbounded if maxConcurrent <= 0), and results are emitted downstream in input order once the stream completes.
Parallel is the in-process, lightweight sibling of FanOut: no queue and no child workflows, just concurrent steps inside the current workflow. Use FanOut when work should distribute across processes, survive independently, or obey queue-level rate limits; use Parallel when a bounded burst of concurrent steps in this process is enough.
Determinism is preserved because each step's ID is assigned on the workflow goroutine at launch time, in stream order (dbos.Go exists precisely for this), and outcomes are collected in that same order. On recovery, completed steps replay from their checkpoints without re-running fn.
If any step fails, results for items before the failure are still emitted downstream (matching sequential fail-fast semantics), all remaining steps are drained, and the pipeline fails with the first error in input order.
func Pure ¶
Pure is a non-durable transform: fn is NOT checkpointed and re-executes on every replay, so it must be deterministic and side-effect free. Use it for cheap reshaping between durable stages; anything effectful or fallible belongs in Step.
func Recv ¶ added in v0.2.0
Recv durably waits for the next message on the topic and emits it downstream, consuming one message per upstream item (the item itself is discarded — reshape beforehand if you need it). Receipt is checkpointed, so a recovered workflow does not consume a second message. A zero or negative timeout means dbos.Recv's no-wait behavior; if no message arrives in time, the stage fails the pipeline.
Recv is how a pipeline pauses for an external signal — a payment confirmation, a human approval — sent to this workflow's ID with a Send stage or Topic.Send from anywhere.
func Reduce ¶
func Reduce[T, A any](name string, fn func(ctx context.Context, acc A, in T) (A, error), seed A, opts ...StepOption) Stage[T, A]
Reduce is a durable fold: each accumulation runs as a checkpointed DBOS step, and the final accumulator is emitted when the source completes.
func Send ¶ added in v0.2.0
func Send[T, M any](name string, topic Topic[M], fn func(in T) (destinationID string, message M, err error)) Stage[T, T]
Send durably sends one message per item on the topic (dbos.Send is checkpointed, so a recovered workflow does not re-send). fn derives the destination workflow ID and the message from the item, which passes through unchanged. The message type is the topic's — a mismatch with the receiving side is a compile error.
func SetEvent ¶ added in v0.2.0
SetEvent durably publishes the event on the workflow for each item and passes the item through unchanged. Read it with a GetEvent stage or Event.Get — the classic use is exposing pipeline progress to the outside world while the workflow runs.
func Step ¶
func Step[T, R any](name string, fn func(ctx context.Context, in T) (R, error), opts ...StepOption) Stage[T, R]
Step is a durable Map: it transforms each item by running fn as a checkpointed DBOS step. On recovery, completed executions are replayed from the database instead of re-running fn.
func Sub ¶ added in v0.4.0
Sub embeds a pipeline as a single named stage — reuse a pipeline segment across pipelines without a wrapper workflow. Unlike Branch/Switch/Loop, Sub applies to the whole stream, not per item: an embedded Reduce folds everything flowing through it.
func Switch ¶ added in v0.4.0
func Switch[T, R any](name string, route func(ctx context.Context, in T) (string, error), cases ...Case[T, R]) Stage[T, R]
Switch is durable multi-way dispatch: route runs as a checkpointed step, and each item flows through the case pipeline matching the returned key — on replay the recorded key routes the item down the same arm. A key with no matching case fails the pipeline. Applied per item on multi-item streams; every arm's outputs are emitted downstream in stream order.
func Tap ¶
func Tap[T any](name string, fn func(ctx context.Context, in T) error, opts ...StepOption) Stage[T, T]
Tap is a durable side effect: fn runs as a checkpointed DBOS step and the item passes through unchanged.
func ToStream ¶ added in v0.2.0
ToStream durably appends each item to the workflow's stream and passes it through unchanged; the stream is closed when the pipeline completes. Readers drain it with a FromStream stage or Stream.Read — the way to expose a pipeline's per-item output while it is still running, instead of waiting for the final result. The stream's type is the pipeline's item type, checked at compile time.
func UnsafeOperator ¶
func UnsafeOperator[T, R any](name string, op func(ro.Observable[T]) ro.Observable[R]) Stage[T, R]
UnsafeOperator admits an arbitrary ro operator into a durable pipeline. duro cannot guarantee replay determinism for it: the operator must be synchronous, order-preserving, and deterministic, or recovery will fail — loudly if step names misalign or execution changes goroutines, silently if identically-named steps swap positions. Prefer the safe constructors.
type State ¶ added in v0.4.0
type State string
State is a run's lifecycle state.
const ( StatePending State = "pending" // running or ready to run StateEnqueued State = "enqueued" // waiting on a queue StateDelayed State = "delayed" // waiting for its start delay StateSuccess State = "success" StateError State = "error" // completed with an error StateCancelled State = "cancelled" StateRetriesExceeded State = "retries_exceeded" // exceeded max recovery attempts )
type StepOption ¶
type StepOption func(*stepConfig)
StepOption configures how a durable stage executes as a DBOS step.
func WithBackoffFactor ¶ added in v0.2.0
func WithBackoffFactor(factor float64) StepOption
WithBackoffFactor sets the exponential multiplier applied to the retry delay after each attempt (default 2.0).
func WithBaseInterval ¶
func WithBaseInterval(d time.Duration) StepOption
WithBaseInterval sets the initial delay between retries (default 100ms).
func WithMaxInterval ¶ added in v0.2.0
func WithMaxInterval(d time.Duration) StepOption
WithMaxInterval caps the delay between retries (default 5s).
func WithMaxRetries ¶
func WithMaxRetries(n int) StepOption
WithMaxRetries sets the maximum number of automatic retries for the stage when its function returns an error. Zero (the default) means no retries. This is the step-level retry limit (DBOS's WithStepMaxRetries), distinct from workflow recovery attempts, which are configured at registration.
func WithRetryPredicate ¶ added in v0.2.0
func WithRetryPredicate(pred func(error) bool) StepOption
WithRetryPredicate restricts which errors are retried: when the stage function returns an error for which pred is false, the stage stops immediately with that error even if retries remain. Use it to spend retries on transient failures only.
func WithTimeout ¶ added in v0.2.0
func WithTimeout(d time.Duration) StepOption
WithTimeout bounds each execution attempt of the stage function: the step context is cancelled after d and the attempt fails with the context's error. Retries get a fresh deadline. DBOS has no native step timeout, so the deadline is enforced in-process per attempt — the stage function must honor context cancellation for the timeout to take effect. For a durable deadline on a whole pipeline, start its workflow from a context derived with dbos.WithTimeout; for child workflows, see WithChildTimeout.
type Stream ¶ added in v0.2.0
type Stream[V any] struct { // contains filtered or unexported fields }
Stream is a typed durable stream channel: values of type V appended by one workflow and drained by readers. Write with a ToStream stage; read with a FromStream stage or Stream.Read.
var Receipts = duro.NewStream[Receipt]("receipts")
func NewStream ¶ added in v0.2.0
func NewStream[V any](key string, opts ...ChannelOption) Stream[V]
NewStream declares a typed stream key.
func (Stream[V]) Read ¶ added in v0.2.0
Read drains the stream written by the given workflow, blocking until the writer closes it or becomes inactive; closed reports whether the stream was cleanly closed. Read is for clients — inside a pipeline use the FromStream stage, which checkpoints the read so replay never observes a changed stream.
type Topic ¶ added in v0.2.0
type Topic[M any] struct { // contains filtered or unexported fields }
Topic is a typed mailbox channel: messages of type M sent to a workflow's mailbox under one topic name. Write with a Send stage or Topic.Send; read with a Recv stage.
var Approvals = duro.NewTopic[Approval]("approvals")
func NewTopic ¶ added in v0.2.0
func NewTopic[M any](name string, opts ...ChannelOption) Topic[M]
NewTopic declares a typed mailbox topic.
type WorkflowFunc ¶ added in v0.2.0
WorkflowFunc is a hand-written durable workflow function, the kind RegisterWorkflow registers and Workflow adapts into a FanOut child. Registered pipelines (Register) never touch this type.
type WorkflowRef ¶ added in v0.2.0
type WorkflowRef[T, R any] interface { // contains filtered or unexported methods }
WorkflowRef identifies a workflow FanOut can start: a registered pipeline (*PipelineWorkflow, which carries its own dispatch metadata) or a hand-written DBOS workflow wrapped with Workflow.
func Workflow ¶ added in v0.2.0
func Workflow[T, R any](fn WorkflowFunc[T, R]) WorkflowRef[T, R]
Workflow adapts a hand-written, dbos-registered workflow function into a WorkflowRef:
duro.FanOut("process", Jobs, duro.Workflow(ProcessJob))
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
housekeeping
command
Package main demonstrates duro's operational toolkit: cron pipelines (RegisterScheduled), burst-collapsing (RegisterDebounced), surgical replay of a finished run from a named stage (ForkFromStage), and the durable identity contract behind pipeline names (the stranded-run warning).
|
Package main demonstrates duro's operational toolkit: cron pipelines (RegisterScheduled), burst-collapsing (RegisterDebounced), surgical replay of a finished run from a named stage (ForkFromStage), and the durable identity contract behind pipeline names (the stranded-run warning). |
|
orders
command
|
|
|
payments
command
Package main demonstrates duro's signal and resilience toolkit on a payment flow: retries with a predicate, per-attempt timeouts, durable pauses, human-in-the-loop approval over a typed Topic, progress Events, and a receipt Stream drained by a second pipeline.
|
Package main demonstrates duro's signal and resilience toolkit on a payment flow: retries with a predicate, per-attempt timeouts, durable pauses, human-in-the-loop approval over a typed Topic, progress Events, and a receipt Stream drained by a second pipeline. |
|
thumbnails
command
Package main demonstrates duro's parallelism toolkit on a thumbnail rendering fleet: declared queues with concurrency/rate/priority/partition controls, FanOut child options (idempotent IDs, deduplication, timeouts, delays, auth), a hand-written child workflow using duro.Context, in-process bounded Parallel, and a registered pipeline used directly as a FanOut child.
|
Package main demonstrates duro's parallelism toolkit on a thumbnail rendering fleet: declared queues with concurrency/rate/priority/partition controls, FanOut child options (idempotent IDs, deduplication, timeouts, delays, auth), a hand-written child workflow using duro.Context, in-process bounded Parallel, and a registered pipeline used directly as a FanOut child. |
|
triage
command
Package main demonstrates duro's control-flow combinators on a support ticket triage pipeline: Switch dispatches by category, a nested Branch escalates urgent bugs, Loop durably polls an external system, Sub reuses a shared notification segment, and Collect folds the batch into a report — all inside one registered pipeline, no hand-written workflow function.
|
Package main demonstrates duro's control-flow combinators on a support ticket triage pipeline: Switch dispatches by category, a nested Branch escalates urgent bugs, Loop durably polls an external system, Sub reuses a shared notification segment, and Collect folds the batch into a report — all inside one registered pipeline, no hand-written workflow function. |