Primitive spec
Daneel
The workflow primitive. Daneel is where the consequences of platform events run — reminders, bookings, integrations, kitchen routing and coursing — with idempotency, retries, compensation, and a durable execution record for every side effect.
What it owns
Daneel owns workflow definitions and the durable record of their execution. Every named workflow is a sequence of actions with explicit idempotency keys, retry policy, and compensation steps. The execution record is what an operator inspects when something went wrong, and what a workflow uses to resume cleanly after a worker crash.
Daneel does not own outbound communication (Speaker), payment movement (Payments), or scheduling state (Seldon). It calls those primitives as action steps and records what came back.
Concepts
- Step handler
- A single named step a workflow can run —
notify(send through Speaker),hober.fire_lines,hober.fire_course,hober.serve_line,daneel.route_station,seldon.book_offer,seldon.cancel_booking,riose.publish_week,devers.reorder_sweep,trevize.ical_pull, orprimitive.invokefor any registered capability. - Action
- A workflow definition (
act_): a trigger (an event class with an optional CEL filter, or a schedule) plus a DAG of steps with control flow, signals, and timers. The DAG and itsdagVersionlive on the Action; each run snapshots the version that started it, so in-flight runs survive edits. - ActionRun
- A live or completed run of an Action (
arun_). Carries the input, every step result, and its status (pending,running,succeeded,failed,skipped). - Signal
- An external input to a running workflow — a manager approving a step, a courier accepting a delivery, a webhook arriving. Signals are how long-running workflows wait on humans or external systems without busy-polling.
- Compensation
- The reverse of an action. A workflow that holds Trantor resources, captures Payments, and then fails on a downstream integration will run compensation steps in reverse order — release the Trantor hold, void the Payments authorization — rather than leaving partial state.
- Needs ledger
- A persisted ledger of deferred obligations. A
Needgates a flow until its condition clears — a booking deposit-to-confirm, an Elliot delivery payment-to-dispatch, a manager's approval-to-proceed. TheNeedLedgerrecords each obligation; a universal emit-at-time timer expires stale needs; and when a flow is abandoned theNeedCompensatorunwinds the satisfied needs in reverse (saga-style). It is the durable backbone behind "don't dispatch until paid" and "don't confirm until the deposit lands," and it surfaces in a Demerzel operator UI as approvals, pending payments, and outstanding obligations. - Capability bindings (event→outcome)
- Not everything needs a full DAG. A
CapabilityBindingwires a single inbound event directly to an outcome — a capability call from the platformCapabilityRegistry, a Daneel DAG, or an SQS publish — so an operator can express "when this happens, do that" without authoring a workflow. Each firing is recorded as aCapabilityBindingExecutionfor inspection. - Booking orchestration (staged flows)
- Multi-stage booking journeys — book a consult, wait, book the procedure, branch on the result — are composed as a Daneel DAG rather than a bespoke Seldon entity. A
seldon.book_offerstep handler books an Offer from inside a workflow (resolving offer / availability / anchor / party from config or payload); on a Seldon problem it returns{status: 'unavailable'}instead of throwing, so a downstream branch can run a compensation path. Aseldon.cancel_bookinghandler is its lenient, idempotent saga rollback. Seldon stays single-booking; Daneel's wait and branch nodes own the gaps and conditional stages. - StationRoutingRule
- A CEL predicate per kitchen station. When an OrderLine fires, Daneel evaluates rules in priority order and routes the line to the first matching station. Rules live in Daneel; stations live in Trantor as DiscreteResources.
- CoursingProfile
- Per-Order pacing rules — fire mains when appetizers are served, hold desserts until the table calls for them. Drives the CoursingWorkflow that runs alongside dine-in orders.
API surface
Endpoints are versioned under /daneel/v1/. Most execution starts via subscribed events (Seldon emissions, Hober line transitions, Payments webhooks); the HTTP surface is for workflow management, signal injection, and execution inspection.
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /daneel/v1/actions | Create or list Actions (trigger + DAG). |
| POST | /daneel/v1/actions/{id}/run | Run an Action now with an input payload. |
| GET | /daneel/v1/action-runs/{id} | Fetch a run with its step results. GET /daneel/v1/action-runs lists them. |
| POST | /daneel/v1/action-runs/{id}/signal | Send a named signal to a waiting run. |
| POST / GET | /daneel/v1/station-routing-rules | Manage CEL routing rules for kitchen stations, ordered by priority. |
| POST / GET | /daneel/v1/coursing-profiles | Manage per-Order pacing profiles. |
| GET | /daneel/v1/needs | Needs ledger; …/{id}/resolve or …/{id}/waive clears one. |
| GET | /daneel/v1/compensations | Compensation runs; …/{id}/retry retries a failed one. |
| POST / GET | /daneel/v1/capability-bindings | Event→outcome bindings and their …/executions. |
Example: a reminder workflow
An Action that emails the customer when a booking's 24-hour reminder fires:
POST /daneel/v1/actions
{
"name": "booking-reminder-24h",
"trigger": {
"type": "event",
"eventClass": "booking.reminder_24h"
},
"steps": [
{
"type": "notify",
"config": {
"channel": "email",
"personIdPath": "customerPersonId",
"scope": "transactional.booking",
"subject": "See you tomorrow",
"body": "Your booking starts at {startsAt}."
}
}
]
}
→ 201 Action act_01JB… dagVersion=1
How it fits with the rest
flowchart LR
OB[(Audit outbox)] --> D(Daneel)
D -- actions --> Sp[Speaker]
D -- book / cancel --> Se[Seldon]
D -- KDS routing --> Ho[Hober]
Daneel subscribes to events from every primitive and dispatches actions back into them. Seldon lifecycle emissions trigger reminder and cleanup workflows, and seldon.book_offer books from inside one. Hober line transitions drive kitchen routing. Payments webhook events fan into automation workflows. Speaker is the dispatch target for any workflow that needs to send a message. Workflow definitions live on the Daneel Action itself, versioned and pinned per run.