Skip to main content

Order Processing

View as Markdown

Run the order Flow that Why Dex? describes. You start Dex, run the samples app, start an order over HTTP, approve the shipment, and look at the run in Dex Web.

SDKs: Python · Go · TypeScript · Java · Rust

1. Start Dex

Start a local Dex with dexcli dev, as Run locally shows. Keep it running.

In a second terminal, get the examples. Every command below runs from the dex folder that this creates.

git clone https://github.com/superdurable/dex.git
cd dex

2. Flow, Steps, and RPCs

The code below is the Flow from Why Dex?, with its Steps, its approve and describe RPCs, and its Attribute.

Pass the mock payment and shipping API to the Flow constructor. Do not set them later with a setter, and do not read them from a global variable.

Each Step passes an idempotency key built from the order ID, which is also the Flow ID. Idempotency keys explains why.

The order-status Attribute is indexed, so you can search Flows by status. Its index key, keyword1, names a search slot. Each indexed Attribute takes one slot, so the limit on slots matters only when you index many Attributes. Read and search Attributes explains slots.

Ship's options route its failure to Refund once its retries run out. The second StepOptions in that route apply to the Refund Step it moves to, here a short retry.

The dex:group and dex:explanation comments label each Step in the Flow graph that Dex Web and these docs draw. They do not change what the code does.

order-processingPython
order_status = Attribute(
"order-status",
str,
AttributeIndex(IndexType.KEYWORD, "keyword1"),
)
seller_ok = Channel[str]("seller-ok", str)


# dex:group group-id:payment group-label:"Payment"
# dex:explanation text:"Charge the customer for the order."
class Charge(Step[OrderRequest]):
def __init__(self, service: MyDependencyService) -> None:
self.service = service

def get_step_options(self) -> StepOptions:

def execute(self, context: Context, input: OrderRequest) -> StepDecision:
self.service.charge_user(
input.email,
input.customer_id,
input.amount,
idempotency_key=f"charge-{input.order_id}",
)
order_status.set(context, "charged")
return go_to(Ship, input)


# dex:group group-id:shipping group-label:"Shipping"
# dex:explanation text:"Wait for seller approval, remind after 24 hours, then ship the order."
class Ship(Step[OrderRequest]):
def __init__(self, service: MyDependencyService) -> None:
self.service = service

def get_step_options(self) -> StepOptions:

def wait_for(self, context: Context, input: OrderRequest) -> Wait:
return Wait.any_of(
seller_ok.for_one(),
Timer.by_duration(timedelta(hours=24)),
)

def execute(self, context: Context, input: OrderRequest) -> StepDecision:
if context.has_timer_fired():
self.service.send_email(
input.email,
"Reminder: approve shipment",
"Please approve or provide a tracking number.",
)
return go_to(Ship, input)
self.service.ship_item(
input.order_id,
input.test_fail_at_shipping,
idempotency_key=f"ship-{input.order_id}",
)
order_status.set(context, "shipped")
return graceful_complete(f"shipped:{input.order_id}")


# dex:group group-id:refund group-label:"Refund"
# dex:explanation text:"Refund the order when shipping keeps failing."
class Refund(Step[OrderRequest]):
def __init__(self, service: MyDependencyService) -> None:
self.service = service

def execute(self, context: Context, input: OrderRequest) -> StepDecision:
self.service.update_external_system(
f"refund {input.order_id}", idempotency_key=f"refund-{input.order_id}"
)
order_status.set(context, "refunded")
return graceful_complete(f"refunded:{input.order_id}")


class OrderProcessingFlow(Flow[OrderRequest]):
def __init__(self, service: MyDependencyService) -> None:
self.service = service
self.refund = Refund(service)
self.ship = Ship(service)
self.charge = Charge(service)

def get_steps(self) -> StepList[OrderRequest]:
return StepList.start_step(self.charge).other_steps(self.ship, self.refund)

def get_persistence_schema(self) -> PersistenceSchema:
return PersistenceSchema.of(order_status, seller_ok)

@rpc
def approve(self, context: Context, _note: str) -> RPCResult[str]:
seller_ok.publish(context, "approved")
return RPCResult("ok")

@rpc
def describe(self, context: Context) -> RPCResult[str]:
return RPCResult(order_status.get(context))

Example: examples/python/dex_examples/products/order-processing

3. Worker and Client

Create a Registry, a Worker, and a Client that connect to dexcli at 127.0.0.1:8801. Construct the Flow with the mock API service, then register it. The samples app registers every example Flow in one Registry, so the snippets below show only the lines for this Flow where they can. The Python samples app runs on an async web framework, so it uses AsyncWorker and AsyncClient. The Quick Start uses the synchronous Worker and Client, which work the same way.

The Worker connects out to dexcli to receive tasks, so it needs no open inbound port. Workers and the task queue covers capacity, readiness, and production setup.

order-processingPython
        service = MyDependencyService()

self.order_processing = OrderProcessingFlow(service)
order-processingPython
        self.registry = Registry(tuple(flows), allow_async_handlers=True)
config.blob_cache_dir.mkdir(parents=True, exist_ok=True)
self.blob_cache = open_blob_cache(
BlobCacheConfig(str(config.blob_cache_dir), 1 << 30)
)
self.worker = AsyncWorker(
self.registry,
self.blob_cache,
WorkerOptions(server_address=config.server_address),
)
self._client = AsyncClient(
self.registry,
self.blob_cache,
ClientOptions(server_address=config.server_address),
)

Example: examples/python/dex_examples/products/order-processing

4. Controller

The HTTP controller has three handlers:

  • start starts the Flow, then waits for the Charge Step with the Client wait-for-Step-completion API.
  • approve calls the approve RPC, which publishes the approval to the seller Channel.
  • describe calls the describe RPC, which reads order-status.

The controller gets the Client and the Flow from its constructor or factory arguments.

order-processingPython
    @blueprint.get("/start")
async def start() -> Response:
flow_id = new_flow_id("order-processing")
request = OrderRequest(
flow_id,
"buyer@example.com",
"customer-1",
42,
optional_query("testFailAtShipping", "") == "true",
)
run_id = await app_state.client.start_flow(
app_state.order_processing,
flow_id,
request,
start_options(),
)
await app_state.client.wait_for_step_completion(
flow_id,
CHARGE_STEP,
WaitForStepCompletionOptions(),
)
return started_flow(flow_id, run_id)

@blueprint.get("/approve")
async def approve() -> Response:
output = await app_state.client.invoke_rpc(
app_state.order_processing.approve,
_required_flow_id(),
optional_query("notes", ""),
)
return jsonify(output)

@blueprint.get("/describe")
async def describe() -> Response:
flow_id = _required_flow_id()
status = await app_state.client.invoke_rpc(
app_state.order_processing.describe,
flow_id,
)
return jsonify({"flowID": flow_id, "status": status})

return blueprint


def _required_flow_id() -> str:
"""Reads the Flow ID from flowId, or from its alias workflowId."""
return optional_query("workflowId", "") or required_query("flowId")

Example: examples/python/dex_examples/products/order-processing

5. Run

Keep dexcli dev running. In the second terminal, from the dex folder, install and start the samples app for your language:

cd examples/python
uv sync --locked
uv run --frozen python main.py

The samples app runs a Worker and an HTTP controller on port 8080.

In a third terminal, start an order, open Dex Web, and approve the shipment:

curl -s 'http://127.0.0.1:8080/products/order-processing/start'

Copy flowID from the JSON. Open http://127.0.0.1:8802, find that Flow, and open its details. The Ship Step is waiting.

curl -s "http://127.0.0.1:8080/products/order-processing/approve?flowId=FLOW_ID"

Replace FLOW_ID with the flowID you copied. The Flow ships the item and completes.

Try the refund path

Start an order whose shipping always fails:

curl -s 'http://127.0.0.1:8080/products/order-processing/start?testFailAtShipping=true'

Approve it as before. The Ship Step fails and retries for 3 seconds. Then the Flow moves to the Refund Step, which refunds the buyer and completes the Flow. Read its status:

curl -s "http://127.0.0.1:8080/products/order-processing/describe?flowId=FLOW_ID"

The status is refunded. In Dex Web, the Execution graph shows Charge, Ship, and then Refund.

6. Dex Web

After you approve the order, open the Flow in Dex Web. The Execution graph shows Charge, then Ship:

Dex Web Execution graph after approve

The Timeline tab shows the same run's events from left to right. Select an event to read its input and output below the Timeline:

Dex Web Timeline after approve

Next

  • Concepts: Flows, Steps, Channels, Timers, and the other building blocks this example uses.
  • Your Application: how to add a Worker and a Client to your own service.
  • Dex vs. Databases: the same order process built on a database and queues.
  • Examples: more complete examples, including two AI agents.