Build Your First App
You will scaffold a small appointment-booking app, replay a golden
fixture, watch jacqos verify go green, then deliberately break a
named invariant and watch it go red. Twenty minutes, no Rust, no
Cargo, no compilation step.
The app you build is the Appointment
Booking example, slimmed down
to its first invariant. Every code block on this page is lifted
verbatim from examples/jacqos-appointment-booking/ so you can paste
freely.
Step 1: Install And Scaffold
Section titled “Step 1: Install And Scaffold”JacqOS ships as a single binary. There is no toolchain to install.
curl -fsSL https://www.jacqos.io/install.sh | shjacqos scaffold my-booking-appcd my-booking-appIf jacqos is not on your PATH after install, add the directory printed by
the installer and open a new terminal.
jacqos scaffold produces a directory of plain text files — a
jacqos.toml, an ontology/ directory of .dh rules, a
mappings/ directory of Rhai mappers, and a fixtures/ directory
with a starter golden fixture. There is no Cargo.toml, no src/,
no build configuration. The jacqos binary interprets every file
directly.
Step 2: Declare The Domain
Section titled “Step 2: Declare The Domain”Open ontology/schema.dh and replace the scaffolded relations with
the ones the booking domain needs. A relation is a typed table:
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_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 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)The intent. prefix is what tells the platform these are
external-action requests, not derived facts. The capability gate in
jacqos.toml decides which effect each intent dispatches to.
Step 3: Map Observations Into Atoms
Section titled “Step 3: Map Observations Into Atoms”Open mappings/inbound.rhai. The mapper is the structural boundary
between raw observations (HTTP webhooks, queue messages, file
deliveries) and the typed atoms the ontology consumes:
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" { return [ atom("reservation.result", body.result), atom("reservation.request_id", body.request_id), atom("reservation.slot_id", body.slot_id), ]; }
if obs.kind == "confirmation.result" { return [ atom("confirmation.result", body.result), atom("confirmation.request_id", body.request_id), ]; }
[]}Mappers are pure and capability-free — they cannot fetch URLs, read files, or call models. That keeps replay deterministic.
Step 4: Derive The Booking Lifecycle
Section titled “Step 4: Derive The Booking Lifecycle”Open ontology/rules.dh and write the lifecycle rules. Each rule
is a derivation: when its body holds, the head fact is produced.
assert flags the head as a tracked outcome; retract removes a
previously asserted fact when a contradicting observation arrives.
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").
rule slot_available(slot) :- slot_listed(slot), not slot_hold_active(_, slot), not booking_confirmed(_, slot).
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 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).
rule booking_status(req, "confirmed") :- booking_confirmed(req, _).Step 5: Add The Invariant
Section titled “Step 5: Add The Invariant”The whole point of a booking system is that a slot can only be held
by one request and confirmed for one patient. Add the named invariants
at the bottom of rules.dh:
invariant no_double_hold(slot) :- count slot_hold_active(_, slot) <= 1.
invariant no_double_booking(slot) :- count booking_confirmed(_, slot) <= 1.invariant is the declarative check the evaluator runs after every
fixed point. If a derived state violates it, the transition is
rejected and the violation is named in the diagnostic.
Step 6: Derive The Intents
Section titled “Step 6: Derive The Intents”Open ontology/intents.dh. Intents are how the ontology requests
external action. They are derived facts whose intent. prefix tells
the runtime to dispatch them through the capability declared in
jacqos.toml.
rule intent.reserve_slot(req, slot) :- booking_request(req, _, slot), slot_available(slot), not slot_hold_active(req, slot).
rule intent.send_confirmation(req, email, slot) :- confirmation_pending(req, email, slot), not confirmation_sent(req).Step 7: Write The Fixture
Section titled “Step 7: Write The Fixture”Open fixtures/happy-path.jsonl. A golden fixture is a JSONL file
of observations the evaluator should replay deterministically:
{"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"}}And the expected world state in fixtures/happy-path.expected.json.
This is the spec the evaluator output is compared against:
{ "facts": [ { "relation": "booking_confirmed", "value": ["req-1", "slot-42"] }, { "relation": "booking_request", "value": ["req-1", "pat@example.com", "slot-42"] }, { "relation": "booking_status", "value": ["req-1", "confirmed"] }, { "relation": "confirmation_sent", "value": ["req-1"] }, { "relation": "slot_hold_active", "value": ["req-1", "slot-42"] }, { "relation": "slot_listed", "value": ["slot-42"] } ], "contradictions": [ { "relation": "confirmation_pending", "value": ["req-1", "pat@example.com", "slot-42"] } ]}The confirmation_pending entry shows up in contradictions
because it was asserted and then retracted by the time the
evaluator reached fixed point. Provenance preserves both edges.
Step 8: Run The Green Loop
Section titled “Step 8: Run The Green Loop”In one terminal start the dev shell. It watches every file you just edited and hot-reloads on save in under 250 ms:
jacqos devIn a second terminal, replay the fixture and verify:
jacqos replay fixtures/happy-path.jsonljacqos verifyjacqos verify replays every fixture from a clean database, checks
the result against *.expected.json, and runs every named invariant
to a fixed point. Your output should look like:
Replaying fixtures... happy-path.jsonl PASS (4 observations, 6 facts matched)
Checking invariants... no_double_hold PASS no_double_booking PASS
All checks passed.This is the green loop. You have a verified app.
Step 9: Break An Invariant On Purpose
Section titled “Step 9: Break An Invariant On Purpose”Verified once, easy. Verified after a deliberate breakage is the
loop you’ll actually live in. Add a second observation to the
fixture that lets a second patient confirm the same slot, by
pasting these lines at the bottom of fixtures/happy-path.jsonl:
{"kind":"booking.request","payload":{"request_id":"req-2","email":"sam@example.com","slot_id":"slot-42"}}{"kind":"reservation.result","payload":{"result":"succeeded","request_id":"req-2","slot_id":"slot-42"}}{"kind":"confirmation.result","payload":{"result":"sent","request_id":"req-2"}}Re-run:
jacqos verifyThe output now names the invariant violation:
Replaying fixtures... happy-path.jsonl FAIL
Invariant violated: no_double_booking(slot-42) count booking_confirmed(_, "slot-42") = 2 (limit 1)
Provenance: booking_confirmed("req-1", "slot-42") <- rules.dh:43 booking_confirmed("req-2", "slot-42") <- rules.dh:43Two booking_confirmed rows for the same slot violates
no_double_booking. The evaluator refuses the transition and the
fixture fails. This is the safety boundary doing its job.
The right fix is not to silence the invariant. Instead, model the
real-world behaviour: the clinic API rejects a second reservation
when a hold already exists. Replace the second reservation.result
line in the fixture with a failure outcome ("result":"failed",
plus a "reason":"slot already held") — exactly what the bundled
double-booking-path fixture
encodes. Verify goes green again, and the invariant is now backed
by a fixture that proves the clinic-level guard.
If you instead introduced a typo into a rule body — say, mistyped
atom(obs, "booking.slot_id", slot) as atom(obs, "booking.slotid", slot) —
the loader would refuse the program with a structural error such as
E1029: expected atom predicate string or E2004: relation '<name>' is not declared, depending on the typo. Diagnostics are
emitted with the stable
EXYYZZ codes the validator
publishes; you can grep for the code in the reference page.
Step 10: Inspect In Studio
Section titled “Step 10: Inspect In Studio”While the dev shell is running, open Studio:
jacqos studioThe Activity timeline populates live. Click any row to drill from the executed effect back to the derived intent, the supporting facts, the atoms the mapper produced, and the original observation in the fixture. This is the Visual Provenance contract — every derived row traces back to specific evidence.
What Just Happened
Section titled “What Just Happened”In one short session you exercised the full observation-first pipeline:
- Raw events became observations in
fixtures/happy-path.jsonl. - The Rhai mapper deterministically projected each observation into typed atoms.
- Stratified Datalog rules in
ontology/rules.dhderived the booking-lifecycle facts with full provenance. - The named invariants
no_double_holdandno_double_bookingacted asCHECKconstraints over the derived view — they passed on the happy path and refused the deliberate breakage. - The
intent.reserve_slotandintent.send_confirmationrules produced intents, ready to fan out to the capabilities declared injacqos.toml. jacqos verifyproved the whole loop end-to-end against a committed expected world state. The result is byte-identical on every run.
You never wrote orchestration code. You never managed state. You declared what the world looks like and the evaluator did the rest.
What To Read Next
Section titled “What To Read Next”You are at rung 5 of the reader ladder. The natural next step depends on where you are heading.
Ship a real-world pattern
Section titled “Ship a real-world pattern”The booking app uses neither LLM proposals nor fallible sensors — it is the smallest scaffold that exercises invariants. To layer in either containment pattern, work through:
- Now Wire In A Containment Pattern
— the rung-6 walkthrough. Adds an
proposal.*decider to the app you just built and shows how the relay boundary is enforced at load time. - LLM Decision Containment — the underlying pattern in depth.
- Fallible Sensor Containment — the other half of the safety story.
Adapt a flagship example
Section titled “Adapt a flagship example”Every flagship example is a finished app you can fork:
- Appointment Booking — the full version of what you just built, with cancellation, an assert/retract contradiction path, and a double-booking error fixture.
- Chevy Offer Containment — the LLM-decision-containment flagship.
- Drive-Thru Ordering — the fallible-sensor flagship.
Understand why this composes
Section titled “Understand why this composes”Optional, but the mental model is worth knowing if you plan to ship more than a toy:
- Observation-First Thinking — why the pipeline is the way it is.
- Invariants and Satisfiability — what the evaluator actually proves when verify goes green.
- Golden Fixtures — the digest-backed contract you just produced.