Concord design

Hotel reservation assistant

User picks a hotel, the app drafts a reservation, finance approves bookings over $500, the vendor is called with an idempotency key, and the user can cancel within a 24-hour window that triggers the vendor cancel endpoint.

Triggeruser_request Planasync · deterministic Cancellationcompensate_then_stop Approvals1 (conditional, >$500) Side effects2 (book_hotel, notify_user)
02

User journey

End-traveler flow from hotel selection to confirmed reservation, with a conditional finance approval branch and a post-confirmation cancellation window.

User journey
flowchart LR Pick([User picks hotel in app]) --> Draft["Draft created
user reviews"] Draft --> Reason["User enters
reason + confirms"] Reason --> Gate{"total > $500?"} Gate -->|no| Book["Vendor book
idempotent"] Gate -->|yes| Wait["Pending finance
approval"] Wait --> Decide{"approver decides"} Decide -->|approved| Book Decide -->|rejected| End1([User notified · cancelled]) Book --> Conf([Booking artifact + email]) Conf --> Window["Cancellation window
24h"] Window -->|cancels| Comp([Vendor cancel · compensated]) Window -->|expires| Final([Final]) style Gate fill:#F5E0D2,stroke:#D97757 style Decide fill:#F5E0D2,stroke:#D97757 style Conf fill:#F1F2EC,stroke:#6B7B5A style Final fill:#F1F2EC,stroke:#6B7B5A

"Done" for the user is the booking artifact (confirmation page + email with vendor confirmation number) plus a visible 24h cancel button. "Done" for the org is a settled domain_effects row for book_hotel and an audit trail tying the reservation to the finance approval (when applicable).

03

Trigger / ingress

The workflow starts when the user taps Confirm on a draft they already shaped in the app.

Note

The draft itself is created by a prior lightweight command (hotel_reservation.draft) with its own idempotency key keyed on (user_id, hotel_id, check_in, check_out). The Confirm command depends on the draft via command_dependencies.

04

Command(s)

Two commands

Per-command shape

command_id          uuid
command_type        hotel_reservation.confirm
requested_by        user_id
ingress             user_request
payload             { draft_id, reason, accepted_terms_at }
context             { workspace_id, user_id, app_id, trace_id }
status              created → validated → queued → running → ...
cancellation_mode   compensate_then_stop
idempotency_key     confirm_booking:{draft_id}
dbos_workflow_id    set on enqueue

Status path: created → validated → queued → running → succeeded on the happy path. On user-initiated cancel inside the 24h window: running → cancelling → compensating → compensated → cancelled. On policy denial or approval rejection: failed.

05

Policy stack

Policies are composed in order on the confirm command. The first non-allow decision short-circuits the rest (except where noted).

permission
cost
approval_requirement
external_sharing
destructive_action
rate_limit
connector_scope
06

Execution plan

Async deterministic plan. Sync prelude validates and creates the command record; the bulk runs on durable queues.

Execution plan
flowchart TB T([Tap Confirm]) --> S1["1 · validate_inputs
sync_function"] S1 --> S2["2 · load_draft
sync_function"] S2 --> S3["3 · run_policy_stack
sync_function"] S3 --> D1{"approval required?"} D1 -->|yes| S4["4 · create_approval_request
human_task · queue: high_risk_operations"] D1 -->|no| S6 S4 --> S5["5 · wait_for_approval
signal · expires 48h"] S5 --> S6["6 · book_hotel
connector_call · queue: connector_calls
idempotency_key set"] S6 --> S7["7 · persist_booking_artifact
sync_function"] S7 --> S8["8 · notify_user
connector_call · queue: notifications"] S8 --> S9["9 · schedule_window_close
schedule · +24h"] S9 --> Done([succeeded]) style D1 fill:#F5E0D2,stroke:#D97757 style Done fill:#F1F2EC,stroke:#6B7B5A

Queues

connector_calls
high_risk_operations
notifications
scheduled_maintenance

Execution modes used: sync_function, connector_call, human_task, external_job (the scheduled window-close timer).

07

Effects table

Effects are declared upfront from CoreResult.effects and inserted as domain_effects rows in planned state; the runtime adapter walks them.

effect_typeside_effect?idempotency_keycompensationcounter_pure?
hotel_booking.book yes (vendor mutation, payment) book_hotel:{draft_id} cancel_reservation yes (vendor confirms full reversal inside window)
notification.user_email yes (external send) notify_booking:{command_id} send_cancellation_email no (new email, not an unsend)
Caution

The vendor's idempotency window may be shorter than ours. Effect retries past the vendor's TTL would create a duplicate booking. Pin RetryPolicy.max_attempts for connector_call to a value that fits inside the vendor's documented idempotency window.

08

Memory

Lightweight, opt-in. The workflow itself doesn't require memory to function; it's an enhancement.

09

Artifacts

All three are Postgres pointers; no large blobs to push to object storage. Versioning: each confirmation is a single immutable row; a cancellation produces a new booking_cancellation artifact rather than mutating the original.

10

Approvals

Single conditional gate. Fires only when total_amount > $500. Reviewer group: finance_approvers. First member to act wins.

Review packet

On rejected or expired: command transitions to failed, no effects executed, user gets a rejection email. No alternate path — the user can re-draft.

Approval flow
sequenceDiagram autonumber actor U as User participant S as Concord
(confirm command) participant A as Finance approver participant V as Vendor hotel API U->>S: tap Confirm (draft_id, reason) S->>S: run_policy_stack Note over S: approval_requirement
returns require_approval S->>A: create approval_request
(review_packet) A-->>S: approve | reject | (timeout 48h) alt approved S->>V: book hotel
idempotency_key set V-->>S: confirmation_number S->>U: booking_confirmation artifact + email else rejected or expired S->>U: rejection email Note over S: command → failed end
11

Agent / swarm config

Not agentic. The plan is deterministic; no AgentRun or SwarmRun is involved.

Note

If a future iteration adds a "concierge agent" that suggests upgrades or alternate hotels, it would sit before the draft command, scoped to allowed_tools = [search_hotels, retrieve_memory] and allowed_connectors = [vendor_search, llm_provider]. The confirm workflow itself stays deterministic.

12

Audit events

Single append-only domain_events table. All rows here use purpose = audit unless noted.

command.created
command.validated
policy.decision
approval.requested
approval.decided
effect.planned
effect.succeeded
effect.failed
compensation.started
compensation.succeeded
artifact.published
command.cancelled

Business stream (purpose = event) emits booking.confirmed, booking.cancelled, booking.rejected for downstream consumers (analytics, finance ledger).

13

Cancellation model

cancellation_mode = compensate_then_stop. The vendor booking is real money and the user is promised a 24h reversible window, so a clean undo is non-negotiable.

Two distinct cancel paths exist:

  1. Mid-flight cancel (before book_hotel succeeds, e.g. during approval wait) — nothing to compensate at the vendor; the approval row is marked cancelled, command exits at cancelled.
  2. Post-confirmation cancel (within 24h) — the workflow re-enters via a new hotel_reservation.cancel command that triggers the compensation chain on the original.
Cancellation cascade
flowchart TB U([User taps Cancel]) --> Check{"< 24h
since confirm?"} Check -->|no| Reject([Refuse · out of window]) Check -->|yes| New["Submit hotel_reservation.cancel
command"] New --> Trigger["Trigger compensate_then_stop
on original command"] Trigger --> R1["Reverse · notification.user_email
send_cancellation_email"] Trigger --> R2["Reverse · hotel_booking.book
cancel_reservation at vendor"] R1 --> Done([compensated → cancelled]) R2 --> Done style Check fill:#F5E0D2,stroke:#D97757 style Done fill:#F1F2EC,stroke:#6B7B5A

Status transitions on the original command: succeeded → cancelling → compensating → compensated → cancelled. propagate_cancellation = true on command_dependencies means a draft-side cancel never gets here in production, but it's defensive correctness.

14

Compensation graph

Two-effect chain, each with a declared inverse. The graph validator at startup confirms no cycles, depth = 1, and both compensations declare of targets that exist.

Compensation graph
flowchart LR E1["hotel_booking.book
produces: vendor reservation"] -.compensated by.-> C1["cancel_reservation
counter_effects=true"] E2["notification.user_email
produces: outbound email"] -.compensated by.-> C2["send_cancellation_email
counter_effects=false"] C1 --> O1([reservation released at vendor]) C2 --> O2([user receives cancellation email]) style E1 fill:#FAEEE3,stroke:#D97757 style E2 fill:#FAEEE3,stroke:#D97757 style C1 fill:#F1F2EC,stroke:#6B7B5A style C2 fill:#F1F2EC,stroke:#6B7B5A

Order of compensation walk is reverse of planned effects: notify-cancellation runs after the vendor cancel succeeds, so the user is never told the booking is cancelled if the vendor leg fails.

cancel_reservation declares counter_effects = true; the runtime drift detector compares emitted effects against the declared inverse set and writes a compensation_drift row if they diverge. send_cancellation_email declares counter_effects = false — it's a new outbound email, not an inverse of the original send.

15

Runtime config

Runtime adapter

DBOS for v1. The compensation chain has depth 1 and is orchestrated by Concord as a sub-workflow, so SAGA_COMPENSATION_NATIVE isn't required.

Required adapter capabilities

DURABLE_WORKFLOWS
DURABLE_STEPS
QUEUES
SCHEDULES
SIGNALS
EFFECT_INTERCEPTION

Retry policies

operationretryablemax_attemptsbackoff_secondsrequires_idempotency_key
book_hotelyes (transient only)32, 6, 18yes
cancel_reservationyes (transient only)51, 3, 9, 27, 60yes
notify_useryes41, 3, 9, 27yes
wait_for_approvalno1n/a

Connector

16

Test plan

Unit (functional core)

Integration (Postgres + runtime adapter)

Workflow (end-to-end)

Safety

Boundary discipline

Acceptance criterion

For every confirmed booking, exactly one book_hotel effect row reaches succeeded with a vendor confirmation number, and the user sees the confirmation artifact within 30 seconds of approval (or immediately, in the no-approval path). For every cancel inside 24h, exactly one matching cancel_reservation effect reaches succeeded.

17

Open questions / risks