Gleamalog by Example: The Mortgage Casebook
An addendum to PLRN-019. The semantics are argued there. Here they are exercised, one query at a time, against a real corpus, in the Wunderlog surface a caller actually writes and the replies a caller actually receives.
The corpus
The examples run against a real-world use case, a mortgage audit trail:
- 1,044 audit records across 200 multi-hop mortgage journeys,
- nine agents,
- one JSON object per record,
- chained per journey by hash.
A journey is a customer request that fans out across a customer agent, an authentication agent, document processing, KYC, credit, fraud detection, a compliance crew, and core banking, with each hop leaving a signed record.
Each record is a set of Pentads. The “schema”, that is, the predicates the queries use are these:
:taskfor the journey identifier,:seqfor the per-journey position,:prevfor the previous record’s hash,:fromand:tofor the source and destination,:methodfor the A2A verb,:decisionfor allow or block,:outcomefor success or error or blocked,:riskfor the chain-of-thought risk level where one was recorded,:flagfor each risk flag,:costfor the hop cost,:latencyfor its milliseconds, and:atfor its timestamp.
Records and journeys appear as concepts over their leading hash bytes,
:rec_7b65b341 and :j_15f18021.
The surface and the reply
A fact is a bare parenthesized Pentad. (:rec_7b65b341 :decision :block) states
that a record was blocked, and the same form with a variable, (?r :decision
:block), reads every record that was. A rule derives a named relation from a
body of juxtaposed goals under ⇒, so blocked_at(?a ?r) ⇒ (?r :decision
:block) (?r :from ?a) names the pairing of a blocked record with its agent. A
write is a Pentad under an operation glyph: ⇑ asserts, ⇓ consumes, ⊢ reads
without consuming, ⇶ infers transitively.
A reply is bare newline-delimited Wunderlog, not JSON. The answer to a query is
the derived assertions themselves, one per line, and nothing wraps them. The
result kind is chosen by a |>> modifier: bindings is the default, |>> pentad
returns constructed Pentads, |>> proof returns a derivation. A parse or type
failure comes back under ↯, so ↯(:syntax, :parse_failure) is an error, not
an answer.
Several queries may be multiplexed into one line-oriented session frame, WZ::εσ
… ETX, terminated by EOT, in the transmission-and-control lineage the surface
takes its cues from, the CCITT Red Book. Concision precedes us.
The tier codes scope a frame to a subset of memory: ε episodic, σ semantic,
κ compliance, among others. Within a frame a squirt is a labelled block,
audit { κ |>> proof … }, carrying its own tier scope and result kind.
Answers are the values the documented semantics produce. Rational reductions are in the fixed-point arithmetic PLRN-019 §4 commits to, scaled by a thousand and truncated toward zero, so a spend is reported in whole milli-dollars rather than in a float that would depend on summation order.
Querying the trail
Selection and join
What were all the hops that were blocked and what agent emitted them? A blocked hop is the system refusing an action mid-application, a control gate firing before something like a credit pull or a fund transfer can go through.
blocked_at(?agent ?r) ⇒ (?r :decision :block) (?r :from ?agent)
blocked_at(?agent ?r)
blocked_at(:Customer_Agent :rec_7b65b341)
blocked_at(:Customer_Agent :rec_65f9e01b)
blocked_at(:Document_Processing_Agent :rec_ee65e066)
…
Thirty-three fired across the corpus, and where they fired is a compliance officer’s first question: 20 at the Customer Agent, the front door, then 8 at Document Processing, 3 at Credit, and one each at Fraud Detection and a Research Assistant. The query pairs each refusal with the agent that attempted it.
Recursion over the hash chain
Deny a mortgage and the law requires you to say precisely why; a regulator or the borrower’s attorney will ask to see the trail that led to no. That trail is the hash chain, and this query walks it backward from each refusal.
The :prev link makes each journey a linked list, and the transitive closure of
that link is the reachability relation over the audit trail. It’s a clear
example of a textbook recursive Datalog program, run against a real chain of
custody. First the closure, then the records upstream of a block, the chain of
custody that preceded each refusal.
leads_to(?a ?b) ⇒ (?b :prev ?a)
leads_to(?a ?c) ⇒ leads_to(?a ?b) leads_to(?b ?c)
before_block(?a ?b) ⇒ leads_to(?a ?b) (?b :decision :block)
before_block(?a ?b)
before_block(:rec_cf490971 :rec_7b65b341)
before_block(:rec_172da3ca :rec_7b65b341)
…
Block :rec_7b65b341 was preceded by its immediate predecessor :rec_cf490971
and, before that, the application’s opening hop :rec_172da3ca; fifty-six such
upstream records stand behind the 33 blocks. Because the rule is recursive, the
walk is as long as the chain, and no hop-by-hop traversal was written by hand.
The leads_to relation it builds on has 3,108 tuples across the 200 journeys.
Stratified negation
Which are the clean journeys, that is, journeys that completed without a single blocked hop. The clean journeys are the applications that went straight through with no gate ever firing, 173 of the 200. That is the straight-through rate that a lending operation’s operating margins live on, and the 27 that hit a block are precisely the exceptions a human has to review.
Negation requires the blocked-journey relation to be complete before the clean-journey relation may read its absence, which is exactly the stratum boundary a negative edge forces.
dirty(?t) ⇒ (?r :task ?t) (?r :decision :block)
clean(?t) ⇒ (?r :task ?t) ¬ dirty(?t)
clean(?t)
clean(:j_b7c25a30)
…
Absence here is a firm no: with no block on file an application is definitely clean, not merely unproven. The genuinely-unknown case, a fact that has not yet landed, belongs to time and to PLRN-018.
Count, grouped
Let’s count hops per application, which is how much work each one took, which tracks how long the borrower waited and what it cost to process.
hops(?t ?n) ⇒ (?r :task ?t) ?n ≔ #(?r) ▷ ?t
hops(?t ?n)
hops(:j_15f18021 12)
hops(:j_b7c25a30 11)
…
The corpus runs from a one-hop application to a twelve-hop one, median between
four and five; the long tail is where delay and expense concentrate. (#
counts, ▷ groups, ≔ binds.)
Sum, in fixed-point
What is the spend per agent over the hops that carried a cost? This is the AI bill to originate a loan, agent by agent. All 200 applications cost $5.39 in model calls, and the Customer Agent alone is $3.51 of it.
spend(?a ?m) ⇒ (?r :from ?a) (?r :cost ?c) ?m ≔ Σ(?c) ▷ ?a
spend(?a ?m)
spend(:Customer_Agent 3511)
spend(:Document_Processing_Agent 777)
spend(:Credit_Agent 531)
spend(:Fraud_Detection_Agent 400)
spend(:Research_Assistant 167)
The figures are whole milli-dollars because you are adding up money: the sum is computed in fixed point rather than floating point, so the ledger reconciles to the same total every run instead of drifting with the order the hops were added. The four agents that never call a model do not appear, having no cost to sum.
The aggregate filter
This is the security team’s watchlist. Which agents keep tripping the risk detector? Count the high-risk hops per agent and keep those with at least five.
hot(?a ?n) ⇒ (?r :from ?a) (?r :risk :high) ?n ≔ #(?r) ▷ ?a ?n ≥ 5
hot(?a ?n)
hot(:Customer_Agent 29)
hot(:Document_Processing_Agent 6)
hot(:Credit_Agent 5)
The Customer Agent lit up 29 times, far ahead of Document Processing and Credit,
which fits: the customer-facing edge is where prompt-injection and
data-exfiltration attempts arrive first. The ?n ≥ 5 keeps only the repeat
offenders.
Meet and join
What is the worst-case latency per agent? The slowest hop each agent ever ran is the tail that blows an SLA and leaves a borrower staring at a spinner.
worst_ms(?a ?ms) ⇒ (?r :from ?a) (?r :latency ?l) ?ms ≔ ⊔(?l) ▷ ?a
worst_ms(?a ?ms)
worst_ms(:Fraud_Detection_Agent 803.0)
worst_ms(:Document_Processing_Agent 670.7)
worst_ms(:Customer_Agent 654.7)
…
Fraud Detection’s worst was 803 milliseconds. ⊔ returns an actual member of
the set, the real slowest hop, not an average that would bury it.
A tumbling window
This is the load curve: how many hops landed in each half-second of the run, the shape an operations team watches to size capacity and catch a burst. The corpus from our mortgage agent simulator is a 2.6-second capture, so the natural bucket is sub-second. Hops per 500-millisecond bucket. The window is a pure function of the timestamp: the bucket is the floor of the timestamp over the width, and no clock is consulted.
per_bucket(?bucket ?n) ⇒ (?r :at ?stamp) ?bucket ≔ ⊞⟦500ms⟧(?stamp) ?n ≔ #(?r) ▷ ?bucket
per_bucket(?bucket ?n)
per_bucket(0 901)
per_bucket(1 101)
per_bucket(2 27)
per_bucket(3 12)
per_bucket(4 1)
per_bucket(5 2)
Time is bucketed by a fixed rule with no clock consulted, so the same capture
always buckets the same way. Its now-anchored sibling ◷, and replayable time
in general, are PLRN-018’s subject.
Provenance, as a proof
A regulator does not accept “the model decided”; the adverse-action file has to
name the specific facts behind the decision. This is the receipt. Which records
witness the claim that a journey was blocked? The |>> proof modifier asks not
for the answer but for its derivation, and the leaves of that derivation are the
facts that support it.
blocked(?t) ⇒ (?r :task ?t) (?r :decision :block)
blocked(?t) |>> proof
blocked(:j_15f18021)
(:rec_7b65b341 :task :j_15f18021)
(:rec_7b65b341 :decision :block)
|>> proof returns exactly those: journey :j_15f18021 was blocked, and here
are the two records that make it so. Across the corpus the 27 blocked journeys
trace to exactly the 33 block records, each answer carrying its own evidence.
Multiplexing: one frame, three payloads
One request, three answers, returned together: the block list and the spend on
the ε-episodic and σ-semantic memory tiers, and the audit derivation on the
κ compliance tier. An auditor wanting the whole picture gets it in a single
round trip rather than three. A session frame carries several queries at once,
each a labelled squirt in its own braces with its own tier scope and result
kind, and the reply comes back as the assertions of each in turn.
WZ::εσ
blocks { εσ |>> bindings
blocked_at(?a ?r) ⇒ (?r :decision :block) (?r :from ?a)
blocked_at(?a ?r)
}
spend { σ |>> bindings
spend(?a ?m) ⇒ (?r :from ?a) (?r :cost ?c) ?m ≔ Σ(?c) ▷ ?a
spend(?a ?m)
}
audit { κ |>> proof
blocked(?t) ⇒ (?r :task ?t) (?r :decision :block)
blocked(?t)
}
ETX
EOT
blocked_at(:Customer_Agent :rec_7b65b341)
…
spend(:Customer_Agent 3511)
…
blocked(:j_15f18021)
(:rec_7b65b341 :task :j_15f18021)
(:rec_7b65b341 :decision :block)
…
The frame is the CCITT Red Book discipline applied to a query: open on
WZ::, scope by tier, carry labelled payloads, close on ETX, end the
transmission on EOT.
One shape the corpus does not exercise is the cyclic body. No agent ever calls back into the one that called it, so the pipeline is a tree, not a web, and the query for a mutually-referencing cluster is well-formed but returns nothing. The worst-case-optimal join it would compile to is the four-clique example in PLRN-019 §4.
Connecting to rules with Sajak
Everything so far here reads facts that were asserted. Sajak is the OWL 2 RL reasoner in WunderOS, and here computes entailment closure over the asserted ABox. Connecting it means Gleamalog queries the closed store rather than the raw one, so a query can return facts that were never written down because they follow soundly from facts that were.
The mortgage trail carries no ontology of its own, so the TBox asserted in each
example below, with ⇑, is authored for illustration over the real agent names
and methods. The ABox is the corpus. The RDFS and OWL vocabulary is written in
glyphs, coherent with the lattice the aggregates already use:
⊑subclass,⊴subproperty,∈type,⇄inverse,⟳transitive property.
Subclasses
An auditor asks to see everything the compliance function touched. Assert that three of the agents are compliance agents.
⇑((:KYC_Agent ⊑ :Compliance_Agent))
⇑((:Fraud_Detection_Agent ⊑ :Compliance_Agent))
⇑((:Compliance_Crew ⊑ :Compliance_Agent))
Each agent’s hops are typed by its class, and Sajak propagates the type up the
subclass chain. Ask for the entailed facts directly, with |>> pentad.
(?a ∈ :Compliance_Agent) |>> pentad
(:KYC_Agent ∈ :Compliance_Agent)
(:Fraud_Detection_Agent ∈ :Compliance_Agent)
(:Compliance_Crew ∈ :Compliance_Agent)
No record was ever tagged “compliance,” yet the answer comes back anyway, 221 hops across the three agents (78 KYC, 76 Fraud Detection, 67 Compliance Crew), because the org chart lives once in the ontology instead of being copied onto every record. Sajak derives the membership; nothing stored it.
Subproperties
Ask who contacted whom and the three different message verbs the agents actually use all count as contact, without anyone rewriting the log: ten agent-to-agent contacts, all fanning out from the Customer Agent.
Assert that the three task-passing methods are all forms of contact.
⇑((:tasks_send ⊴ :contacts))
⇑((:tasks_get ⊴ :contacts))
⇑((:tasks_subscribe ⊴ :contacts))
Sajak lifts every edge under a subproperty to the superproperty, so each
agent-to-agent task call becomes a :contacts edge. Read the contact graph.
(?x :contacts ?y)
(:Customer_Agent :contacts :KYC_Agent)
(:Customer_Agent :contacts :Credit_Agent)
(:Document_Processing_Agent :contacts :Compliance_Crew)
…
Inverses
Assert that being called is the inverse of calling.
One caller for the credit agent; the KYC agent, asked the same way, answers with three: the customer agent, document processing, and fraud detection.
⇑((:calls ⇄ :calledBy))
Sajak transposes every :calls edge into a :calledBy edge in the other
direction. The question “who called the credit agent,” which the raw store can
answer only by scanning every source, becomes a direct read of an entailed
predicate.
(:Credit_Agent :calledBy ?caller)
(:Credit_Agent :calledBy :Customer_Agent)
Transitivity
Escalation is transitive: if a case escalates from one agent to a second and the second to a third, it has escalated to the third, whether or not that last leg was ever logged. Declared once as a property, Sajak closes every chain, and from the Customer Agent escalation reaches all seven downstream agents.
Declare escalation a transitive property.
⇑((:escalatesTo ∈ ⟳))
Given the escalation edges along each journey, Sajak closes the chain, so an escalation two hops away is entailed as directly as one hop away.
(:Customer_Agent :escalatesTo ?a)
(:Customer_Agent :escalatesTo :KYC_Agent)
(:Customer_Agent :escalatesTo :Compliance_Crew)
…
The closure the hash-chain query computed by hand is here a fact about the vocabulary, that is, about relations in the world, not the query, that is, the question we ask about them.
A query at every joint
The examples so far each show one feature. Real audit questions need several at once, and this last one is worth working all the way through, because the finding at the end is real.
Hard questions auditors ask or should
- Mark a block warned if any earlier record on its chain flagged high risk.
- A blind block is a block that was never warned.
- Find every agent that handled a hop upstream of a blind block, and on which loan.
- Per agent, count the distinct loans it touched and take its worst latency.
- Keep the agents on two or more such loans.
- Of those, keep the ones the ontology calls compliance agents.
Start with the worst kind of refusal. Call a block blind when the system never saw it coming: the application was stopped, but nothing earlier on its file had raised a high-risk flag. That is the denial that produces an angry phone call and a fair-lending complaint, because the record holds no warning to explain it. Thirty-one of the 33 blocks in this corpus are blind.
Now the question an examiner would actually ask: among the agents who were handling a file just before it was blindly blocked, who turns up again and again, and are any of them the compliance specialists whose job is to catch exactly this?
The query answers it in six steps, reusing leads_to, the recursive walk over
:prev from the hash-chain example, and the ⊑ compliance TBox from the Sajak
section.
warned(?b) ⇒ leads_to(?u ?b) (?u :risk :high)
blind_block(?b) ⇒ (?b :decision :block) ¬ warned(?b)
exposed(?a ?t) ⇒ leads_to(?r ?b) blind_block(?b) (?r :from ?a) (?r :task ?t)
exposed_lat(?a ?l) ⇒ leads_to(?r ?b) blind_block(?b) (?r :from ?a) (?r :latency ?l)
j_count(?a ?n) ⇒ exposed(?a ?t) ?n ≔ #(?t) ▷ ?a
j_worst(?a ?w) ⇒ exposed_lat(?a ?l) ?w ≔ ⊔(?l) ▷ ?a
scorecard(?a ?n ?w) ⇒ j_count(?a ?n) ?n ≥ 2 j_worst(?a ?w)
compliance_scorecard(?a ?n ?w) ⇒ scorecard(?a ?n ?w) (?a ∈ :Compliance_Agent)
scorecard(?a ?n ?w)
compliance_scorecard(?a ?n ?w)
scorecard(:Customer_Agent 16 401.7)
scorecard(:Authentication_Agent 8 62.1)
scorecard(:Document_Processing_Agent 4 250.6)
scorecard(:KYC_Agent 3 58.9)
scorecard(:Credit_Agent 2 32.3)
compliance_scorecard(:KYC_Agent 3 58.9)
Five agents recur upstream of blind blocks. Rank them and one stands out for the wrong reason: the KYC agent, a compliance specialist, was on the file for three separate blind blocks and flagged none of them. That is a finding an examiner escalates, and the point of the pipeline is to surface it before the examiner does.
What makes it a real test of the language is that no single feature answers it. It takes recursion to walk the chain, negation to define “never warned,” two aggregates to score each agent, a threshold to keep the repeat offenders, and an ontology to know which agents are compliance agents, all in one program. Every capability the earlier examples showed in isolation, working together on one question that matters, with the engine alone working out the order to run them in. Every stratum boundary PLRN-019 §4 describes, crossed once.
A note on method
Written in conversation with Claude Opus 4.8 (Anthropic) as structured interlocutor. Gleamalog and Sajak were implemented by agents under my direction. The design, claims, and the framing are mine.
Kendall Clark · k@pentad.ai
Great Falls, Virginia
10 July 2026