Skip to main content

Deal DSL

Define a durable deal as data, then run the same interpreter for any sellable item.

Product requirements

The process represents one buyer's deal for one item. The item can be a dataset, a physical product, a subscription, a service, or another sellable good. It must:

  1. Accept a definition containing a process ID, item ID and name, initial state, initial state data, states, actions, waits, and transitions.
  2. Validate the definition before execution. Every referenced state and action must exist, identifiers must be valid, and each condition name must identify one wait point.
  3. Run InitializeDeal once. It stores an immutable definition snapshot, buyer and item identity, initial state data, and the first state to enter.
  4. Run WaitForDealCondition before entering a state. If the state declares a precondition, the Step waits for one message on that condition's keyed Channel. Otherwise it proceeds immediately.
  5. Merge the condition message into DealStateData, then execute the state's actions in their declared order. Each action gets its own ExecuteDealAction execution and durable checkpoint.
  6. Run EvaluateDealTransition after the actions. It can wait for another external condition, compare one state-data value against ordered equality cases, and select the matching state or the fallback state.
  7. Repeat the interpreter loop until it reaches a state without a transition. The Flow then completes with the accumulated state data.
  8. Keep running executions independent from later definition edits. The Go application stores editable definitions in PostgreSQL and passes a snapshot into each new Flow.

The Flow waits for precondition messages, transition messages, and retry delays after a failed Step attempt. A wait does not occupy a Worker.

Potential failures include:

  • Definition validation can reject a missing item, an unknown initial or destination state, a duplicate condition name, an invalid identifier, or an unregistered action.
  • A condition message can be malformed, target an unknown or stopped Flow, or arrive for a condition that the definition does not expose.
  • A condition result is invalid unless exactly one message satisfies the keyed wait. The Step fails instead of merging ambiguous input.
  • Payment, refund, sample delivery, or item delivery can time out or return an error. Dex retries the failed Step without rerunning earlier completed actions.
  • An action can succeed remotely while its response is lost. Production adapters should use the Flow ID and Step execution identity as an idempotency key.
  • State data can omit a decision key. The transition then follows its explicit fallback state.
  • A definition can contain a business loop with no eventual external decision. The Flow remains durable, but product-level validation or timeouts must bound unwanted loops.

Flow design

This interactive definition graph is generated from the runnable Python Flow.

Definition graph

DealDSLFlow

Valid

python · examples/python/dex_examples/products/deal_dsl/deal_dsl_flow.py

The Flow is a small durable interpreter. A definition chooses the business path; four Step types provide the execution machinery:

DealStart(definition snapshot, buyer, item)


InitializeDeal


WaitForDealCondition ◄──────────────┐
wait on keyed Channel │
│ │
▼ │
ExecuteDealAction │
one execution per ordered action │
│ │
▼ │
EvaluateDealTransition │
optional wait + case / fallback │
│ │
next state? ── yes ─────────────┘
│ no

complete with accumulated state data

It combines these design patterns:

  • Interpreter keeps the executable Flow stable while process definitions supply states, waits, actions, and transitions.
  • Finite-state machine makes the current state and every transition explicit and inspectable.
  • Immutable execution snapshot prevents an edit to the catalog definition from changing an in-flight deal.
  • Keyed Channel rendezvous maps each named external condition to its own durable inbox without creating one Channel definition per condition.
  • One-action checkpoint isolates retries and records the exact action that failed. Ordered actions never collapse into one opaque Step.
  • Guarded transition with fallback evaluates ordered equality cases against shared state data and always has an explicit default path.
  • Blackboard state lets condition messages and actions contribute small key-value updates to one durable DealStateData map.
  • Entity store gives each Go execution a stable Flow ID and searchable process, item, buyer, current-state, and pending-condition Attributes.

The portable SDK examples use one action list per state. The Go product example extends the same loop with separate pre-actions and post-actions, a PostgreSQL definition catalog, search endpoints, and a browser UI.

Core implementation

Each tab includes the Flow signature, persistence schema, and the complete four-Step interpreter path. Every snippet comes from the linked runnable example.

class DealDSLFlow(Flow[DealStart]):
definition = Attribute("DealDefinition", DealDefinition)
state_data = Attribute("DealStateData", dict[str, str])
process_id = Attribute("DealProcessID", str)
item_id = Attribute("DealItemID", str)
buyer_id = Attribute("DealBuyerID", str)
current_state = Attribute("DealCurrentState", str)
pending_condition = Attribute("DealPendingCondition", str)
condition_messages = ChannelMap("DealConditionMessages", dict[str, str])

def __init__(self) -> None:
self.initialize = InitializeDeal(self)
self.wait_for_condition = WaitForDealCondition(self)
self.execute_action_step = ExecuteDealAction(self)
self.evaluate_transition = EvaluateDealTransition(self)

def get_steps(self) -> StepList[DealStart]:
return StepList.start_step(self.initialize).other_steps(
self.wait_for_condition,
self.execute_action_step,
self.evaluate_transition,
)

def get_persistence_schema(self) -> PersistenceSchema:
return PersistenceSchema.of(
self.definition,
self.state_data,
self.process_id,
self.item_id,
self.buyer_id,
self.current_state,
self.pending_condition,
self.condition_messages,
)


class InitializeDeal(Step[DealStart]):
def execute(self, context: Context, input: DealStart) -> StepDecision:
definition = input.definition
definition.state(definition.initial_state)
self.flow.definition.set(context, definition)
self.flow.process_id.set(context, definition.process_id)
self.flow.item_id.set(context, definition.item_id)
self.flow.buyer_id.set(context, input.buyer_id)
self.flow.state_data.set(context, dict(definition.initial_state_data))
return go_to(WaitForDealCondition, StateStepInput(definition.initial_state))


class WaitForDealCondition(Step[StateStepInput]):
def wait_for(self, context: Context, input: StateStepInput) -> Wait:
state = self.flow.definition.get(context).state(input.state_name)
if state.pre_condition is None:
return Wait.skip_immediately()
self.flow.pending_condition.set(context, state.pre_condition.name)
return Wait.until(self.flow.condition_messages.for_one(state.pre_condition.name))

def execute(self, context: Context, input: StateStepInput) -> StepDecision:
state = self.flow.definition.get(context).state(input.state_name)
if state.pre_condition is not None:
self.flow.merge_condition(context, state.pre_condition.name)
self.flow.pending_condition.delete(context)
self.flow.current_state.set(context, state.name)
if state.actions:
return go_to(ExecuteDealAction, ActionStepInput(state.name, 0))
return go_to(EvaluateDealTransition, input)


class ExecuteDealAction(Step[ActionStepInput]):
def execute(self, context: Context, input: ActionStepInput) -> StepDecision:
state = self.flow.definition.get(context).state(input.state_name)
if input.action_index < 0 or input.action_index >= len(state.actions):
raise ValueError(f"invalid action index {input.action_index}")
self.flow.execute_action(context, state.actions[input.action_index])
next_index = input.action_index + 1
if next_index < len(state.actions):
return go_to(ExecuteDealAction, ActionStepInput(state.name, next_index))
return go_to(EvaluateDealTransition, StateStepInput(state.name))


class EvaluateDealTransition(Step[StateStepInput]):
def wait_for(self, context: Context, input: StateStepInput) -> Wait:
transition = self.flow.definition.get(context).state(input.state_name).transition
if transition is None or transition.wait_for is None:
return Wait.skip_immediately()
self.flow.pending_condition.set(context, transition.wait_for.name)
return Wait.until(self.flow.condition_messages.for_one(transition.wait_for.name))

def execute(self, context: Context, input: StateStepInput) -> StepDecision:
transition = self.flow.definition.get(context).state(input.state_name).transition
if transition is None:
return graceful_complete(self.flow.state_data.get(context))
if transition.wait_for is not None:
self.flow.merge_condition(context, transition.wait_for.name)
self.flow.pending_condition.delete(context)
state_data = self.flow.state_data.get(context)
next_state = transition.else_state
for deal_case in transition.cases:
if state_data.get(transition.key) == deal_case.equals:
next_state = deal_case.go_to_state
break
return go_to(WaitForDealCondition, StateStepInput(next_state))

Example: examples/python/dex_examples/products/deal_dsl/deal_dsl_flow.py

Demo

This completed Go run sold the Premium research package item to buyer-demo. The Flow waited for buyer-confirmation, charged the buyer, delivered the item, and completed with itemDeliveryStatus: delivered. Each action appears as a separate completed ExecuteDealAction execution.

Completed Deal DSL direct-purchase Step execution graph