Appointment Booking Walkthrough
What You’ll Build
Section titled “What You’ll Build”A booking system where patients request appointment slots, the platform reserves them via an external clinic API, and sends confirmation emails. The system enforces that no slot is double-booked and every request reaches exactly one terminal state.
This walkthrough covers the full JacqOS pipeline:
- Observations arrive as JSON events (slot listed, booking requested, reservation result, confirmation result)
- Mappers extract semantic atoms from each observation
- Rules derive facts like
slot_available,booking_confirmed, andbooking_status - Invariants enforce that no slot has two holds or two confirmed bookings
- Intents derive outbound actions (reserve a slot, send a confirmation)
- Fixtures prove the system handles happy paths, cancellations, and double-booking races
Project Structure
Section titled “Project Structure”jacqos-appointment-booking/ jacqos.toml ontology/ schema.dh # 14 relations rules.dh # Lifecycle rules + 3 invariants intents.dh # Intent derivation mappings/ inbound.rhai # 5 observation kinds -> atoms fixtures/ happy-path.jsonl # Successful booking contradiction-path.jsonl # Cancellation mid-flow double-booking-path.jsonl # Race condition schemas/ booking-intake.json prompts/ triage-system.mdStep 1: Configure the App
Section titled “Step 1: Configure the App”jacqos.toml declares the app identity, file paths, and effect capabilities:
app_id = "jacqos-appointment-booking"app_version = "0.1.0"
[paths]ontology = ["ontology/*.dh"]mappings = ["mappings/*.rhai"]fixtures = ["fixtures/*.jsonl"]
[capabilities]http_clients = ["clinic_api", "notify_api"]timers = true
[capabilities.intents]"intent.reserve_slot" = { capability = "http.fetch", resource = "clinic_api" }"intent.send_confirmation" = { capability = "http.fetch", resource = "notify_api" }Every outbound action is a declared capability. The evaluator rejects any intent that references an undeclared resource at load time.
Step 2: Declare Relations
Section titled “Step 2: Declare Relations”ontology/schema.dh declares every relation the system can derive. This is the full schema of what the booking system knows about:
relation booking_request(request_id: text, patient_email: text, slot_id: text)relation slot_listed(slot_id: text)relation slot_available(slot_id: text)relation slot_hold_active(request_id: text, slot_id: text)
relation booking_rejected(request_id: text, slot_id: text, reason: text)relation booking_cancelled(request_id: text, slot_id: text)relation booking_confirmed(request_id: text, slot_id: text)
relation confirmation_pending(request_id: text, patient_email: text, slot_id: text)relation confirmation_sent(request_id: text)relation confirmation_failed(request_id: text, reason: text)
relation booking_terminal(request_id: text)relation booking_status(request_id: text, status: text)
relation intent.reserve_slot(request_id: text, slot_id: text)relation intent.send_confirmation(request_id: text, patient_email: text, slot_id: text)Relations prefixed with intent. derive outbound actions. Everything else is internal derived state.
Step 3: Map Observations to Atoms
Section titled “Step 3: Map Observations to Atoms”mappings/inbound.rhai translates raw JSON observations into typed atoms. Each observation kind produces a specific set of atoms:
fn map_observation(obs) { let body = parse_json(obs.payload);
if obs.kind == "slot.status" { return [ atom("slot.id", body.slot_id), atom("slot.state", body.state), ]; }
if obs.kind == "booking.request" { return [ atom("booking.request_id", body.request_id), atom("booking.email", body.email), atom("booking.slot_id", body.slot_id), ]; }
if obs.kind == "reservation.result" { let atoms = [ atom("reservation.result", body.result), atom("reservation.request_id", body.request_id), atom("reservation.slot_id", body.slot_id), ];
if body.contains("reason") { atoms.push(atom("reservation.reason", body.reason)); }
return atoms; }
if obs.kind == "booking.cancelled" { return [ atom("booking.cancelled.request_id", body.request_id), atom("booking.cancelled.slot_id", body.slot_id), ]; }
if obs.kind == "confirmation.result" { let atoms = [ atom("confirmation.result", body.result), atom("confirmation.request_id", body.request_id), ];
if body.contains("reason") { atoms.push(atom("confirmation.reason", body.reason)); }
return atoms; }
[]}The mapper is a pure function. It has no side effects, no network access, and no access to previously derived state. It sees one observation at a time and returns atoms.
Step 4: Write Derivation Rules
Section titled “Step 4: Write Derivation Rules”ontology/rules.dh is where the booking logic lives. Rules derive higher-level facts from atoms and other facts.
Base Facts from Atoms
Section titled “Base Facts from Atoms”The first rules project atoms into typed relations:
rule booking_request(req, email, slot) :- atom(obs, "booking.request_id", req), atom(obs, "booking.email", email), atom(obs, "booking.slot_id", slot).
rule slot_listed(slot) :- atom(obs, "slot.id", slot), atom(obs, "slot.state", "listed").atom() is the built-in base relation that bridges observations into the logic layer. Every derived fact traces back to specific atoms, and every atom traces back to a specific observation.
Derived State
Section titled “Derived State”Slot availability is derived, not stored. A slot is available when it is listed, has no active hold, and is not already confirmed:
rule slot_available(slot) :- slot_listed(slot), not slot_hold_active(_, slot), not booking_confirmed(_, slot).Reservation results from the clinic API produce assertions and retractions:
rule assert slot_hold_active(req, slot) :- atom(obs, "reservation.result", "succeeded"), atom(obs, "reservation.request_id", req), atom(obs, "reservation.slot_id", slot).
rule assert booking_rejected(req, slot, reason) :- atom(obs, "reservation.result", "failed"), atom(obs, "reservation.request_id", req), atom(obs, "reservation.slot_id", slot), atom(obs, "reservation.reason", reason).Cancellation retracts the hold, making the slot available again:
rule assert booking_cancelled(req, slot) :- atom(obs, "booking.cancelled.request_id", req), atom(obs, "booking.cancelled.slot_id", slot).
rule retract slot_hold_active(req, slot) :- booking_cancelled(req, slot).Confirmation Lifecycle
Section titled “Confirmation Lifecycle”Confirmation is pending when a request has an active hold. It gets retracted when the confirmation succeeds or fails:
rule assert confirmation_pending(req, email, slot) :- booking_request(req, email, slot), slot_hold_active(req, slot).
rule assert confirmation_sent(req) :- atom(obs, "confirmation.result", "sent"), atom(obs, "confirmation.request_id", req).
rule retract confirmation_pending(req, email, slot) :- booking_request(req, email, slot), slot_hold_active(req, slot), confirmation_sent(req).
rule assert booking_confirmed(req, slot) :- confirmation_sent(req), slot_hold_active(req, slot).Terminal State and Status
Section titled “Terminal State and Status”Every request eventually reaches a terminal state. The booking_status relation computes the current lifecycle position:
rule booking_terminal(req) :- booking_confirmed(req, _).rule booking_terminal(req) :- booking_rejected(req, _, _).rule booking_terminal(req) :- booking_cancelled(req, _).
rule booking_status(req, "requested") :- booking_request(req, _, _), not slot_hold_active(req, _), not booking_terminal(req).
rule booking_status(req, "reserved") :- slot_hold_active(req, _), not confirmation_sent(req), not confirmation_failed(req, _), not booking_cancelled(req, _).
rule booking_status(req, "confirmed") :- booking_confirmed(req, _).
rule booking_status(req, "rejected") :- booking_rejected(req, _, _).
rule booking_status(req, "cancelled") :- booking_cancelled(req, _).Invariants
Section titled “Invariants”Three invariants enforce structural correctness across all evaluation states:
invariant no_double_hold(slot) :- count slot_hold_active(_, slot) <= 1.
invariant no_double_booking(slot) :- count booking_confirmed(_, slot) <= 1.
invariant one_terminal_outcome(req) :- count booking_terminal(req) <= 1.These are not tests. They are constraints checked after every fixed point. If any invariant is violated, the evaluator halts.
Step 5: Derive Intents
Section titled “Step 5: Derive Intents”ontology/intents.dh derives outbound actions from stable state. Intents only fire when preconditions are met:
rule intent.reserve_slot(req, slot) :- booking_request(req, _, slot), slot_available(slot), not slot_hold_active(req, slot), not booking_terminal(req).
rule intent.send_confirmation(req, email, slot) :- confirmation_pending(req, email, slot), not confirmation_sent(req), not confirmation_failed(req, _), not booking_cancelled(req, _).intent.reserve_slot fires when a booking request exists for an available slot that doesn’t already have a hold. intent.send_confirmation fires when confirmation is pending and hasn’t been sent or cancelled. The shell executes these intents against the declared capabilities in jacqos.toml, and the results come back as new observations.
Step 6: Write Golden Fixtures
Section titled “Step 6: Write Golden Fixtures”Fixtures define deterministic scenarios and their expected world state. Each fixture is a JSONL file where every line is an observation.
Happy Path
Section titled “Happy Path”A patient requests a slot, the clinic reserves it, and the confirmation email is sent:
{"kind":"slot.status","payload":{"slot_id":"slot-42","state":"listed"}}{"kind":"booking.request","payload":{"request_id":"req-1","email":"pat@example.com","slot_id":"slot-42"}}{"kind":"reservation.result","payload":{"result":"succeeded","request_id":"req-1","slot_id":"slot-42"}}{"kind":"confirmation.result","payload":{"result":"sent","request_id":"req-1"}}The expected world state after replay includes booking_confirmed("req-1", "slot-42"), booking_status("req-1", "confirmed"), and booking_terminal("req-1"). The confirmation_pending fact appears in contradictions because it was asserted when the hold was placed and retracted when the confirmation was sent.
Cancellation Path
Section titled “Cancellation Path”A patient cancels after the reservation succeeds but before confirmation:
{"kind":"slot.status","payload":{"slot_id":"slot-42","state":"listed"}}{"kind":"booking.request","payload":{"request_id":"req-2","email":"cancelled@example.com","slot_id":"slot-42"}}{"kind":"reservation.result","payload":{"result":"succeeded","request_id":"req-2","slot_id":"slot-42"}}{"kind":"booking.cancelled","payload":{"request_id":"req-2","slot_id":"slot-42"}}After replay, booking_cancelled("req-2", "slot-42") is asserted and slot_hold_active is retracted — the slot becomes available again. The booking_status is "cancelled" and the request is terminal.
Double-Booking Race
Section titled “Double-Booking Race”Two patients request the same slot. The first reservation succeeds; the second is rejected:
{"kind":"slot.status","payload":{"slot_id":"slot-42","state":"listed"}}{"kind":"booking.request","payload":{"request_id":"req-1","email":"alice@example.com","slot_id":"slot-42"}}{"kind":"booking.request","payload":{"request_id":"req-2","email":"bob@example.com","slot_id":"slot-42"}}{"kind":"reservation.result","payload":{"result":"succeeded","request_id":"req-1","slot_id":"slot-42"}}{"kind":"reservation.result","payload":{"result":"failed","request_id":"req-2","slot_id":"slot-42","reason":"slot already held"}}After replay, req-1 has status "reserved" with an active hold and pending confirmation. req-2 is "rejected" and terminal. The no_double_hold and no_double_booking invariants hold because only one request has an active hold.
Step 7: Verify
Section titled “Step 7: Verify”Run all fixtures and invariants:
$ jacqos verifyReplaying fixtures... happy-path.jsonl PASS (4 observations, 7 facts) contradiction-path.jsonl PASS (4 observations, 7 facts) double-booking-path.jsonl PASS (5 observations, 8 facts)
Checking invariants... no_double_hold PASS no_double_booking PASS one_terminal_outcome PASS
All checks passed.Every fixture replays from scratch on a clean database. The output is deterministic — same observations, same evaluator, same facts every time.
Step 8: Run the Full Integration Gate
Section titled “Step 8: Run the Full Integration Gate”Checkpoint B for this example exercises the full public surface on a clean database: replay, verify, lineage fork, Studio comparison, exports, and stats.
# Start cleanrm -rf .jacqos
# Replay the main lineagejacqos replay fixtures/happy-path.jsonljacqos replay fixtures/contradiction-path.jsonljacqos verify
# Fork a child lineage from the committed parent headjacqos lineage fork
# Example: replay the rejected second booking into the child branch onlyjacqos replay --lineage <CHILD_LINEAGE_ID> fixtures/double-booking-path.jsonl
# Open Studio on the child; the Compare lens chip pins the parent as the comparison evaluatorjacqos studio --lineage <CHILD_LINEAGE_ID>
# Export proof artifacts and graph viewsjacqos export verification-bundlejacqos export graph-bundle
# Inspect storage and namespace-reduct partitionsjacqos statsWhat you should see:
jacqos verifyreports the shadow evaluator conformance summary and the rule-shape lint summary.jacqos studio --lineage <CHILD_LINEAGE_ID>opens Studio on the child worldview. Provenance for the rejected attempt is available in the drill inspector and timeline. The Compare lens chip pins the parent-vs-child comparison evaluator (full dual-pane render in V1.1). The Ontology destination groups relations by stratum and prefix; per-relation rule-shape and namespace-partition visuals ship with the V1.1 rule-graph surface — until then the same summaries live ingenerated/verification/.jacqos export verification-bundleandjacqos export graph-bundlewrite fresh artifacts undergenerated/.jacqos statsreports namespace-reduct partitions in JSON.- The parent lineage remains unchanged after the child replay. There is no merge-back path.
How the Pipeline Flows
Section titled “How the Pipeline Flows”Here is the complete flow for the happy path:
Observation: slot.status {slot_id: "slot-42", state: "listed"} -> Atoms: slot.id="slot-42", slot.state="listed" -> Fact: slot_listed("slot-42") -> Fact: slot_available("slot-42")
Observation: booking.request {request_id: "req-1", email: "pat@example.com", slot_id: "slot-42"} -> Atoms: booking.request_id="req-1", booking.email="pat@example.com", booking.slot_id="slot-42" -> Fact: booking_request("req-1", "pat@example.com", "slot-42") -> Intent: intent.reserve_slot("req-1", "slot-42") [slot is available, no hold yet]
Observation: reservation.result {result: "succeeded", request_id: "req-1", slot_id: "slot-42"} -> Atoms: reservation.result="succeeded", reservation.request_id="req-1", reservation.slot_id="slot-42" -> Assert: slot_hold_active("req-1", "slot-42") -> Retract: slot_available("slot-42") [slot now has a hold] -> Assert: confirmation_pending("req-1", "pat@example.com", "slot-42") -> Intent: intent.send_confirmation("req-1", "pat@example.com", "slot-42")
Observation: confirmation.result {result: "sent", request_id: "req-1"} -> Atoms: confirmation.result="sent", confirmation.request_id="req-1" -> Assert: confirmation_sent("req-1") -> Assert: booking_confirmed("req-1", "slot-42") -> Retract: confirmation_pending("req-1", "pat@example.com", "slot-42") -> Fact: booking_terminal("req-1") -> Fact: booking_status("req-1", "confirmed")Every fact traces back to specific observations. Open Studio to follow any provenance edge visually.
Key Patterns
Section titled “Key Patterns”Assert and retract model state changes explicitly. When a cancellation arrives, slot_hold_active is retracted and appears in contradictions. This is not an error — it is the system recording that a fact was believed and then revoked.
Intents derive from stable state, not events. intent.reserve_slot fires because the world state says a slot is available and a request exists, not because a “booking requested” event was received. If the slot becomes unavailable before the intent executes, it won’t re-derive.
Invariants are universal, fixtures are specific. The no_double_booking invariant holds across all evaluation states. The happy-path fixture proves one specific scenario produces the right output. You need both.
Next Steps
Section titled “Next Steps”- Observation-First Model — the mental model behind this architecture
- Golden Fixtures — how fixture verification works in detail
- Invariant Review — how invariants replace code review
- Visual Provenance — tracing facts back to observations in Studio
- Crash Recovery — how effect reconciliation works after crashes
- jacqos.toml Reference — configuration format used by this example
- CLI Reference — every CLI command