Skip to content

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.

JacqOS ships as a single binary. There is no toolchain to install.

Terminal window
curl -fsSL https://www.jacqos.io/install.sh | sh
jacqos scaffold my-booking-app
cd my-booking-app

If 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.

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.

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.

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, _).

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.

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).

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.

In one terminal start the dev shell. It watches every file you just edited and hot-reloads on save in under 250 ms:

Terminal window
jacqos dev

In a second terminal, replay the fixture and verify:

Terminal window
jacqos replay fixtures/happy-path.jsonl
jacqos verify

jacqos 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.

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:

Terminal window
jacqos verify

The 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:43

Two 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.

While the dev shell is running, open Studio:

Terminal window
jacqos studio

The 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.

In one short session you exercised the full observation-first pipeline:

  1. Raw events became observations in fixtures/happy-path.jsonl.
  2. The Rhai mapper deterministically projected each observation into typed atoms.
  3. Stratified Datalog rules in ontology/rules.dh derived the booking-lifecycle facts with full provenance.
  4. The named invariants no_double_hold and no_double_booking acted as CHECK constraints over the derived view — they passed on the happy path and refused the deliberate breakage.
  5. The intent.reserve_slot and intent.send_confirmation rules produced intents, ready to fan out to the capabilities declared in jacqos.toml.
  6. jacqos verify proved 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.

You are at rung 5 of the reader ladder. The natural next step depends on where you are heading.

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:

Every flagship example is a finished app you can fork:

Optional, but the mental model is worth knowing if you plan to ship more than a toy: