Logic Cells
The complete guide to DataGroutβs symbolic reasoning layer: engines, rules, in-rule tool calls, tabling, probabilistic reasoning, batteries, and cross-namespace composition.
For the per-tool API reference (logic.remember, logic.query, logic.assert, β¦) see the Logic Tools Reference. This guide covers what you can do inside a cell.
What a logic cell is
A logic cell is a durable, per-namespace fact-and-rule space with a Prolog engine attached. Facts and rules persist in encrypted storage as the source of truth; a warm Prolog worker per namespace serves queries at sub-millisecond latency and is rebuilt automatically from storage whenever it goes cold. You never manage workers β assert facts, store rules, query.
Namespaces are fully isolated: their facts, rules, engine choice, and worker are their own. logic.worlds lists yours.
Engines: SWI, Scryer, and the ISO default
Every namespace runs on one of two Prolog engines:
| Scryer (ISO) | SWI | |
|---|---|---|
| ISO conformance | strict | no (superset) |
Tabling (:- table) |
β | β |
call_tool in queries & stored rules |
β | β |
Probabilistic rules (P::head) |
β via battery rewrite | β native |
Graph traversal (path, reachable, neighbors) |
β | β |
Weighted probability (probability/2, *_prob, *_h) |
β | β |
SWI dicts (_{k: v}), SWI-only builtins |
β | β |
The default is ISO. A fresh namespace is configured auto and resolves to Scryer. If you later store a rule that genuinely needs SWI (dicts, SWI-only builtins), an auto namespace upgrades to SWI automatically β one-way and sticky for the life of the namespace. Rules that use call_tool or ProbLog notation do not trigger the upgrade; both are fully supported on the ISO engine.
Inspect or set the engine with logic.namespace:
{ "namespace": "my-app" } // read: configured + resolved engine, pin state
{ "namespace": "my-app", "engine": "swi" } // set explicitly (auto | swi | scryer)
{ "namespace": "my-app", "iso_pin": true } // pin ISO-only (see below)
The read form works before the namespace exists and reports exactly what first use will establish.
ISO pinning
iso_pin: true makes the ISO guarantee inviolable: the namespace can never fall back to SWI, and rules using non-ISO constructs are rejected rather than triggering an upgrade. Pinning is monotonic β it cannot be undone; fork to a new namespace if you need a fresh engine choice. Use it when strict ISO conformance is a requirement (compliance tiers, reactor apps), not just a preference.
Switching a populated namespace onto Scryer reloads it immediately and returns any non-ISO rules that had to be disabled (disabled_rules), so nothing dies silently.
Rules (constraints)
Store rules through logic.assert with the prolog field (or logic.constrain):
{ "namespace": "crm", "prolog": "hot_lead(L) :- attribute(L, score, S), S > 80." }
Rules are validated before compilation (call-graph analysis, termination classification, sandbox check) and each one gets a Cognitive Trust Certificate recording the assurances: validated, termination_class (datalog / well_founded / generator / unbounded), sandboxed, cycle-freedom, and the defined/called predicate lists. Queries mint their own certificates referencing the rules they exercised. Rules that call tools (see below) are fully certifiable.
Multi-clause blobs are fine β facts and rules can share one prolog string. Engine directives like :- table are recognized, applied, and reported separately (directives in the response) β they are never stored or certified as rules.
Predicates available inside rules and queries
Fact predicates
entity(Name), attribute(Entity, Attr, Value), relation(Subj, Rel, Obj), metric(Entity, Metric, Value), tag(Entity, Tag), context(Key, Value).
Values match flexibly: a goal written with atoms matches string-stored values and vice versa, so the same query text works regardless of how a fact was asserted.
Graph and probability
Graph traversal β both engines. reachable(From, To) / reachable(From, To, RelType, MaxDepth) for existence, path(From, To, RelType, MaxDepth, Path) for the route, neighbors(Node, Neighbors) / neighbors(Node, RelType, Direction, Neighbors) for one-hop adjacency (Direction: out / in / both). Walks are cycle-safe via a visited set and bounded by MaxDepth (default 10).
Path is a list of hops, each a [from-F, relation-R, to-T] pair-list; neighbors entries are [node-N, relation-R, direction-D]. Read them with member/2 β the same portable shape call_tool results use.
Weighted probability β SWI only. probability(Goal, P) (noisy-OR marginal), path_prob/6, reachable_prob/5, lc_fact_weight(Handle, P), and the handle-aware fact variants (attribute_h/4, relation_h/4, β¦) form one subsystem that depends on per-fact weights being joined by fact handle. ISO namespaces load facts without handles, so none of it is available there β ISO gets rule-weighted probability instead, via psuccess/2 (see Probabilistic rules below).
Fact handles themselves are a platform primitive rather than a Prolog one: logic.assert returns the handles it wrote and logic.forget takes them, so handle-based work belongs at the tool layer and behaves identically on both engines.
Utilities
findall/3, aggregate_all/3, member/2, memberchk/2, append/3, sort/2, msort/2, length/2, between/3, sum_list/2, max_list/2, min_list/2, nth0/3, nth1/3, include/3, exclude/3, maplist/2-4, term inspection (functor/3, is_list/1, compound/1), arithmetic and comparison operators, and string helpers (atom_string/2, string_concat/3, β¦). Note catch/3 is not available to user goals β error handling uses bound error markers instead (see below).
call_tool/3 β tools as evidence, mid-proof
Rules and queries can call any tool on your server, right in the middle of resolution:
akin(A, B) :-
ev(A, VA), ev(B, VB), A \== B,
call_tool('data-grout@1/linalg.cosine_sim@1', [a-VA, b-VB], R),
member(similarity-S, R), S > 0.5.
-
Args are a pair-list (
[key-Value, ...]) β it becomes the toolβs JSON arguments. -
Results come back as a pair-list too: read fields with
member(key-V, R)ormemberchk/2. Lists of records arrive as lists of pair-lists. -
Multi-solution backtracking over
call_toolis safe and returns every solution (bounded at 1,000 per query). - Billing, policy, and PII rules apply exactly as if the agent called the tool directly.
call_tool/4 additionally binds the response metadata (credits, cache_ref, certificate) for cost-tracking and audit rules: call_tool(Tool, Args, Payload, Meta).
call_tool_cached/3,4 memoizes on the canonicalized (tool, args) β the first call dispatches, every repeat is a plain fact lookup. Use it for repeated identical evidence calls and inside tabled reasoning. Ground arguments only; the cache is per-worker and bounded (512 entries); lc_clear_tool_cache/0 drops it.
Limits and refusals. At most 64 tool calls per query, one nesting level of toolβquery recursion, and logic.* tools are refused as inner calls against the running namespace (they would deadlock its worker) β cross-namespace logic calls have their own predicates below. Refusals and tool failures never abort your query: the result binds an error marker your rule can pattern-match, and comparisons against it simply fail. For tool failures and refusals the marker carries a pair-list you can read with member/2:
call_tool(T, A, R),
( R = '$call_tool_error'(E) -> member(reason-Why, E), handle(Why)
; use(R) ).
E is [error-Message, reason-Why] (reasons include tool_error, tool_exception, denylisted_lc_tool, depth_exceeded, parse_error, unsafe_inner_goal); transport-level failures bind an atom instead (e.g. '$call_tool_error'(json_parse_failed)). The shape is identical on both engines.
call_ns/3,4 β query another namespace
call_ns('forensics', 'causal_chain(cert_expired, X, Path)', Solutions).
call_ns(NS, Goal, Solutions, Limit). % default Limit 50
Evaluates a goal against another of your namespaces β on that namespaceβs own worker and engine (an ISO cell can query an SWI cell and vice versa) β and binds the solutions as a list of [VarName-Value, ...] pair-lists. The target must be a different namespace than the running query; chains are depth-limited. Errors bind '$call_ns_error'(Reason).
call_tool_into/4 β route a toolβs results into a namespace
call_tool_into('salesforce@1/get-accounts@1', [region-west],
'crm_mirror', Receipt).
Dispatches the tool, then materializes its result as facts in the target namespace: each record becomes an entity (named from its id/name field, or content-hashed β so re-running converges instead of duplicating) with an attribute per scalar field. Receipt reports entities, facts_written, and any nested fields that were skipped. Same-namespace targets are refused; errors bind '$call_tool_into_error'(Reason).
findall_limit/4
findall/3 with a solution cap β the right tool for bounding generative sub-goals inside a rule.
Tabling
:- table pred/arity is honored on both engines. Tabled predicates memoize their answers, which makes recursive definitions over cyclic data terminate:
:- table reach/2.
reach(X, Y) :- edge(X, Y).
reach(X, Z) :- edge(X, Y), reach(Y, Z).
Pass that whole block as the prolog field of logic.assert.
The directive may precede or follow its clauses, in the same batch or a later one. Tables are invalidated automatically when facts change, so tabled answers always reflect current data. Tabling composes with call_tool_cached (table the pure predicates; make evidence calls cached).
Probabilistic rules
ProbLog-style weight annotations work on both engines:
{ "prolog": "0.7::likes(alice, prolog)." }
-
On SWI, the notation is native; query marginals with
probability(Goal, P). -
On ISO/Scryer namespaces, the rule is transparently rewritten to
prob_rule/2form and theprob-core-isobattery is auto-installed; query withpsuccess(Goal, P)(pluspand/2for conjunctions andexpected/3for expected values). The namespace stays on the ISO engine.
Weighted rules accumulate under their head β each P::head :- body is an additive rule, matching ProbLog authorship. Re-stating a body updates its weight; pass mode: "replace" to overwrite the whole set (how you express a correction).
Batteries
Batteries are installable rule packs, scoped per namespace:
-
batteries.search/batteries.install_many/batteries.installed/batteries.validate/batteries.remove -
batteries.validateruns every predicate the battery defines and tells you exactly which facts to assert to make it fire β run it after install.
Four rule libraries are preloaded into every worker on both engines (no install needed): forensic (causal chains, blast radius, contradictions β see the Forensic Tools page), fsm (state-machine reachability, cycles, validation), plan-fsm (plan execution tracking), and lumen-bench (usage/cost analysis over _lumen facts).
Configuration quick reference
| Setting | Where | Values / limits |
|---|---|---|
| Engine |
logic.namespace engine |
auto (default, ISO-first) / swi / scryer |
| ISO pin |
logic.namespace iso_pin |
true (one-way) |
| Namespace |
every logic.* tool |
any string; default "default" |
| Query timeout |
logic.query timeout |
1β10 s (default 3) |
| Result paging |
logic.query limit/offset |
limit β€ 500 (default 50), has_more in response |
| Solutions per streamed query | fixed | 1,000 |
| Tool calls per query | fixed | 64 |
| Assert mode |
logic.assert/remember mode |
append / upsert / preview; dry_run: true validates only |
| Weighted-rule mode |
logic.assert mode |
merge (default, additive) / replace |
Serving rules as apps
Any rule in a cell can be exposed as a live HTTP API and shareable web form β see Reactor Apps & Smart Panels.
Error conventions
Inside a query, tool and cross-namespace failures bind markers instead of throwing: '$call_tool_error'(Detail), '$call_ns_error'(Reason), '$call_tool_into_error'(Reason). For call_tool, Detail is a [error-Message, reason-Why] pair-list (read it with member/2); for the cross-namespace predicates the argument is the reason itself. The shapes are identical on both engines. Pattern-match them when you want fallback behavior; otherwise downstream comparisons fail cleanly and the query simply yields fewer solutions. Errors that reach the top level (syntax errors, timeouts, undefined predicates) come back as structured tool errors with a diagnostic and, where possible, a suggestion.