Client Basics
Client calls are made by your application, not by a Step. The examples below use a Flow ID named order-123.
Start a Flow
StartFlow creates a Flow execution and returns as soon as Dex Server accepts it. It takes a registered Flow, a business Flow ID, the start Step input, and StartFlowOptions.
The Flow ID is the identity your application chooses. Dex Server creates the Run ID. See Flow options for timeout, request idempotency, ID reuse, retry, initial Attributes, and configuration overrides.
run_id = await app_state.client.start_flow(
app_state.client_apis, "order-123", "first order", start_options()
)
Example: examples/python/dex_examples/primitives/client-apis/controller.py
Stop a Flow
StopFlow closes an active Flow. The default asks it to cancel cooperatively. You can instead terminate it immediately, or fail it with a reason.
await client.stop_flow("order-123", StopFlowOptions(reason="customer cancelled"))
Example: examples/python/dex_examples/products/job-post/controller.py
Invoke an RPC
InvokeRPC calls a registered RPC handler and returns its result. A query-only RPC can read a retained terminal execution. An RPC that needs to commit effects requires an active execution.
result = await client.invoke_rpc(flow.trigger, "order-123", "approved")
Example: examples/python/dex_examples/primitives/rpc/controller.py
Read and write Flow state
Use Flow RPCs as the application boundary for changing Attribute, AttributeMap, Channel, and ChannelMap state. Define a typed RPC for each command, select only the state it needs, then call it with InvokeRPC. This keeps authorization, validation, locks, transactions, and state changes inside the Flow.
To read stored Attribute values, call GetAttributes. It reads committed state directly from Dex Server. Use an RPC instead when the read needs Flow-side validation or authorization, Channel messages, a derived view, or Attribute locks. Existing read-only RPCs keep working, so an application can move to GetAttributes at its own pace.
The Client still provides WaitForAttributeMatch as a blocking observation API. Use it when an application must wait for a known scalar Attribute condition rather than read an arbitrary state snapshot.
Read Attributes
GetAttributes reads the values of registered Attributes and AttributeMap instances from outside the Flow. Pass one read per value: a singleton Attribute, or one AttributeMap instance. The API has only a batch form. To read one value, pass one read.
The SDKs name it GetAttributes in Go, get_attributes in Python and Rust, and getAttributes in Java and TypeScript.
The Rust call blocks the calling thread, like every other Rust Client method. It takes a tuple of 1 to 12 reads and returns a tuple of the same size. To read more values, make several calls. Values from different calls can come from different commits.
The Attribute primitive example reads a status Attribute, the payment instance of a progress AttributeMap, and an email Attribute that the Flow never sets.
flow = app_state.attribute
payment_progress = flow.progress.instance("payment")
try:
values = await app_state.client.get_attributes(
required_query("workflowId"),
flow.status,
payment_progress,
flow.email,
)
except FlowNotFoundError as error:
response = jsonify({"error": str(error)})
response.status_code = 404
return response
return jsonify(
{
"status": values.get(flow.status, "not started"),
"paymentProgress": values.get(payment_progress, "not started"),
# This Flow never sets email, so get returns None.
"email": values.get(flow.email),
}
)
Example: examples/python/dex_examples/primitives/attribute/controller.py
Run the HTTP example from the examples playground. Its Read Attributes endpoint is available in every example server.
The read has these semantics:
- One commit. The Client sends every read in one server request. All values come from the same committed state.
- Current or latest run. The call reads the current run, or the latest run when no run is open. A closed run stays readable until Dex purges it.
- No Worker. Dex Server answers the read itself. It dispatches nothing to a Worker, uses no Worker capacity, and works while no Worker is running.
- Committed state only. The call does not see writes that a running Step or RPC has made but not yet committed. An RPC handler, by contrast, also sees the writes it made earlier in the same invocation. GetAttributes takes no Attribute locks, so use an RPC for a read-modify-write.
- Large and hard-delete values. Dex Server resolves hard-delete values before it responds, and a deleted value reads as unset. The Client loads other large values from blob storage, then decodes every value with its definition. The call returns values only after all of them decode.
An Attribute or AttributeMap instance without a stored value is unset. Each SDK represents an unset read in its own way:
| SDK | Unset read |
|---|---|
| Go | The destination keeps its previous value. AttributeReadResult.IsSet and IsMapInstanceSet return false. |
| Python | The read is absent from the returned AttributeValues. values.get(read, default) returns the default, read in values is false, and values[read] raises KeyError. |
| Java | AttributeValues.get(read) returns Optional.empty(). |
| TypeScript | The tuple element is undefined. |
| Rust | The tuple element is None. Each element is an Option of the read's value type. |
In Python and TypeScript, a stored value that decodes to None or null is still set. In Java, a stored value that decodes to null reads as Optional.empty(), the same as an unset value, because an Optional cannot hold null.
Each read must use a definition that a Flow in the Client's Registry declares. A definition from any registered Flow is accepted. Dex reads the key by name and does not check that the target Flow's type declares it, so a same-named Attribute with another value type fails to decode.
The call reports these errors:
- A Flow ID that does not exist, or whose latest run is being purged, returns a Flow-not-found error. It is FlowNotFoundError in Go, Python, and TypeScript, FlowNotFoundException in Java, and SdkError::FlowNotFound in Rust.
- A definition that is not registered in the Client's Registry, or that does not match the registered definition, fails before any request. The error is FlowDefinitionError in Go, Python, and TypeScript, FlowDefinitionException in Java, and SdkError::FlowDefinition in Rust. Go, Python, and Rust compare the definition with the registered one, including its value type. Java and TypeScript require the exact registered definition object.
- An empty or blank Flow ID, no reads, or two reads of the same value are rejected before any request. An empty AttributeMap instance key, or a key that contains /, is also rejected before any request. Java throws IllegalArgumentException, and Rust returns SdkError::InvalidArgument. In Rust, an empty tuple of reads does not compile.
- In Python and TypeScript, passing an AttributeMap without instance raises a TypeError before any request. In Go, ReadAttribute accepts only an Attribute and ReadAttributeMapInstance only an AttributeMap, so the compiler rejects this mistake. In Java and Rust, an AttributeMap is not a read, so the compiler also rejects it.
- A stored value that cannot be decoded returns a value mapping error. It is ValueMappingError in Go, Python, and TypeScript, ValueMappingException in Java, and SdkError::ValueMapping in Rust.