Examples

Two end-to-end pattern walkthroughs combining wait/signal, response header directives, and HTTP triggers into something runnable. Both are also seeded into the marketplace under @dispatched so you can clone them into your project in one click.

Async checkout — wait/signal

A multi-step checkout that pauses three times for external input: the customer’s address, their shipping selection, and the payment outcome. Each pause is a wait step that holds the run until something signals it from the outside — a frontend, a webhook, a background worker, whatever fits.

Marketplace entry: @dispatched/async-checkout.

What it shows

  • wait with a single named signal (address-submitted, shipping-selected).
  • wait with any — two possible signals (payment-confirmed or payment-cancelled) wake the same step, and a when guard branches on which one arrived.
  • Reading the signal payload back into the next step via {{ steps.<step_id>.signal.body }}.
  • timeout + on_timeout: "fail" so a stalled checkout doesn’t hang forever.

Shape

create_cart  →  confirm_address (wait)  →  compute_shipping
                                              ↓
                                       select_shipping (wait)
                                              ↓
                                       authorize_payment
                                              ↓
                                       confirm_payment (wait: any)
                                              ↓
                              finalize_order (when payment-confirmed)

Workflow

name: async-checkout
version: 1

triggers:
  - type: http

steps:
  create_cart:
    request:
      method: POST
      url: "{{ secrets.ECOM_API_URL }}/carts"
      body:
        user_id: "{{ trigger.body.user_id }}"

  confirm_address:
    after: [create_cart]
    wait:
      signal: address-submitted
      timeout: 30m
      on_timeout: fail

  compute_shipping:
    after: [confirm_address]
    request:
      method: POST
      url: "{{ secrets.ECOM_API_URL }}/shipping/quote"
      body:
        cart_id: "{{ steps.create_cart.response.body.id }}"
        address: "{{ steps.confirm_address.signal.body }}"

  select_shipping:
    after: [compute_shipping]
    wait:
      signal: shipping-selected
      timeout: 30m
      on_timeout: fail

  authorize_payment:
    after: [select_shipping]
    request:
      method: POST
      url: "{{ secrets.ECOM_API_URL }}/payments/authorize"
      body:
        cart_id: "{{ steps.create_cart.response.body.id }}"
        shipping_method: "{{ steps.select_shipping.signal.body.method }}"

  confirm_payment:
    after: [authorize_payment]
    wait:
      any: [payment-confirmed, payment-cancelled]
      timeout: 15m
      on_timeout: fail

  finalize_order:
    after: [confirm_payment]
    when: "steps.confirm_payment.signal.name == 'payment-confirmed'"
    request:
      method: POST
      url: "{{ secrets.ECOM_API_URL }}/orders"
      body:
        cart_id: "{{ steps.create_cart.response.body.id }}"
{
  "name": "async-checkout",
  "version": 1,
  "triggers": [{ "type": "http" }],
  "steps": {
    "create_cart": {
      "request": {
        "method": "POST",
        "url": "{{ secrets.ECOM_API_URL }}/carts",
        "body": { "user_id": "{{ trigger.body.user_id }}" }
      }
    },
    "confirm_address": {
      "after": ["create_cart"],
      "wait": {
        "signal": "address-submitted",
        "timeout": "30m",
        "on_timeout": "fail"
      }
    },
    "compute_shipping": {
      "after": ["confirm_address"],
      "request": {
        "method": "POST",
        "url": "{{ secrets.ECOM_API_URL }}/shipping/quote",
        "body": {
          "cart_id": "{{ steps.create_cart.response.body.id }}",
          "address": "{{ steps.confirm_address.signal.body }}"
        }
      }
    },
    "select_shipping": {
      "after": ["compute_shipping"],
      "wait": {
        "signal": "shipping-selected",
        "timeout": "30m",
        "on_timeout": "fail"
      }
    },
    "authorize_payment": {
      "after": ["select_shipping"],
      "request": {
        "method": "POST",
        "url": "{{ secrets.ECOM_API_URL }}/payments/authorize",
        "body": {
          "cart_id": "{{ steps.create_cart.response.body.id }}",
          "shipping_method": "{{ steps.select_shipping.signal.body.method }}"
        }
      }
    },
    "confirm_payment": {
      "after": ["authorize_payment"],
      "wait": {
        "any": ["payment-confirmed", "payment-cancelled"],
        "timeout": "15m",
        "on_timeout": "fail"
      }
    },
    "finalize_order": {
      "after": ["confirm_payment"],
      "when": "steps.confirm_payment.signal.name == 'payment-confirmed'",
      "request": {
        "method": "POST",
        "url": "{{ secrets.ECOM_API_URL }}/orders",
        "body": { "cart_id": "{{ steps.create_cart.response.body.id }}" }
      }
    }
  }
}

Driving it

# Start a run; `dispatched run start --async` prints just the run id.
RUN=$(dispatched run start async-checkout --async \
  --data '{"user_id": "user_42"}')

# Watch the step states; you'll see confirm_address go to "waiting".
dispatched run show $RUN

# Wake up confirm_address with the address payload.
dispatched run signal $RUN address-submitted \
  --data '{"street": "100 Main St", "city": "Berlin", "zip": "10115"}'

# …then select_shipping.
dispatched run signal $RUN shipping-selected --data '{"method": "express"}'

# …then confirm_payment. Send `payment-cancelled` instead to bail out.
dispatched run signal $RUN payment-confirmed \
  --data '{"confirmed_at": "2026-05-15T12:00:00Z"}'
# Start a run.
RUN=$(curl -s -X POST http://localhost:4000/workflows/async-checkout \
  -H "Dispatched-Session: $TOKEN" \
  -H "Prefer: respond-async" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_42"}' | jq -r .run_id)

# Watch step states; you'll see confirm_address go to "waiting".
curl -s http://localhost:4000/api/runs/$RUN \
  -H "Dispatched-Session: $TOKEN" | jq .

# Wake up confirm_address.
curl -X POST http://localhost:4000/api/runs/$RUN/signal/address-submitted \
  -H "Dispatched-Session: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"street": "100 Main St", "city": "Berlin", "zip": "10115"}'

# …then select_shipping.
curl -X POST http://localhost:4000/api/runs/$RUN/signal/shipping-selected \
  -H "Dispatched-Session: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method": "express"}'

# …then confirm_payment. Send `payment-cancelled` instead to bail out.
curl -X POST http://localhost:4000/api/runs/$RUN/signal/payment-confirmed \
  -H "Dispatched-Session: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirmed_at": "2026-05-15T12:00:00Z"}'

/api/runs/{id} exposes step status, not step output — if a frontend needs the shipping quotes the run computed, surface them via a response directive or a separate endpoint. For more on the wait step shape, see Workflows → Patterns.

Response header directives — service-driven orchestration

An order-processing workflow where the upstream services steer the run through Dispatched-* response headers instead of the workflow file calling every shot up front.

Marketplace entry: @dispatched/response-header-directives.

What it shows

Every directive the engine accepts gets exercised once:

Header Where Effect in the example
Dispatched-Log All steps Service streams a human-readable progress line into the run’s event log.
Dispatched-Delay process_order Service asks the engine to wait 250ms before kicking off the next step — useful when its read replicas need a moment to catch up.
Dispatched-Spawn process_order Service starts a long-running fulfill-order workflow as a detached child run, so the parent doesn’t have to wait.
Dispatched-Cancel authorize_payment Payment gateway already knows the parallel fraud screen is moot and cancels that run.
Dispatched-Signal screen_for_fraud Fraud service signals back into the same run to ask the workflow to wait for manual review.

The workflow itself just orchestrates four HTTP calls. The interesting behaviour comes from what the services return.

The opt-in model

Each step lists the directive types it accepts under directives. Anything else that comes back gets recorded as a directive.skipped event and discarded. This is deliberate — directives let an upstream trigger cross-run effects (spawning, signalling, cancelling), so each step has to explicitly grant the privilege.

Workflow

name: response-header-directives
version: 1

triggers:
  - type: http

steps:
  process_order:
    request:
      method: POST
      url: "{{ secrets.ORDER_SERVICE_URL }}/orders"
      headers:
        authorization: "Bearer {{ secrets.ORDER_SERVICE_TOKEN }}"
        content-type: application/json
      body: "{{ trigger.body }}"
    directives:
      - log
      - delay
      - spawn

  authorize_payment:
    after: [process_order]
    request:
      method: POST
      url: "{{ secrets.PAYMENT_GATEWAY_URL }}/authorize"
      headers:
        authorization: "Bearer {{ secrets.PAYMENT_GATEWAY_TOKEN }}"
        content-type: application/json
      body:
        order_id: "{{ steps.process_order.response.body.order_id }}"
        amount_cents: "{{ steps.process_order.response.body.total_cents }}"
    directives:
      - log
      - cancel

  screen_for_fraud:
    after: [process_order]
    request:
      method: POST
      url: "{{ secrets.FRAUD_SERVICE_URL }}/screen"
      headers:
        authorization: "Bearer {{ secrets.FRAUD_SERVICE_TOKEN }}"
        content-type: application/json
      body:
        order_id: "{{ steps.process_order.response.body.order_id }}"
        customer_email: "{{ steps.process_order.response.body.customer_email }}"
    directives:
      - signal
      - log

  record_order:
    after: [authorize_payment, screen_for_fraud]
    when: |
      return steps.authorize_payment.response.status == 200
    request:
      method: POST
      url: "{{ secrets.ORDER_SERVICE_URL }}/orders/{{ steps.process_order.response.body.order_id }}/confirm"
      headers:
        authorization: "Bearer {{ secrets.ORDER_SERVICE_TOKEN }}"
        content-type: application/json
    directives:
      - log

response:
  status: 200
  body:
    order_id: "{{ steps.process_order.response.body.order_id }}"
    status: confirmed
{
  "name": "response-header-directives",
  "version": 1,
  "triggers": [{ "type": "http" }],
  "steps": {
    "process_order": {
      "request": {
        "method": "POST",
        "url": "{{ secrets.ORDER_SERVICE_URL }}/orders",
        "headers": {
          "authorization": "Bearer {{ secrets.ORDER_SERVICE_TOKEN }}",
          "content-type": "application/json"
        },
        "body": "{{ trigger.body }}"
      },
      "directives": ["log", "delay", "spawn"]
    },
    "authorize_payment": {
      "after": ["process_order"],
      "request": {
        "method": "POST",
        "url": "{{ secrets.PAYMENT_GATEWAY_URL }}/authorize",
        "headers": {
          "authorization": "Bearer {{ secrets.PAYMENT_GATEWAY_TOKEN }}",
          "content-type": "application/json"
        },
        "body": {
          "order_id": "{{ steps.process_order.response.body.order_id }}",
          "amount_cents": "{{ steps.process_order.response.body.total_cents }}"
        }
      },
      "directives": ["log", "cancel"]
    },
    "screen_for_fraud": {
      "after": ["process_order"],
      "request": {
        "method": "POST",
        "url": "{{ secrets.FRAUD_SERVICE_URL }}/screen",
        "headers": {
          "authorization": "Bearer {{ secrets.FRAUD_SERVICE_TOKEN }}",
          "content-type": "application/json"
        },
        "body": {
          "order_id": "{{ steps.process_order.response.body.order_id }}",
          "customer_email": "{{ steps.process_order.response.body.customer_email }}"
        }
      },
      "directives": ["signal", "log"]
    },
    "record_order": {
      "after": ["authorize_payment", "screen_for_fraud"],
      "when": "return steps.authorize_payment.response.status == 200",
      "request": {
        "method": "POST",
        "url": "{{ secrets.ORDER_SERVICE_URL }}/orders/{{ steps.process_order.response.body.order_id }}/confirm",
        "headers": {
          "authorization": "Bearer {{ secrets.ORDER_SERVICE_TOKEN }}",
          "content-type": "application/json"
        }
      },
      "directives": ["log"]
    }
  },
  "response": {
    "status": 200,
    "body": {
      "order_id": "{{ steps.process_order.response.body.order_id }}",
      "status": "confirmed"
    }
  }
}

What each upstream needs to return

The workflow above is just the orchestrator’s view. The directives come from the HTTP responses the services it calls return.

The order service emits log lines, a brief delay, and spawns a detached fulfillment run:

HTTP/1.1 201 Created
Content-Type: application/json
Dispatched-Log: validated 3 line items, charging USD 49.99
Dispatched-Delay: 250ms
Dispatched-Spawn: fulfill-order; input=eyJvcmRlcl9pZCI6Im9yZF80MiJ9; mode=detach

{"order_id": "ord_42", "total_cents": 4999, "customer_email": "..."}

Dispatched-Spawn‘s input is base64-encoded JSON; it becomes the child’s trigger.body. mode=detach (the default and only currently supported mode) means the parent does not wait for the child.

The payment gateway cancels the parallel fraud screen when it already flagged the charge during authorization:

HTTP/1.1 200 OK
Content-Type: application/json
Dispatched-Log: authorization succeeded, capture pending
Dispatched-Cancel: run_xyz789

{"auth_id": "auth_abc"}

The fraud service signals back into the same run to ask for manual review:

HTTP/1.1 200 OK
Content-Type: application/json
Dispatched-Signal: run=run_parent; name=needs-review; data=eyJyZWFzb24iOiJ2ZWxvY2l0eSJ9

{"verdict": "manual_review"}

The optional data parameter is base64-encoded JSON delivered as the signal’s payload.

For the full directive reference (events, limits, encoding rules), see Integrating Services → Directives.

Cloning from the marketplace

Both examples are seeded under @dispatched with secret placeholders for the URLs and tokens they reference. From the Marketplace, search for the slug, click Use this workflow, and you’ll get a copy in your project with the secrets pre-listed as pending placeholders on the Secrets tab. Fill them in and you’re ready to trigger.