Applications can read committed Attributes directly with Client.GetAttributes.
Typed RPCs read staged state and perform writes. The Client does not expose
direct Attribute writes, Channel publication, or pending-message mutation. This
keeps each external state transition behind a Flow-owned method.
An RPC can stage channel.Delete(ctx, messageID). Register that RPC with
dex.RPCOptions{IsTransactional: true} when a missing ID must abort every
other RPC write. Attribute locking already selects transactional execution, while
Channel deletion requires this explicit option.
RPCs receive ordinary Attributes and all Channel size metadata by default. AttributeMap entries and pending Channel messages are opt-in:
func (flow *OrderFlow) GetRPCs() []dex.RPCDef {
return []dex.RPCDef{
dex.DefineRPC(flow.GetOrder, &dex.RPCOptions{
LoadAttributeMapInstances: []dex.AttributeMapLoad{Items.Load("tenant-a")},
LoadChannels: []dex.ChannelDef{Queued},
LoadChannelMaps: []dex.ChannelDef{ByTenant},
}),
}
}Put an AttributeMap or ChannelMap directly in its plural load option when every current instance is needed. Use the singular instance options for the less common exact-instance case. A selected empty queue returns an empty slice; reading an unselected map entry returns AttributeMapNotLoadedError. An unselected pending-message snapshot returns ChannelMessagesNotLoadedError. Pending messages preserve FIFO order and include the server-assigned message ID. The snapshot does not change after the handler stages a publish or deletion.
Loading controls which data reaches the Worker. IsTransactional controls atomic commit and Channel deletion validation. Attribute locks add isolation only among cooperating Steps and RPCs using the same lock. Write-only publish and delete operations do not require loading.
An RPC's protocol name is its Go method name. Set RPCOptions.Name when a Flow
type that another language defines names the RPC, so a Go Worker serves it and
a Go Client invokes it. Go code still passes the bound method:
dex.DefineRPC(flow.SendUserMessage, &dex.RPCOptions{Name: "send_user_message"})
var queued bool
err := client.InvokeRPC(ctx, flowID, flow.SendUserMessage, message, &queued)RPCOptions remains the registration-time contract for timeout, transactionality,
locks, and fixed state loads. Use RPCInvokeOptions when the caller chooses an
exact map instance at runtime:
partition := "partition-007"
err := client.InvokeRPCWithOptions(
ctx,
flowID,
flow.UpsertCustomerProfile,
profile,
&result,
dex.RPCInvokeOptions{
LockAttributeMapInstances: []dex.AttributeLock{
dex.LockAttributeMap(CustomerProfiles, partition),
},
LoadAttributeMapInstances: []dex.AttributeMapLoad{
CustomerProfiles.Load(partition),
},
},
)Invocation selections are additive. The Client unions, sorts, and deduplicates them with the registered selections. Locking and loading are independent; a read-modify-write RPC must request both for the same AttributeMap instance.
Runnable applications use this API in the sequential chunking and hash partitioning patterns. The product guides explain the ordered append layout and stable-key lookup layout.
An RPC can declare a state-dependent Action in GetRPCs. Dex maintains the
KeywordList Search Attribute DexWorkQueuePermissions:
func (flow RefundFlow) GetRPCs() []dex.RPCDef {
return []dex.RPCDef{
dex.DefineRPC(flow.ApproveRefund, &dex.RPCOptions{
Action: dex.DefineAction(
"Approve",
dex.WhenAttributeMatches(
caseStatus,
dex.AttributeMatchEqual("awaiting-manager"),
dex.AttributeMatchEqual("awaiting-agent"),
),
dex.ActionRequiresPermission("refund.approve"),
),
}),
}
}An Action has a non-empty display label, one Attribute condition, and exactly
one required permission. A condition accepts one or more
AttributeMatchEqual values and matches when any value equals the current
Attribute value. The source must be a registered ordinary scalar Attribute.
String, boolean, integer, finite floating-point, and named forms of those types
are supported. A missing or deleted Attribute does not match. Permissions use
lowercase alphanumeric segments separated by dots or hyphens, such as
refund.approve.
The initial projection is the sorted union of every matching Action permission. Duplicate permissions are removed. StartFlow and SubFlow evaluate the initial Attributes directly. When a successful WaitFor, Execute, timeout handler, or RPC writes an Action source, the Worker sends the complete Action mapping with the business writes. The Server overlays those writes on its authoritative Attribute state and atomically adds newly matching permissions to the stored set. Unrelated writes and read-only RPCs do not send the mapping. Projection-only Attribute locks are not required.
The Server skips an upsert when no permission is added. Once a permission has matched, later state changes and empty mappings do not remove it. The set is kept for the life of the Flow and remains searchable after completion. An upgrade preserves permissions that are still stored but does not reconstruct permissions removed before the upgrade. A new, independent Flow execution starts from its own initial Attributes. A changed Action definition is first evaluated when a later source write sends its mapping or a new Flow starts.
DexWorkQueuePermissions is reserved for Server-managed runtime projection
and is available as dex.WorkQueuePermissionsIndexKey for Work Queue searches. An
application cannot register that name as an Attribute or custom IndexKey. The
projection identifies Runs where an Action is or has been available. An Action
gateway must authorize the selected RPC against its statically registered
permission and current state. Search Attribute contents are never proof that a
caller is authorized or that the Action remains available.
The Go SDK requires a Dex Server with protocol version 4 or later
(server-zd newer than v0.1.16). Upgrade the Server before upgrading the SDK. Worker startup
fails immediately against older Servers. Client construction stays lazy and performs
no network I/O. Every Client method negotiates before its first request, and
concurrent first calls share one GetServerInfo request. Each call's context,
and the RequestTimeout of a durable wait, bounds both its wait for negotiation
and its own request. Successful negotiation
and a definitive protocol mismatch are cached. A failed GetServerInfo request
returns a *ServiceError with Op GetServerInfo, like other request failures,
and the next call retries. A Server whose minimum protocol is newer than the SDK
reports that the Go SDK must be upgraded.
StepOptions provides the same five selections independently for WaitFor and
Execute: all AttributeMap instances, exact AttributeMap instances, Channels,
all ChannelMap instances, and exact ChannelMap instances. The two methods receive
independent snapshots. Execute reads after the winning Wait consumes messages;
retries of one logical method call reuse its first snapshot.
FlowTimeoutHandlerOptions provides Execute-style timeouts, heartbeat detection,
retry, durability, Attribute locks, and the same state selections. Set it on
StartFlowOptions or SubFlowOptions only for a positive timeout using the
handler policy. Exhausted retries may proceed to a registered Step[None]; read
the final failure with Context.RecoveryError.
Recognize Dex time limits with RecoveryErrorInfo.IsMethodTimeout,
IsHeartbeatTimeout, and IsRetryTotalDurationExceeded. Do not compare
ErrorType with a fixed string, because each Dex backend spells these error
types differently. The methods return false for a nil RecoveryErrorInfo. When
the call to the Worker fails before Dex records a method timeout, the failure is
a Worker API failure instead. IsRetryTotalDurationExceeded recognizes historical
records from before the upgrade. Current server-zd never ends a running attempt at
the retry total duration. RetryPolicy.TotalDuration starts when the Step is first
handed to a Worker and decides whether to schedule another retry after an attempt
fails. It does not end a running attempt.
RecoveryErrorInfo.AttemptProgress reports progress across all attempts of the
failed Step method. StepAttemptProgressNotStarted guarantees the handler never
ran. StepAttemptProgressNotCompleted means an attempt may have run but none
completed. Treat StepAttemptProgressUnspecified from an older Server like
NotCompleted when deciding whether external side effects may have happened.
type timeoutRecoveryStep struct {
dex.StepDefaultsNoWaitFor[dex.None]
}
func (timeoutRecoveryStep) Execute(
ctx dex.Context,
_ dex.None,
) (*dex.StepDecision, error) {
failure := ctx.RecoveryError()
if failure.IsMethodTimeout() || failure.IsHeartbeatTimeout() {
return dex.ForceFail("timeout handler did not finish in time"), nil
}
if failure == nil {
return dex.ForceFail("timeout recovery has no failure"), nil
}
return dex.ForceFail(failure.Detail), nil
}The Go SDK is being rewritten around the current Dex Flow, Step,
Attribute, Channel, Stream, WaitFor, and Execute contracts.
The rewrite provides the public model, value/protobuf mapping, immutable registration, application-hosted pull Workers, and typed FlowService Client.
Sustainable Use License 1.0, with legacy portions under their original terms as described in LEGACY_NOTICES.md.
Application packages import dex, never gen/dexpb.
var (
OrderStatus = dex.DefineAttribute[string](
"order-status",
dex.Indexed(dex.AttributeIndex{Type: dex.IndexKeyword}),
)
Commands = dex.DefineChannel[Command]("commands")
Progress = dex.DefineStream[string]("progress", 10<<20)
)
type WaitForCommandStep struct {
dex.StepDefaults
}
func (WaitForCommandStep) WaitFor(
ctx dex.Context,
input OrderInput,
) (*dex.Wait, error) {
var checkpoint string
found, err := ctx.GetLastHeartbeatValue(&checkpoint)
if err != nil {
return nil, err
}
if !found {
checkpoint = "starting"
}
if err := ctx.RecordHeartbeat(checkpoint); err != nil {
return nil, err
}
if err := Progress.Write(ctx, "waiting for a command"); err != nil {
return nil, err
}
if err := Progress.Write(ctx, "inventory check started"); err != nil {
return nil, err
}
if err := OrderStatus.Set(ctx, "waiting"); err != nil {
return nil, err
}
return dex.AnyOf(
Commands.ForOne(dex.WithConditionID("command")),
dex.Timer(
30*time.Minute,
dex.WithConditionID("timeout"),
),
), nil
}
func (WaitForCommandStep) Execute(
ctx dex.Context,
input OrderInput,
) (*dex.StepDecision, error) {
if ctx.HasTimerFired() {
return dex.ForceFail("command timed out"), nil
}
commands, err := Commands.GetConditionResults(ctx)
if err != nil {
return nil, err
}
return dex.GracefulComplete(commands), nil
}
var WaitForCommand = WaitForCommandStep{}
type OrderFlow struct {
dex.FlowDefaults
}
func (OrderFlow) GetSteps() []dex.StepDef {
return []dex.StepDef{
dex.DefineStartStep(WaitForCommand),
}
}
func (OrderFlow) GetPersistenceSchema() dex.PersistenceSchema {
return dex.PersistenceSchema{
Attributes: []dex.AttributeDef{OrderStatus},
Channels: []dex.ChannelDef{Commands},
Streams: []dex.StreamDef{Progress},
}
}Flows use dex.DefineStartStep for at most one starting step and
dex.DefineStep for every non-starting step.
Execute-only steps embed dex.StepDefaultsNoWaitFor[IN]. Step transitions use
dex.GoTo, or dex.MovementOf with dex.GoToMulti.
StepOptions.WaitForMethodTimeout and ExecuteMethodTimeout bound the two
handler calls. Timer and channel conditions determine how long a Step waits.
Use dex.Until(condition) for one condition, and dex.AllOf or dex.AnyOf
for multiple conditions.
WaitForRetry and ExecuteRetry limit one logical handler execution across all
attempts, including the ASYNC fast path and its fallback. Attempt numbers start
at one. Total duration starts at the first hand-out to a Worker and decides
whether to schedule another retry after failure; it does not interrupt a
running attempt. The default is four hours.
ASYNC lowers latency and speeds up individual Flows; sustained throughput is
about the same as SYNC. Step durability resolves from the method override,
then FlowConfig, then StepDurabilitySync. Method timeouts default to two hours
with a one-minute heartbeat timeout. The Server's minimum explicit heartbeat
timeout defaults to ten seconds and can be changed by operators; the Go SDK
validates only non-negative whole seconds in the signed int32 range.
Context.RecordHeartbeat emits a checkpoint from WaitFor or Execute. A retry
restores the most recent regular-activity checkpoint through
Context.GetLastHeartbeatValue. Passing nil, including a typed nil, explicitly
clears the checkpoint. Local-activity heartbeats are ignored. Flow timeout
handlers and RPCs cannot send heartbeat or Stream progress.
Stream.Write emits a fire-and-forget frame on the same Worker response stream.
A handler may write any number of messages to the same or different Streams.
The call reports local validation, encoding, and gRPC send failures, but it does
not wait for a Stream Store acknowledgment. Server-side store rejection or
unavailability is visible through server logs and metrics rather than the
handler return value. Heartbeats and Stream writes may run concurrently; their
frames are serialized by the Worker.
Use NewBufferedTextStream for token-sized text deltas. It preserves text
exactly, flushes after one second or a soft 16 KiB UTF-8 threshold, and emits
the tail before the handler result or error:
progress, err := dex.NewBufferedTextStream(ctx, Thinking)
if err != nil {
return nil, err
}
if err := progress.Write(delta); err != nil {
return nil, err
}BufferedTextStreamFlushInterval and BufferedTextStreamMaxBytes override the
defaults. Empty buffers emit no message or heartbeat. Retry does not restore an
unsent buffer or deduplicate batches already sent.
Messages written by a Step have #<stepExecutionID> in
StreamMessage.Source. Client writes accept any non-empty source, including
values containing #. Reusing a source appends another message; source is
metadata, not an idempotency key.
Client.ReadStream reads forward one message at a time and can long-poll.
Client.ListStreamMessages returns a non-blocking newest-first retained page.
Pass the registered typed Stream directly, a positive page size, and an empty
token for the first page. Pass StreamMessagesPage.NextPageToken unchanged to
read older messages until it is empty. The server caps page size at 1000 by
default. A trimmed anchor returns an empty page, and newer concurrent writes do
not enter an existing older-page chain.
NewStandaloneContext creates a Context for running Step code outside a
Worker, for example in a tool server or a test. It represents one handler
attempt. It inherits cancellation, deadlines, and values from its parent
context, and its metadata methods return the values in
StandaloneContextOptions. Attempt is required and must be at least 1.
var lastHeartbeatValue any
if record.CheckpointJSON != nil {
var checkpoint SendCheckpoint
if err := json.Unmarshal(record.CheckpointJSON, &checkpoint); err != nil {
return nil, err
}
lastHeartbeatValue = checkpoint
}
ctx, err := dex.NewStandaloneContext(attemptContext, dex.StandaloneContextOptions{
FlowID: "invoice-tool-server",
StepExecutionID: record.RequestKey,
FirstAttemptAt: record.FirstAttemptAt,
Attempt: record.Attempt,
LastHeartbeatValue: lastHeartbeatValue,
OnRecordHeartbeat: func(value any) error {
var checkpointJSON []byte
if value != nil {
encoded, err := json.Marshal(value)
if err != nil {
return err
}
checkpointJSON = encoded
}
// SaveCheckpoint writes only if record.Attempt is still the stored attempt.
return store.SaveCheckpoint(record.RequestKey, record.Attempt, checkpointJSON)
},
})
if err != nil {
return nil, err
}
decision, err := SendInvoiceStep{}.Execute(ctx, invoiceID)The NewStandaloneContext API reference has the complete example.
Dex stores nothing for a standalone Context. GetLastHeartbeatValue and
GetStepExecutionLocal read only the seeded LastHeartbeatValue and
StepExecutionLocals. RecordHeartbeat, SetStepExecutionLocal, and
RecordEvent validate and encode their value, then call the matching On*
hook before returning, so the caller can persist the value and seed the next
attempt.
- Decode a value persisted as JSON into its original Go type, such as the
checkpoint struct,
int32, or[]byte, before seeding it. Do not decode it intoany: JSON numbers becomefloat64and byte slices become base64 strings, so decoding into the original type later fails. Do not seed the JSON itself asjson.RawMessageor[]byte. - After the parent context is done, the hooks never run. The three write
methods then return an error that wraps the parent's error when the
arguments are valid, the value encodes, and the event name is not already
recorded; otherwise the validation error comes first. A write already in progress
can still race with the end of its attempt, so a persisting hook must write
conditionally on
Attemptand never let an older attempt replace a newer attempt's value. HasTimerFired,HasTimerFiredByIndex, andWaitForMethodFailedreturn false.- Attribute, Channel, Stream, and
BufferedTextStreamoperations that return an error return an invalid invocation context error. SubFlowResultandSubFlowIDreturn errors stating that they require a Dex invocation Context.- These methods have no error result and panic on a standalone Context:
Channel.Size,ChannelMap.Size,ChannelMap.MapSize,ChannelMap.AllInstanceKeys,AttributeMap.MapSize, andAttributeMap.AllInstanceKeys. Do not call them from Step code that runs outside a Worker.
A successful Step can cancel queued or active executions while continuing with its normal decision:
return dex.GoTo(RecordQuote, quote).
CancelSiblingSteps(CarrierA, CarrierB).
CancelSteps(GlobalQuoteTimeout), nilCancelSteps selects every current execution of each registered Step type.
CancelSiblingSteps selects only executions whose
Context.FromStepExecutionID() matches the current execution. Repeated calls
form a union, and a Flow-wide selector wins for the same Step type. The Worker
rejects nil, mismatched, or unregistered selectors as invalid Step results.
Dex resolves one snapshot after the current execution succeeds. Completed, already-canceled, and absent targets are no-ops. Next Steps created by the same decision are not in that snapshot. Dex immediately applies the next or close action without waiting for target handlers; late decisions, writes, retries, and recovery Steps are discarded.
An RPC may call RPCResult.CancelSteps for Flow-wide selection while also
returning output and scheduling Next Steps. RPCs do not support sibling
selection because an RPC invocation has no Step execution lineage.
A Join waits for parallel branches of one Flow. It is a ChannelMap of branch
results with the same name, registered in PersistenceSchema.Channels.
Fork uses the split Step's execution ID as the round key, counts the branch
builders, and builds every branch movement and the join movement with the same
JoinRound:
var QuoteReceived = dex.DefineJoin[string]("quoteReceived")
func (RequestQuotes) Execute(ctx dex.Context, request QuoteRequest) (*dex.StepDecision, error) {
movements, err := QuoteReceived.Fork(ctx,
[]func(dex.JoinRound) dex.StepMovement{
func(round dex.JoinRound) dex.StepMovement { return dex.MovementOf(QuoteFromSupplierA{}, round) },
func(round dex.JoinRound) dex.StepMovement { return dex.MovementOf(QuoteFromSupplierB{}, round) },
},
func(round dex.JoinRound) dex.StepMovement {
return dex.MovementOf(AcceptQuotes{}, QuoteJoinInput{Request: request, Round: round})
},
)
if err != nil {
return nil, err
}
return dex.GoToMany(movements...), nil
}
func (QuoteFromSupplierA) Execute(ctx dex.Context, round dex.JoinRound) (*dex.StepDecision, error) {
return QuoteReceived.EndBranch(ctx, round, "A")
}
func (AcceptQuotes) WaitFor(_ dex.Context, input QuoteJoinInput) (*dex.Wait, error) {
return dex.Until(QuoteReceived.ForAllBranches(input.Round)), nil
}Each branch reports exactly one result per round with EndBranch or
ReportBranchResult. The join Step waits with ForAllBranches,
ForFirstBranch, or ForBranches(round, n) and reads BranchResults. A Flow
that forks the same Join in a loop resets each round in the next split Step's
WaitFor with ForRemainingBranches(previousRound, n) and reads the late results
with RemainingBranchResults. RPCs have no Step execution ID and use
ForkWithRoundKey. One invocation forks one Join at most once. Invalid counts
fail when Dex serializes the Wait. Join.ChannelMap returns the registered
ChannelMap, so an RPC can read or load a round's pending results with the
round's RoundKey as the instance key. Never publish to it directly; report
only with EndBranch or ReportBranchResult. The Join doc comment lists the traps,
including canceled branches and canceling joins.
StartFlowOptions.Timeout starts a durable Dex timer rather than a backend
execution timeout. A Flow implementing FlowTimeoutHandler defaults to
TimeoutHandler; other Flows default to TimeoutFail. Override either default
with TimeoutFail or TimeoutCancel:
func (OrderFlow) HandleTimeout(ctx dex.Context) (*dex.StepDecision, error) {
return dex.ForceComplete("expired"), nil
}
timeout := 30 * time.Minute
options := dex.StartFlowOptions{
Timeout: &timeout,
TimeoutPolicy: dex.TimeoutHandler,
}The hook is Execute-only, receives no input, and runs at most once after its
durable timer fires or is skipped. It may use Context normally and return any
StepDecision. The deadline is preserved across Flow state transfers. Zero or nil timeout disables the feature.
Use dex.None when a Step, RPC, or Channel has no application payload, and pass
nil at every call site. It rejects accidental values unlike any and makes
the absence of a payload explicit.
Flow RPCs are methods matching dex.RPC[IN, OUT]. Attributes and channels stay
strongly typed inside handlers. Step-execution locals and recorded events accept
arbitrary values through dex.Context.
Invocation attribute reads return (value, error). A missing static or map
attribute returns *dex.AttributeNotFoundError, which callers can inspect with
errors.As when absence is expected.
Until, AllOf, and AnyOf use unnamed Conditions by default. Do not add
Condition IDs merely because a Condition is nested in one of these waits.
Every Condition in AnyComboOf must use WithConditionID. Reusing the same
Condition value across combinations is supported, while duplicate IDs on
distinct Conditions are rejected.
Clients wait on typed scalar comparisons in the current run with
WaitForAttributeMatch or WaitForAttributeMapInstanceMatch. Build matches
with AttributeMatchEqual, AttributeMatchNotEqual,
AttributeMatchGreaterThan, AttributeMatchGreaterThanOrEqual,
AttributeMatchLessThan, or AttributeMatchLessThanOrEqual. The matched value
is decoded into the output pointer. String and bool support equality operators;
integers and doubles support every operator. Every AttributeMap and ChannelMap
instance must be non-empty and must not contain /. Objects,
bytes, null, non-finite doubles, and invalid ordering fail before the RPC.
Request IDs are optional for both waits. The Server does not read them for these
waits. RequestTimeout bounds the total SDK call across transport reattachments;
zero waits indefinitely. Request expiry returns *dex.RequestTimeoutError.
A caller disconnect releases its wait slot.
Inside a handler, AttributeMap.MapSize and AllInstanceKeys include buffered
sets and deletes. ChannelMap.MapSize and AllInstanceKeys are RPC-only and
include buffered publishes, but omit empty instances. Keys are decoded and
sorted. Use ForceCompleteIfChannelsEmpty for atomic conditional completion.
SubFlows are normal, independently addressable Flows used as durable Conditions:
func (ParentStep) WaitFor(_ dex.Context, input ChargeInput) (*dex.Wait, error) {
return dex.Until(dex.SubFlow(ChargeFlow{}, input)), nil
}
func (ParentStep) Execute(ctx dex.Context, _ ChargeInput) (*dex.StepDecision, error) {
result, err := dex.SubFlowResult(ctx)
if err != nil {
return nil, err
}
var receipt Receipt
if err := result.DecodeSingleOutput(&receipt); err != nil {
return nil, err
}
return dex.GracefulComplete(receipt), nil
}SubFlowID(ctx, index...) returns the generated identity, including for a running
AnyOf loser. SubFlowOptions configures timing, timeout policy, retry, initial
target Attributes, Flow config, Condition ID, and reuse. Parent completion does not cancel an
unfinished SubFlow.
Registration uses Go type names without package or pointer by default:
*orders.OrderFlow becomes OrderFlow. Name each Flow type after its domain,
such as ApprovalFlow, and never just Flow: the package does not
disambiguate Flow types in one Registry, and Dex Web shows the name as is.
Embed FlowDefaults in a Flow. Waiting steps embed StepDefaults;
execute-only steps embed StepDefaultsNoWaitFor[IN]. Both include the default
step type and options. Override GetFlowType or GetStepType only when an
explicit durable identity is required. A generic Flow or Step type has no
default name and must override it. dexcli visualize and Dex Web v2 use these
same registered names.
Registration is assembled once from each Flow's final durable type, steps, persistence schema, and exported RPC methods. It rejects empty or duplicate names, multiple starting steps, invalid indexes, undeclared locks, and incompatible execute-failure targets before the Worker starts.
Worker.Start aggregates every registered Indexed Attribute and synchronizes
the physical index names with Dex Server before opening task streams. Existing
indexes return immediately; a synchronization failure or the default two-minute
deadline fails startup. Indexed AttributeMaps must declare a fixed index key.
Opt an Attribute or AttributeMap into the Flow's external latest-state projection, then select Server-configured Attribute Stores on the Flow:
var CustomerEmail = dex.DefineAttribute[string](
"customer-email",
dex.SyncToAttributeStore(),
)
config := &dex.FlowConfig{
AttributeStoreNames: []string{"profiles", "audit"},
}Store updates are asynchronous. Every enabled Attribute write is sent to every
selected Store. Deleting an opted-in Attribute writes SQL NULL, and a Store
failure does not roll back the Flow Attribute. A nil AttributeStoreNames
preserves current targets; an empty non-nil slice disables future synchronization
while preserving protocol presence.
Use HardDeleteOnNull for short-lived secrets that must not stay in Flow
history:
var ModelAPIKey = dex.DefineAttribute[string](
"model-api-key",
dex.HardDeleteOnNull(),
)Dex stores the value only in its blob store, and Delete permanently removes
it. Handlers receive the value inline. Only string and object values are
supported, and the option cannot be combined with Indexed or
SyncToAttributeStore. It requires a Server with an enabled blob store and
protocol version 3. Each Flow keeps one stored value per key and overwrites it
in place, so a retried Step or RPC attempt can observe a write or deletion made
by a failed attempt. See the HardDeleteOnNull documentation for every
limitation.
DefineStep and DefineStartStep retain the step input type behind a private
typedStepDef that implements the sealed StepDef interface. Runtime
movements resolve through the current Flow's registered step definitions, so a
same-name Step value cannot replace the registered handler or defaults.
RPCs require no communication schema. A Flow lists its RPCs and immutable
execution options explicitly through GetRPCs:
func (flow *OrderFlow) GetRPCs() []dex.RPCDef {
return []dex.RPCDef{
dex.DefineRPC(flow.GetOrder, nil),
dex.DefineRPC(flow.UpdateOrder, &dex.RPCOptions{
Timeout: 30 * time.Second,
LockAttributes: []dex.AttributeLock{dex.LockAttribute(OrderStatus)},
}),
}
}Each registered method uses this exact shape and keeps its Go method name as the durable RPC name:
func (
ctx dex.Context,
input IN,
) (*dex.RPCResult[OUT], error)Only methods returned from GetRPCs are RPCs. Exported helpers and unregistered
RPC-shaped methods are ignored. Each definition must contain a direct bound
method on that Flow value; package functions, methods from another Flow, method
expressions, closures, wrappers, duplicates, and invalid options fail Registry
construction. Client calls pass the same direct bound method, such as
Orders.Update, but do not pass RPC options. Return nil, err on failure;
returning nil, nil is an invalid Worker result.
Registered Flow and Step values are retained and may be invoked concurrently.
They must be immutable or concurrency-safe and must keep
invocation state in dex.Context.
Applications share one Registry and BlobCache between Worker and Client:
The cache comes from
github.com/superdurable/dex/blob-cache-go/blobcache.
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
registry, err := dex.NewRegistry([]dex.Flow{Orders})
if err != nil {
return err
}
cache, err := blobcache.New(&blobcache.Config{
Dir: "/var/tmp/order-dex-blobs",
MaxBytes: 1 << 30,
Logger: logger,
})
if err != nil {
return err
}
worker, err := dex.NewWorker(registry, cache, dex.WorkerOptions{
Logger: logger,
})
if err != nil {
return err
}
client, err := dex.NewClient(registry, cache, dex.ClientOptions{
Logger: logger,
})
if err != nil {
return err
}
defer client.Close()
if err := worker.Start(); err != nil {
return err
}Workers receive tasks over outbound pull streams to FlowServiceAddress.
They need only an outbound connection to the Dex Server. Every Client start and
SubFlow uses pull routing automatically; no Worker target is configured.
Credential authenticates the startup calls, task streams, and blob hydration.
Set WorkerOptions.Pull to tune capacity, identity, registration, or shutdown.
Nil uses the same pull defaults.
worker, err := dex.NewWorker(registry, cache, dex.WorkerOptions{
FlowServiceAddress: "dex.example:8801",
Pull: &dex.PullWorkerOptions{
MaxConcurrentTasks: 8,
StopDrainTimeout: 30 * time.Second,
},
})
if err != nil {
return err
}
client, err := dex.NewClient(registry, cache, dex.ClientOptions{
FlowServiceAddress: "dex.example:8801",
})
if err != nil {
return err
}
defer client.Close()
defer worker.Stop(context.Background())
if err := worker.Start(); err != nil {
return err
}Since 1.45.0, a Worker can store the Flow Definition Graph (FDG 2.0) JSON of its
Flows on the Dex server, so Dex Web does not need a local
--flow-rendering-dir. WorkerOptions.FlowDefinitionPaths names files or
directories; directories are read recursively for *.json files, and JSON
files that are not FDG 2.x documents, such as package.json, are skipped.
WorkerOptions.FlowDefinitions passes documents built in memory. Generate the
files with dexcli visualize --schema-version 2.0 --json.
worker, err := dex.NewWorker(registry, cache, dex.WorkerOptions{
FlowDefinitionPaths: []string{"flow-definitions"},
})NewWorker returns an error for a missing path, an invalid named file, an
invalid generatedAt, or a Flow type given twice.
Start uploads only the documents of Flow types in the Registry, after it
negotiates the Server protocol and before it opens task streams. An absolute
source.path is uploaded as a path relative to the working directory, or as
its base name. Upload failures, including an older server without the API,
are logged as warnings; the Worker still starts, and Stop cancels an upload
in progress. The server keeps the document
with the latest generated time, so give every process of one release the same
time. Deployment scripts can call dex.ReadFlowDefinitionFiles and
Client.UploadFlowDefinitions instead, with force to roll a definition back.
Call WaitPullRegistered(ctx) after launching Start on a goroutine to gate
startup and rolling deployments. Readiness requires at least one registered
STEP_AND_RPC stream and, when ReservedRPCTasks is positive, one RPC_ONLY
stream. Concurrent waits are safe. The context bounds the wait; startup failures
retain their error chain. Once ready, later waits return nil even after Stop.
PullRegistrationError.Queue identifies the slot kind with its full
POLL_WORKER_SLOT_KIND_STEP_AND_RPC or POLL_WORKER_SLOT_KIND_RPC_ONLY name,
not a Flow type or Worker ID. A permanent rejection is fatal to readiness only
when no other stream of that kind remains registered. PullRegistrationState
keeps its existing Queues field, with one entry per required slot kind.
Transient failures preserve readiness after initial registration. A successful
registration clears that kind's permanent error. Remove stopping Workers from
service separately because Stop preserves latched readiness. See the
runnable startup and HTTP readiness example.
StopOnPermanentStreamRejection defaults to false, which retries permanent
rejections. When enabled, a permanent rejection leaving a required slot kind
without a registered stream makes Start drain all streams within
StopDrainTimeout, then cancel them and wait for handlers before returning the
registration error. Always call Stop before exiting.
The Worker keeps exactly MaxConcurrentTasks long-lived execution slot streams.
Zero uses the CPU count. Each stream registers an empty queue, the Worker ID,
and every registered Flow type; it is ready while idle and busy while executing
one task. ReservedRPCTasks streams accept only RPCs; the others accept Steps
and RPCs. The Worker executes every received task without capacity rejections.
IdleStreamsPerQueue is deprecated, remains accepted, and has no effect.
Start fails without background retry when the Server's first registration reports
a nonzero max_concurrent_tasks_per_app_worker below MaxConcurrentTasks.
The error names both values and matching.maxConcurrentTasksCap. The Worker then
half-closes its streams, so a task sent right after registration returns to the
Server without costing an attempt. A lower limit reported on a later reconnect
only logs a warning; the Server rejects any surplus stream.
Unknown Step types and RPC names can still be rejected before the handler runs. Flow type names must be unique within a Dex Server.
The generated Worker ID uses the first nonempty value from DEX_WORKER_INSTANCE, POD_NAME,
HOSTNAME, or the system hostname, plus a random suffix. An AppWorkerID
override must identify one Worker process uniquely and change when that process
restarts. It must contain 1-63 ASCII letters, digits, dots, underscores, or
hyphens, and must not equal a registered Flow type. The Worker reconnects after
stream failures. On a server-directed reconnect, an idle stream half-closes at
once and a busy stream finishes its task and sends its final output first. It logs permanent
registration failures and retries them after at least one minute. Start
returns startup synchronization errors, not later poll stream errors.
The SDK sends an upstream Pong every 20 seconds while a task runs and stops
before its final frame. This internal interval needs no Worker option.
Start blocks and is one-shot. Stop(ctx) starts no new tasks. Idle streams
immediately half-close their request side; busy streams finish the handler, send
final output, then half-close. Tasks arriving after shutdown begins are dropped
without calling a handler, including frames arriving after the half-close.
Streams keep reading until the Server ends them. After StopDrainTimeout
(default 30 seconds) or context expiry, Stop resets active streams and cancels
handlers instead of half-closing a running task. The deadline also resets
half-closed streams that the Server has not ended, such as after a network
partition, and streams whose sends are blocked. The Server counts a task it sent
to such a stream as possibly run. A stream whose handler already produced its final
output, even one still waiting behind another frame, is not reset at the deadline: it half-closes, and Stop waits up to five
more seconds for the Server to end it. Stop waits for handlers to return,
so handlers must honor cancellation. Local stream failures, such as an oversized
or undecodable frame, or a task with no task ID, an invalid deadline, or no
request, always reset the stream and never half-close it; the handler does not run. Stop is idempotent. A broken connection
cancels its tasks, and their output is never sent on a later connection.
The SDK does not install signal handlers; the Order example
shows signal-driven shutdown.
Each invocation receives independent buffered attributes, channel messages, locals, and events. Successful handlers commit the whole response. Returned errors, mapping failures, and recovered panics commit nothing. Flow and Step values may be called concurrently and must be concurrency-safe.
Blob-backed values hydrate through private FlowService calls.
FlowServiceAddress defaults to localhost:8801. Registry and BlobCache are
required; the caller closes the cache after Client and Worker stop using it.
When LoadBlobs omits requested blobs, the error lists every missing blob ID,
its Flow ID, and each affected Worker value: Step or RPC input, Attribute key,
Channel name and message ID, Step-execution local key, heartbeat, or condition
result. It also shows the resolved FlowServiceAddress. Check that the Worker's
Flow service matches the Server dispatching the call. Worker error details are
bounded to 1 KiB; when the complete diagnostic exceeds that limit, the SDK also
logs every missing blob and value through its logger.
ClientOptions.GrpcMaxMessageBytes and WorkerOptions.GrpcMaxMessageBytes
limit the gRPC messages the Client or Worker receives. Zero uses
dex.DefaultGrpcMaxMessageBytes (32 MiB), which matches the Dex server's
default api.grpcMaxMessageBytes, and keeps gRPC's unlimited send limit, so
the server bounds what the SDK sends. A positive value limits both directions;
negative values are invalid. The Worker applies the limit to its outbound task
streams, which receive Step and RPC invocations, and to its FlowService
connection. Every hop applies the smaller limit of its two ends, so raise the
server and SDK limits together. Keep the limit above the largest application
value by an explicit margin for the message envelope; the default leaves 16 MiB
of headroom for values up to 16 MiB.
Hydration first requests every uncached blob in one LoadBlobs call. Blob
references do not carry sizes, so when a response exceeds a gRPC message limit,
the SDK splits the request in half and retries each half. N uncached blobs take
at most 2N-1 calls. A single blob that still does not fit returns
*dex.BlobMessageTooLargeError with its Flow ID and blob ID.
dex.Logger supports structured debug, info, warning, and error messages.
blobcache.Config.Logger defaults to slog.Default and is inherited by Client
and Worker. Their option-level loggers override it for that component.
Set ReservedRPCTasks when synchronous RPC latency must stay responsive behind
long-running Steps. When unset, it reserves one slot if the resolved task limit
is at least two, otherwise zero. With the CPU-count default, Steps get one fewer
slot than before. Set it explicitly to zero to opt out. With one slot and no
explicit reserve, Worker start logs one warning: RPCs may wait behind
long-running Steps; use a limit of at least two or set the reserve explicitly.
Flows must stay responsive because RPCs share Worker task capacity with Steps.
The value must be nonnegative and less than resolved MaxConcurrentTasks;
invalid values fail construction. Reserved streams register as RPC_ONLY so
Steps use the other slots without being declined. RPCs can use either kind.
For example, eight task slots with two reserved slots means six shared streams
and two RPC-only streams:
pull := dex.PullWorkerOptions{MaxConcurrentTasks: 8, ReservedRPCTasks: ptr.Any(2)}PullReservedRPCDeclines() is deprecated and always returns zero.
FlowServiceAddress uses plaintext gRPC by default for both the Client and
Worker, including pull mode. For a TLS-terminating Dex endpoint, use its
host:443, dns:///host:443, or dns://authority/host:443 address and set
FlowServiceTLS on both the Client and Worker. TLS requires version 1.2 or
newer and negotiates ALPN h2.
The certificate name defaults to the address host. Extra PEM root CAs extend
the system roots; they do not replace them. A non-nil empty option uses only
the system roots. TLS never falls back to plaintext. Existing bearer, file,
and API key credentials work over either transport. The API key token endpoint
has its own HTTPS trust configuration.
rootPEM, err := os.ReadFile("/etc/dex/root-ca.pem")
if err != nil {
return err
}
tlsOptions := &dex.FlowServiceTLSOptions{AdditionalRootCAsPEM: rootPEM}
worker, err := dex.NewWorker(registry, cache, dex.WorkerOptions{
FlowServiceAddress: "dex.example.com:443",
FlowServiceTLS: tlsOptions,
Pull: &dex.PullWorkerOptions{},
})
if err != nil {
return err
}
client, err := dex.NewClient(registry, cache, dex.ClientOptions{
FlowServiceAddress: "dex.example.com:443",
FlowServiceTLS: tlsOptions,
})
if err != nil {
return err
}
defer client.Close()
defer worker.Stop(context.Background())
if err := worker.Start(); err != nil {
return err
}Without ServerName, the HTTP/2 :authority is the address host and port, as
with plaintext, and an IPv6 address keeps its brackets. The DNS server in a
dns://authority/host:port address never becomes the certificate name or
:authority. Set ServerName only when the certificate name differs from the
address host. It is then also sent as :authority, without a port. An IPv6
ServerName produces an unbracketed :authority that strict HTTP/2 servers
reject, so leave it empty for an IPv6 address or use a DNS name. An IP address
sends no SNI and requires a matching IP SAN. An AdditionalRootCAsPEM with no
parseable certificate fails NewClient or NewWorker. The other SDKs will gain
the FlowService TLS option in a later release.
Client methods target the current run by sending an empty run ID. This always
reaches the Flow's current run. StartFlow and TimeTravel return run IDs for
diagnostics, while normal calls need only the flow ID.
ClientOptions.Credential and WorkerOptions.Credential send a bearer
credential with every FlowService call. A Dex server with authentication enabled
requires one on every public API call. dex.FileCredential(path) reads a
token file, such as a projected service account token, and rereads it when it
changes; an emptied or missing file fails calls instead of sending a stale
token. The Worker sends its credential with startup index verification and
blob hydration.
Use dex.APIKeyCredential(tokenURL, serverID, apiKey) when the application
holds a long-lived API key instead of an access token. The credential posts
{"server_id":"<server ID>","api_key":"<API key>"} to the token URL and sends
the returned bearer access token. The server ID is the identifier that the token
endpoint scopes the access token to; supply it explicitly, because the SDK never
derives it from a host name.
credential, err := dex.APIKeyCredential(
"https://auth.example.com/oauth/token",
"orders-production",
os.Getenv("DEX_API_KEY"),
dex.WithAPIKeyExchangeTimeout(5*time.Second),
)
if err != nil {
return err
}
client, err := dex.NewClient(registry, cache, dex.ClientOptions{Credential: credential})- Construction validates the arguments without contacting the token endpoint.
The token URL must use
https;httpis allowed only forlocalhost,127.0.0.0/8, and::1. - The first call performs the exchange. The token is cached until 80% of its
expires_inlifetime has elapsed. The next call then starts one background refresh and keeps using the cached token. - At most one exchange runs at a time, and concurrent callers share its result.
A failed refresh keeps serving the cached token and is retried at most once
every 5 seconds. After the token expires, a call waits for the exchange,
bounded by
WithAPIKeyExchangeTimeout(default 10 seconds), and fails with the exchange error if no token arrives. - The token's expiry counts from the start of the exchange request. If a slow response arrives after that expiry, the exchange fails and the token is never cached or sent.
- When Dex rejects a call with
UNAUTHENTICATED, the SDK discards that token if it is still cached. A unary call is retried exactly once with a fresh token. A rejected pull Worker poll stream discards its token and reconnects once immediately with a fresh token. If that reconnection is rejected too, the Worker falls back to its usual reconnection, which waits about a minute. - The credential never follows redirects and never includes the API key or an
access token in errors or formatted output. It replaces the API key and known
access tokens with
[REDACTED]in the token endpoint'serroranderror_descriptionfields and in transport errors. It also redacts every JWT-shaped substring there: three base64url segments separated by dots, each at least 10 characters long. This covers an older token that the credential no longer remembers.WithAPIKeyHTTPClientsupplies a proxy-aware or test HTTP client. - The application owns the credential and may share it between several Clients
and Workers, so closing a Client or Worker does not stop it. The credential
starts no long-lived goroutine and needs no
Close. An exchange goroutine ends within one exchange timeout when the HTTP transport honors request context cancellation, as the default transport does.
StartFlow uses the starting step retained by Registry. Its input must match that step's input type, and its step options come from the registered Step. Flows without a starting step require nil input.
The SDK generates request IDs for StartFlow and InvokeRPC. A
StartFlowOptions.RequestID may provide a stable business identifier spanning
separate calls. Step completion and Attribute waits accept an optional request
ID, but the server does not read it.
A stable request ID alone does not make a StartFlow retry succeed. A retried
StartFlow returns the existing run only when AlreadyStarted.IgnoreError is
true and the request ID matches the one that started that run. Without
IgnoreError, the retry returns FlowAlreadyStartedError even when the first
attempt created the Flow. The match applies only when the ID reuse policy
rejects the start; a policy that permits another run starts a new one. Set both
options on every attempt of one logical start:
flowID := "order-" + orderID
requestID := "start-order-" + orderID
runID, err := client.StartFlow(ctx, orderFlow, flowID, input, dex.StartFlowOptions{
AlreadyStarted: &dex.AlreadyStartedOptions{IgnoreError: true},
RequestID: &requestID,
})Client is safe for concurrent calls. Close is idempotent and closes only its
owned gRPC connection. Calls after Close return a local error.
WaitForFlow hydrates every requested completion before returning. Decode one
known output with result.DecodeSingleOutput(&output); it returns a local error
unless exactly one completion exists. For multiple outputs, match
result.Completions by StepType or StepExecutionID, then call
completion.Output.Decode(&target). The slice preserves server collection
order, but parallel branch order is not deterministic. No-output Flows return
an empty slice.
DescribeFlow(ctx, flowID) reads one known Flow's current or latest run without
waiting. Use it for a one-off status check, for example when a read-only RPC
returns a non-terminal business state and the caller needs to know whether the
Flow has failed. Its FlowInfo contains FlowID, RunID, FlowType, Status,
StartedAt, and ClosedAt. ClosedAt is zero while the run is open or when
the close time is unavailable. Use WaitForFlow when the caller needs to wait
for a terminal result, and SearchFlows to find Flow IDs by query.
GetAttributes reads selected registered Attributes in one server call, without
dispatching a Worker. A read sees committed state, including on a closed run
until it is purged. It does not see writes staged inside an active RPC. Pass
ReadAttribute or ReadAttributeMapInstance with a *T destination for each
value:
orderStatus := dex.DefineAttribute[string]("order-status")
notes := dex.DefineAttributeMap[Note]("notes")
var status string
var note Note
result, err := client.GetAttributes(ctx, flowID,
dex.ReadAttribute(orderStatus, &status),
dex.ReadAttributeMapInstance(notes, "n1", ¬e),
)
if err != nil {
return err
}
if !result.IsSet(orderStatus) {
status = "new"
}An unset Attribute or AttributeMap instance leaves its destination unchanged.
Most callers ignore the returned AttributeReadResult; use IsSet or
IsMapInstanceSet when an unset value must be told apart from a stored zero
value. Destinations are written only after every value decodes, so an error
never leaves partial writes. The servers resolve hard-delete values, and the
Client resolves large-value references through LoadBlobs before decoding. An
unregistered definition, or one whose value type differs from the registered
definition, returns *FlowDefinitionError. A missing Flow or a run being purged
returns *FlowNotFoundError, and a decode failure returns *ValueMappingError.
Remote FlowService failures use concrete Go error types. Match expected
conditions with errors.As instead of comparing gRPC codes or sub-statuses:
var inactive *dex.FlowNotActiveOrNotFoundError
if errors.As(err, &inactive) {
// The Flow is missing or already closed for this mutation.
}FlowNotFoundError is returned by operations requiring an existing Flow,
including DescribeFlow, GetAttributes, WaitForFlow, and TimeTravel. RPCs, timer operations,
and step or attribute waits return FlowNotActiveOrNotFoundError when no active
Flow is available.
RPCs also use this error when the target Flow cannot be found, including query-only reads. A query-only RPC without locks, transactional execution, durable effects, or Server-forced Update routing can read a retained terminal execution. At that confirmed read boundary, this error means no readable target was found; return the application's missing or unavailable result directly, without a lifecycle probe, a short timeout, a retry, or historical Step-output decoding. Preserve other service and Worker failures. For mutations and active-only RPCs, the error can mean either missing or closed and does not prove that the requested action succeeded. RPC success does not prove that the Flow is active.
Duplicate starts return FlowAlreadyStartedError. Server long polls return
LongPollTimeoutError, including the operation and Flow ID. Durable Step and
Attribute waits reattach transparently instead of exposing that error. Worker handler
failures return WorkerInvocationError; its Worker field preserves the
original Worker code, type, and detail. Concurrent locking RPCs return
RPCLockConflictError separately.
Update-class requests rejected during Flow close cleanup return FlowClosingError
with ErrorSubStatusFlowClosing and gRPC FailedPrecondition. Re-read the
retained terminal Flow state before deciding whether another action is needed.
WaitForFlow returns FlowResult for every terminal status. Inspect Status,
ErrorType, ErrorMessage, and any requested Completions; transport and
long-poll failures remain errors.
Every specific service error unwraps to *dex.ServiceError and then to the
original gRPC error. ServiceError exposes Op, FlowID, Code, SubStatus,
and Detail; sub-status is diagnostic metadata, not application control flow.
Unknown or malformed server details fall back to ServiceError.
Registry failures use FlowDefinitionError, invalid Worker handler results use
InvalidStepResultError, and codec failures use ValueMappingError. Ordinary
argument and context errors remain ordinary Go errors.
Strings, booleans, signed integers, representable unsigned integers, and
floating-point values use native Dex value arms. Strings must contain valid
UTF-8; use []byte for arbitrary binary data. Byte slices use an object arm
with wire encoding "raw" and store the bytes directly without base64.
Structs, maps, other slices, arrays, and JSON-compatible values use an object
arm with wire encoding "json".
Internal Blob references are opaque. Hydration sends the owning Flow ID with
each reference, and the local cache is isolated by (flowID, blobRef).
Returned dynamic values remain opaque until decoded:
var result OrderResult
if err := value.Decode(&result); err != nil {
return err
}Decode requires a non-nil pointer. Invalid UTF-8 strings, integer overflow, incompatible targets, unknown encodings, unhydrated blob references, and malformed JSON return errors.
Ordinary nil encodes with the Dex null arm. In an Attribute write, that arm deletes the Attribute. In every other Value context, it represents null.
Keyword and text indexes accept valid UTF-8 strings. Keyword-array indexes
accept slices containing valid UTF-8 strings. Int, double, and bool indexes
accept their matching Go primitive types. Datetime indexes accept time.Time
or RFC3339Nano strings, including UTC Z and numeric offsets. Fractional
seconds are preserved. Numeric strings are not treated as Unix nanoseconds.
Initial indexed values are validated by dex.InitialAttribute and
dex.InitialAttributeMapValue.
The SDK generates UUIDs for StartFlow and InvokeRPC.
StartFlowOptions.RequestID may override the generated start ID. Step
completion and Attribute waits accept a request ID, but the server does not
read it.
Large string and object values may be returned as blob references. Worker inputs and Client results hydrate before handler or application decode. Decode never performs network I/O.
Compilable examples:
make unitTests
make clientIntegTests
make workerIntegTests
make e2eTests
make docsCheck
make copyright-check
docsCheck parses the hand-written dex, logging, and shared blobcache
packages and requires a Go doc comment on every exported type, function,
method, field, interface method, constant, and variable. Comments must begin
with the declared name. Generated protobufs and tests are excluded. Use
go doc dex.<Name> (or an IDE hover) to inspect the same application-facing
API documentation locally.
e2eTests uses the current checkout's dexcli dev environment. It
runs the migrated iWF Go SDK scenarios through the public Dex SDK. It also
visualizes integ/webv2approval and integ/webv2reply into a Flow rendering
directory and checks that Dex Web v2 lists, searches, displays, edits, acts
on, and starts both Flows under the names the Worker registers.
DEX_INTEG_DEX_PORT and DEX_INTEG_WEB_PORT pin the Dex gRPC and Dex Web
ports; each defaults to a free port. Each script run starts a fresh dexcli dev
environment with server-zd and pull Workers. DEX_INTEG_ENGINE=zd remains accepted.
Set DEX_SERVER_ZD_BINARY to reuse a built dex-server-zd.
integTests runs ./integ against a Dex you start yourself. Start it with
dexcli dev -flow-rendering-dir pointing at the two visualized fixtures, and
set DEX_FLOW_SERVICE_ADDRESS and DEX_WEB_ADDRESS (default
127.0.0.1:8801 and 127.0.0.1:8802).
Run the local Client and Worker integration suites plus all dexcli dev
E2E scenarios with Go coverage:
make integrationCoverageThe report measures only production packages under ./dex/.... Unit
tests, standalone BlobCache module tests, examples, generated protobuf stubs under
gen/, and *_test.go files are excluded. Open coverage/index.html for
annotated source, or inspect coverage/coverage.txt for per-function
totals. coverage/coverage.out is the profile uploaded by CI.
CI uploads the profile to Codecov with GitHub OIDC, so no upload secret is
stored in this repository. The report uses the sdk-go-integration flag
and contributes to the Go SDK component defined in the root codecov.yml.
The Actions run also publishes the report directory as
sdk-go-integration-coverage.
The detailed design and later phase boundaries are in the Go SDK rewrite plan.
Worker startup calls GetServerInfo, negotiates the highest version in the
intersection of the Server and Go SDK protocol intervals, synchronizes Attribute
indexes, and then opens task streams. The SDK interval is [4,4].
Missing server information, invalid or disjoint intervals, and RPC failures stop
startup before polling. The negotiated protocol is retained for future
protocol-specific paths.
The diagnostic SDK version comes from Go build information for the
github.com/superdurable/dex/sdk-go module. Source builds without module version
metadata report dev; neither value changes compatibility decisions.
Searches return runs that match the query. Include FlowType when using shared
index slots, and keep the query and page size consistent across pages.