Datalog Rules
Datalog rules let you define custom inference logic that goes beyond what
OWL and RDFS provide. A rule is an ordinary query where clause — the
body — plus an insert clause naming the triples to derive for every
solution — the heads. Rules run in a fixpoint loop that can chain rules
together and recurse.
Rule bodies are evaluated by the same query engine as any other query, so a
rule can use everything a where clause can: edge annotations (claims and
other relationship metadata), the full FILTER expression language, BIND,
VALUES, UNION, property paths and subqueries. The only constructs a rule
body may not use are the non-monotonic ones — OPTIONAL, MINUS, not-exists,
aggregates and SERVICE — which a fixpoint cannot evaluate soundly; those are
rejected with an error naming the construct (see Validation).
For background concepts see Reasoning and inference; for enabling reasoning in queries see Query-time reasoning.
Quick example
Infer a grandparent relationship from two parent hops:
{
"@context": {"ex": "http://example.org/"},
"select": ["?gp"],
"where": {"@id": "ex:alice", "ex:grandparent": "?gp"},
"reasoning": "datalog",
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": {"@id": "?person", "ex:parent": {"ex:parent": "?gp"}},
"insert": {"@id": "?person", "ex:grandparent": {"@id": "?gp"}}
}
]
}
The rule says: “For any ?person whose parent has a parent ?gp, insert
that ?person has a grandparent ?gp.” The query then finds Alice’s
grandparents using the inferred facts.
Rule format
Each rule is a JSON object with three parts:
| Key | Required | Description |
|---|---|---|
@context | Yes | JSON-LD context for expanding compact IRIs |
where | Yes | The rule body — any query where clause (see Body) |
insert | Yes | Pattern(s) of new facts to derive when the body matches |
@id | No | Optional name/IRI for the rule (used in diagnostics) |
Body: the where clause
The body is parsed and lowered exactly like a query’s where clause, using
the rule’s own @context.
Single pattern:
"where": {"@id": "?person", "ex:parent": "?parent"}
Multiple patterns (implicit join on shared variables):
"where": [
{"@id": "?person", "ex:parent": "?parent"},
{"@id": "?parent", "ex:name": "?parentName"}
]
Nested patterns (shorthand for multi-hop traversal):
"where": {"@id": "?person", "ex:parent": {"ex:parent": "?gp"}}
Property-position variables (the predicate is a variable):
"where": [
{"@id": "?s", "ex:sameAs": {"@id": "?other"}},
{"@id": "?other", "?prop": "?val"}
]
A variable in predicate position matches any predicate and binds it; the
bound predicate can be reused in the insert clause.
Edge annotations (claims and relationship metadata):
"where": [
{"@id": "?a", "ex:knows": {"@id": "?b", "@annotation": {"ex:confidence": "?c"}}},
["filter", "(> ?c 0.85)"]
]
Binding an annotation returns one solution per annotation, so a fact asserted
by two sources with different confidence values is considered once per claim.
The claim-first form works too — start from the metadata and walk back to
the edge with @reifies:
"where": [
{"ex:confidence": "?c", "ex:source": {"@id": "ex:sourceB"},
"@reifies": {"@id": "?a", "ex:knows": {"@id": "?b"}}},
["filter", "(> ?c 0.85)"]
]
See Edge annotations for the model.
Filters, BIND, VALUES, UNION, paths: the same syntax as in queries —
["filter", "(and (> ?age 18) (< ?age 65))"], ["bind", "?pct", "(* ?c 100)"],
["values", ["?src", [{"@id": "ex:sourceA"}, {"@id": "ex:sourceB"}]]],
["union", {...}, {...}], and @path aliases for transitive traversal. See
JSON-LD query.
Filter operands
A filter operand is classified by how it is written:
| Operand | Read as |
|---|---|
?name | Variable |
62, 1.5, true | Number / boolean literal |
ex:ssn (prefix defined in the rule’s @context), <http://example.org/ssn>, http://example.org/ssn | IRI — compared by term identity, as in SPARQL |
"senior", "John Smith" | String literal (quoted; may contain spaces) |
foo:ssn (prefix not defined in the @context) | Rejected — the rule is invalid (see below) |
senior (unquoted bare word) | Rejected — quote it for a string, prefix it for an IRI |
Three rules worth knowing:
- IRI comparison is by identity.
(= ?p ex:knows)matchesex:knowsand notfoaf:knows.=and!=are the meaningful comparisons for IRIs. - A bare word is rejected, not guessed.
(= ?p knows)is an error naming the operand: write"knows"(quoted) to compare against the string, orex:knowsto compare against the IRI. Against an IRI-bound variable a string comparison fails invisibly in both directions —=derives nothing, and!=keeps every row, so an exclusion filter derives exactly the facts it was written to exclude. - An unresolvable IRI operand is an error, not a fallback. If a filter
names a prefix the rule’s
@contextdoes not define, or a namespace the ledger has never seen, the rule is rejected rather than run with a filter that cannot match.
Insert clause
The insert clause defines the triples to produce for each solution of the
body.
"insert": {"@id": "?person", "ex:grandparent": {"@id": "?gp"}}
-
Variables in any position — subject, predicate, or object — are replaced with the bound values from
where. A predicate variable lets a rule write a property whose name is computed:"insert": {"@id": "?s", "?prop": "?val"}copies every?prop/?valbound in thewhereclause onto?s. -
Use
{"@id": "?var"}or{"@id": "ex:iri"}for IRI/entity values; use"?var"directly for literal values. Typed literals use the value-object form:{"@value": "2024-01-01", "@type": "xsd:date"}; language-tagged strings use{"@value": "chat", "@language": "fr"}. -
@typederivesrdf:typetriples; an array of objects (or an@graph) derives several heads from one rule. -
Every variable used in
insertmust also appear inwhere. A head that references a variable the body never binds rejects the whole rule with an error naming the variable (awhere/inserttypo is the usual cause):"where": {"@id": "?s", "ex:relType": {"@id": "?relation"}}, "insert": {"@id": "?s", "?rel": {"@id": "?s"}}?relis never bound — thewhereclause binds?relation. -
Every node in an
insertpattern needs an@id. An anonymous node has no subject to derive facts about. -
Heads derive plain triples. An
@annotationblock in a head (deriving an edge annotation, for example to record which claims a derived edge rests on) is not supported yet and is rejected by name; see Reifier minting in rule heads.
Providing rules
Rules can be provided in two ways:
1. Query-time rules
Pass rules directly in the query via the rules array. This is the simplest
approach and doesn’t require any prior setup:
{
"select": ["?result"],
"where": {"@id": "?s", "ex:derived": "?result"},
"reasoning": "datalog",
"rules": [ ... ]
}
Note: Providing a
rulesarray automatically enables datalog reasoning — you don’t strictly need"reasoning": "datalog", though including it is recommended for clarity.
2. Database-stored rules
Rules can be stored in the database as f:rule assertions. A JSON-LD rule is
stored as a @json literal — the @type: "@json" wrapper is required, so
the rule body’s ?variables are kept as data rather than read as transaction
template variables:
{
"@context": {
"f": "https://ns.flur.ee/db#",
"ex": "http://example.org/"
},
"insert": {
"@id": "ex:grandparentRule",
"f:rule": {
"@type": "@json",
"@value": {
"@context": {"ex": "http://example.org/"},
"where": {"@id": "?person", "ex:parent": {"ex:parent": "?gp"}},
"insert": {"@id": "?person", "ex:grandparent": {"@id": "?gp"}}
}
}
}
}
Stored rules apply to any query that enables datalog reasoning
("reasoning": "datalog"). To apply them to every query without asking,
set a ledger default in the config graph:
PREFIX f: <https://ns.flur.ee/db#>
INSERT DATA {
GRAPH <urn:fluree:mydb:main#config> {
<urn:fluree:mydb:main:config:ledger> a f:LedgerConfig ;
f:reasoningDefaults [ f:reasoningModes f:Datalog ] ;
f:datalogDefaults [
f:datalogEnabled true ;
f:rulesSource [ a f:GraphRef ; f:graphSource [ f:graphSelector f:defaultGraph ] ] ;
f:allowQueryTimeRules true
] .
}
}
See Setting groups — datalogDefaults for full configuration options and Writing config data for the other ways to write the config graph.
f:rulesSource also supports cross-ledger references — set
f:ledger on the inner f:graphSource to pull f:rule JSON
bodies from another ledger at query time. See
Cross-ledger governance — Cross-ledger datalog rules
for the end-to-end pattern and failure modes.
When stored rules, cross-ledger rules, and query-time rules are present, they are all merged and execute together in the same fixpoint loop.
SPARQL rules
Rules can alternatively be written as SPARQL CONSTRUCT ... WHERE ...
queries: the CONSTRUCT template is the rule head (insert) and the WHERE
clause is the rule body. The language of a stored f:rule literal is
selected by its RDF datatype — @json means the JSON-LD rule format above;
the f:sparql datatype (https://ns.flur.ee/db#sparql) means SPARQL.
Store a SPARQL rule:
{
"@context": {"f": "https://ns.flur.ee/db#"},
"insert": {
"@id": "http://example.org/grandparentRule",
"f:rule": {
"@type": "f:sparql",
"@value": "PREFIX ex: <http://example.org/> CONSTRUCT { ?person ex:grandparent ?gp } WHERE { ?person ex:parent ?p . ?p ex:parent ?gp }"
}
}
}
The same typed-value form works inside a query-time rules array entry
(directly, or as the f:rule value of a stored-rule-shaped entry).
A SPARQL rule body may use anything a SPARQL WHERE clause may, including the RDF 1.2 annotation surface, subject to the same monotonicity rule as JSON-LD rules:
PREFIX ex: <http://example.org/>
CONSTRUCT { ?a ex:trustedKnows ?b }
WHERE { ?a ex:knows ?b {| ex:confidence ?c |} FILTER(?c > 0.85) }
PREFIX ex: <http://example.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
CONSTRUCT { ?a ex:trustedKnows ?b }
WHERE { ?claim rdf:reifies <<( ?a ex:knows ?b )>> ; ex:confidence ?c FILTER(?c > 0.85) }
Include PREFIX declarations in the rule text; the request @context is not
applied to rule sources. A CONSTRUCT template may not contain blank nodes (a
head cannot mint fresh nodes — every round would mint new ones) and, like a
JSON-LD head, may not carry an annotation tail yet.
Validation: a rule that cannot run is an error
A rule that fails to parse or validate fails the query with an error
naming the rule (its @id, or its position in the rules array) and the
problem. This applies to stored rules too: a ledger whose rule set contains a
broken rule fails its reasoning queries until the rule is fixed. Answering
over an incomplete rule set would be silently wrong, which is worse.
Rejected:
- Non-monotonic constructs in the body: OPTIONAL (
["optional", …]), MINUS, NOT EXISTS (["not-exists", …]), GROUP BY / aggregates, SERVICE. A fixpoint cannot evaluate negation or left joins soundly without stratification. - A head variable the body never binds (range restriction). A variable the
body only reads — a filter operand, or a variable bound inside an
existsblock — does not count as bound, because a matched row carries no value for it. LIMIT/OFFSET, on the body or on a subquery inside it: which solutions a slice keeps depends on how much the fixpoint has derived so far, so the rule would derive different facts depending on round order.- Graph-source patterns (R2RML, SERVICE, full-text / vector / geo / S2 search): they read data the fixpoint does not own and the reasoning cache key does not cover, so a derived fact could outlive the source it came from.
- A head literal its datatype cannot accept. Head literals are coerced to
their declared datatype, the same conversion a transaction applies, so a
derived
xsd:datereally is a date. A value the datatype rejects fails the rule rather than being stored mislabelled. - Filter operands that cannot match: a bare unquoted word, an undefined prefix, or a namespace the ledger has never seen (see Filter operands).
- A head that would mint an annotation or a blank node.
- Malformed rule documents: a missing
whereorinsert, a filter element that is not a string or array expression.
Positive existence checks (["exists", …]), UNION, BIND, VALUES, property
paths and subqueries are all allowed. A subquery may use DISTINCT and
ORDER BY — neither changes which solutions exist as the fixpoint grows — but
not GROUP BY, aggregates, LIMIT or OFFSET, which are rejected by name
like any other non-monotonic construct.
Negation is rejected in every spelling, and the rule is positional rather than
a list of negating operators. An EXISTS is allowed only where its truth value
is read directly and positively: as the filter condition itself, or under AND
/ OR in such a position. Anywhere else it is rejected — as a comparison
operand (FILTER(EXISTS { … } = false)), as an IF branch, as a function
argument, or bound to a variable (BIND(EXISTS { … } AS ?e)), because each of
those can negate it and a fixpoint cannot evaluate negation soundly without
stratification. FILTER(NOT EXISTS { … }), ["not-exists", …] and
FILTER(!EXISTS { … }) are all the same construct to the engine; an even
number of ! is not negation, so FILTER(!(!EXISTS { … })) still runs.
The rule is deliberately conservative: FILTER(EXISTS { … } = true) is
monotone and is still refused, because the check cannot tell it from its
negated twin. It fails toward a named error rather than a wrong answer.
Examples
Sibling inference
Infer siblings from shared parents:
{
"@context": {"ex": "http://example.org/"},
"select": ["?sibling"],
"where": {"@id": "ex:alice", "ex:sibling": "?sibling"},
"reasoning": "datalog",
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": [
{"@id": "?person", "ex:parent": "?parent"},
{"@id": "?sibling", "ex:parent": "?parent"},
["filter", "(!= ?person ?sibling)"]
],
"insert": {"@id": "?person", "ex:sibling": {"@id": "?sibling"}}
}
]
}
Chained rules (uncle + aunt)
Multiple rules that build on each other:
{
"@context": {"ex": "http://example.org/"},
"select": ["?aunt"],
"where": {"@id": "ex:alice", "ex:aunt": "?aunt"},
"reasoning": "datalog",
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": {"@id": "?person", "ex:parent": {"ex:brother": "?uncle"}},
"insert": {"@id": "?person", "ex:uncle": {"@id": "?uncle"}}
},
{
"@context": {"ex": "http://example.org/"},
"where": {
"@id": "?person",
"ex:uncle": {
"ex:spouse": {"@id": "?aunt", "ex:gender": {"@id": "ex:Female"}}
}
},
"insert": {"@id": "?person", "ex:aunt": {"@id": "?aunt"}}
}
]
}
The second rule (aunt) depends on facts derived by the first rule (uncle). The fixpoint loop handles this automatically — it keeps iterating until no new facts are produced.
Trusted edges from claims
Two sources make claims about the same fact; derive a trustedKnows edge
from any claim above a threshold, then walk only trusted edges:
{
"@context": {"ex": "http://example.org/", "trusted+": {"@path": ["+", "ex:trustedKnows"]}},
"select": ["?reachable"],
"where": {"@id": "ex:alice", "trusted+": {"@id": "?reachable"}},
"reasoning": "datalog",
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": [
{"@id": "?a", "ex:knows": {"@id": "?b", "@annotation": {"ex:confidence": "?c"}}},
["filter", "(> ?c 0.85)"]
],
"insert": {"@id": "?a", "ex:trustedKnows": {"@id": "?b"}}
}
]
}
Derived predicates are ordinary predicates to the rest of the query, so a
property path over ex:trustedKnows traverses the derived edges.
Rules with filters
Classify people by age:
{
"@context": {"ex": "http://example.org/"},
"select": ["?person"],
"where": {"@id": "?person", "ex:status": "senior"},
"reasoning": "datalog",
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": [
{"@id": "?person", "ex:age": "?age"},
["filter", "(>= ?age 65)"]
],
"insert": {"@id": "?person", "ex:status": "senior"}
}
]
}
Combining with OWL reasoning
Datalog rules can build on OWL-derived facts. For example, use OWL 2 RL to materialize transitive and symmetric properties, then use Datalog for custom business logic:
{
"select": ["?recommendation"],
"where": {"@id": "ex:alice", "ex:recommended": "?recommendation"},
"reasoning": ["owl2rl", "datalog"],
"rules": [
{
"@context": {"ex": "http://example.org/"},
"where": [
{"@id": "?person", "ex:friend": "?friend"},
{"@id": "?friend", "ex:likes": "?item"},
{"@id": "?person", "ex:likes": "?item"}
],
"insert": {"@id": "?person", "ex:recommended": {"@id": "?item"}}
}
]
}
If ex:friend is declared as a owl:SymmetricProperty, OWL 2 RL
materializes the reverse friendship links, and then the Datalog rule can
find items liked by mutual friends.
Execution model
Fixpoint evaluation
Rules execute in a fixpoint loop:
- Every rule body is evaluated by the query engine against the current data (base + previously derived facts).
- The heads are instantiated for each solution; new facts are collected.
- If any new facts were produced, go back to step 1 with the expanded fact set.
- When no new facts are produced (fixpoint reached), the loop terminates.
This means:
- Recursive rules work. A rule can produce facts that trigger itself again.
- Rule chaining works. Rule A can produce facts that trigger Rule B, and vice versa.
- Termination is guaranteed by a maximum fixpoint-iteration bound and by
the shared reasoning budget — a maximum derived-fact count, a maximum
memory estimate for the derived facts, and a maximum wall-clock time, the
same budget OWL2-RL uses. The fact ceiling is checked before each derived
fact is kept, so a run returns at most that many and a single round cannot
overshoot. The budget bounds the derived facts, not a rule body’s
intermediate binding rows: each body is evaluated in full before its heads
are instantiated, so a body matching far more rows than it derives facts can
exceed the memory estimate while running. Hitting the budget stops early and
marks the result
cappedin the tracked response’sreasoningblock. Configure it withf:reasoningMaxFacts/f:reasoningMaxSeconds/f:reasoningMaxMemoryMb(ledger config),"reasoningBudget"(query), orFLUREE_REASONING_MAX_FACTS/FLUREE_REASONING_MAX_SECONDS/FLUREE_REASONING_MAX_MEMORY_MB(server).
Execution order
Rules are ordered before the fixpoint by a lightweight heuristic (fewest predicate dependencies first). Because the fixpoint re-runs every rule each iteration until no new facts are produced, this ordering only affects how quickly the fixpoint converges — never the final set of derived facts. Rule chaining (rule A’s output feeding rule B) works regardless of the order.
Interaction with OWL 2 RL
When both OWL 2 RL and Datalog are enabled:
- OWL 2 RL materialization runs first.
- Datalog rules run over the combined base data + OWL-derived facts.
- Both result sets are merged into a single overlay for query execution.
Performance considerations
- Rule bodies are planned queries. They take the same index lanes and join ordering as any query, so the usual query advice applies: put a selective pattern in the body, and prefer bound predicates.
- Budget limits apply. The same time / fact / memory budgets as OWL 2 RL materialization apply to Datalog execution (default: 30s and 1M derived facts, with a memory ceiling derived from the fact ceiling).
- Results are cached. A materialization is cached per ledger state and rule set: the same rules (stored or query-time, hashed by content) against an unchanged ledger return instantly from the reasoning cache. Any commit invalidates the entry.
Related pages
| Topic | Page |
|---|---|
| Conceptual introduction | Reasoning and inference |
| Enabling reasoning in queries | Query-time reasoning |
| Edge annotations (claims) in rule bodies | Edge annotations |
| Engine design and the head-annotation decision | Rules engine design |
| OWL & RDFS constructs | OWL & RDFS reference |
| Ledger-wide config | Setting groups |