Skip to content

Use case: SPARQL, structured query & SHACL

Counts, aggregates, path patterns, and validation all need exact answers. Three complementary APIs run over the same object-storage graph, and all three agree on the snapshot they serve:

  • Structured query: a typed SPARQL-subset SELECT/aggregate you build as JSON. Best from application code.
  • SPARQL 1.1 text: the conformant engine, plus a native /sparql Protocol endpoint for external tools.
  • SHACL: validation, shape selection, property paths, and rule-based inference.

The structured surface is a SELECT/ASK over a basic graph pattern with typed filters, GROUP BY, aggregates, HAVING, and ORDER BY. Use it when you are assembling a query in code and want typed attribute predicates without writing RDF IRIs.

One server-side query answers “commits per area per month”:

{
"patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }],
"group_keys": [
{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } },
{ "property": { "var": "c", "field": "area", "as": "area" } }
],
"aggregates": [{ "func": "count", "as": "n" }],
"order_by": [{ "var": "m" }]
}

GROUP BY keys can be entity identity, a typed scalar attribute ({ property: { var, field, as } }), or a calendar bucket of a datetime attribute ({ date_bucket: … }). Those same attribute fields work in filters and as SUM/AVG/MIN/MAX operands. A filters/having entry has the shape { compare: { op: eq|ne|lt|le|gt|ge, left, right } } (or and/or/not), where each operand is { var }, { property: { var, field } }, or a typed { value: { str | i64 | f64 | bool | date_time | entity } }.

Discover the queryable attribute field names with an ontology view (property_defs) or a schema read. Attribute pattern predicates are case-insensitive here. Add combinators (union/optional/minus/exists/ not_exists) for group-graph-pattern legs.

// Typed attribute filters without RDF IRIs.
await lbb.entities.filterByAttributes({
patterns: [{ subject: { var: "service" }, predicate: "WRITES_TO", object: { var: "db" } }],
where: [{ field: "slo", op: "ge", value: 0.99 }, { var: "db", field: "tier", value: "prod" }],
select: ["service"],
});
// Full structured SELECT/aggregate.
await lbb.sparql(/* SparqlSelectRequest body */);

Run conformant SPARQL SELECT/ASK and get parsed rows back, with no zipping of head.vars against binding values:

const { vars, rows } = await lbb.sparqlRows({
query: `SELECT ?service ?db WHERE {
?service <https://littlebigbrain.com/r/writes_to> ?db
} LIMIT 10`,
entailment: "rdfs", // apply the RDFS rules at query time
});
for (const row of rows) console.log(row.service, "->", row.db);
const exists = (await lbb.sparqlRows({ query: "ASK { ?s ?p ?o }" })).boolean;

Engine extensions are options or keyword arguments. The default published projection is asserted-only (entailment: "none", reason: false). entailment: "subclass", "rdfs", and "owl" apply that regime at query time from the pinned generation’s schema triples (see reasoning); reason: true returns a typed error because stored rules already run at publish time. as_of_commit_seq requests one exact retained checkpoint-plus-delta view and returns 400 historical_generation_unavailable only when that lineage has been reclaimed. SPARQL valid-time pins fail closed; use the temporal state/history APIs for valid-time reads.

The JSON query APIs default to eventual (bounded-staleness) serving. An eventual read pins the immutable F3 base named directly by the branch head and serves exactly its served_at_seq; maintenance lag can make that watermark older. Strong is an explicit opt-in: it applies the head’s ordered, gap-free per-commit F3 delta suffix on top of that base and is therefore exact immediately after every acknowledged write. Neither mode reads WAL, switches to another snapshot, or falls back to a leveled maintenance reader. The modes are selected as:

  • Structured SparqlSelectRequest and AnalyticQueryRequest (/v1/query/analytics): set consistency in the JSON body (default eventual); request "strong" for head-exact results.
  • Text (/v1/query/sparql-text): select it on the URL with ?consistency=eventual, since the text dialect carries a bare query body (default eventual).
  • Native SPARQL 1.1 Protocol (/sparql): ?consistency= on the URL too, with the same eventual default. Request strong explicitly for head-exact results. An unknown value is a typed 400.

An eventual response reports the watermark it served from on the snapshot envelope: served_at_seq (the commit the results reflect) plus stale: true when it lags head. Entailment regimes apply on eventual and strong reads alike, always from the pinned base’s schema; strong changes whether its exact delta suffix is applied, never the reasoning. In the console Query view, a per-run selector switches modes. An eventual result shows an “as of @N” badge. If the immutable base is damaged or temporarily unavailable, Query polls reconciliation status and reruns the original strong request after the base is repaired; eventual remains an explicit older-base choice.

A standalone stack serves the native SPARQL 1.1 Protocol at /sparql, so YASGUI, Protégé, and RDFLib’s SPARQLWrapper connect directly through GET ?query=, a POST form, or an application/sparql-query body. SELECT and ASK negotiate JSON, XML, CSV, and TSV; CONSTRUCT and DESCRIBE negotiate Turtle or N-Triples. The engine serves one default graph per little big brain database graph; named graphs are refused at every write entrance, and dataset isolation comes from graphs and branches instead. Graph-form responses have a hard 100,000-triple materialization cap, and a response that exceeds it returns an error. See the HTTP API.

Per SPARQL semantics, a FILTER whose comparison is a type error silently drops the row, so comparing datatypes the engine does not order yields an empty answer with no error message. little big brain defines value comparison (</<=/>/>=/=, MIN/MAX, ORDER BY) for:

  • Numerics across the XSD promotion lattice, strings (codepoint order), and booleans.
  • Temporal types, same datatype on both sides: xsd:dateTime, xsd:date, xsd:time, and the Gregorian fragments gYear, gYearMonth, gMonthDay, gMonth, gDay. The fragments are an extension beyond the SPARQL 1.1 operator table, using the XSD 1.1 order, because real corpora type years and months this way.
  • Durations, same datatype on both sides: xsd:yearMonthDuration and xsd:dayTimeDuration (total order), and xsd:duration (XSD’s partial order, where indeterminate pairs like P30D vs P1M remain type errors).

Cross-datatype pairs (xsd:date vs xsd:dateTime, gYear vs gYearMonth) and timezone-indeterminate pairs (a naive vs a zoned value within XSD’s ±14:00 window) remain type errors, so make bounds the same datatype as the data and keep timezones consistent. When a query answers suspiciously few rows, the execution stats report how many rows the filters dropped through type errors (filter_type_errors), which separates “nothing matched” from “the comparison was undefined”.

The temporal extraction functions follow the same rule. YEAR() accepts xsd:dateTime, xsd:date, and the year-bearing fragments xsd:gYear and xsd:gYearMonth. MONTH() accepts xsd:dateTime, xsd:date, and xsd:gYearMonth. DAY() accepts xsd:dateTime and xsd:date. So SUM(YEAR(?o)) over a corpus that types years as gYear (DBLP does) answers the sum. Component-less combinations (MONTH() or DAY() on gYear, DAY() on gYearMonth) stay type errors, their dropped rows count in filter_type_errors, and a SUM/AVG/MIN/MAX whose every input errored comes back unbound. An empty group still sums to 0, per the spec.

SHACL validation runs as durable maintenance work. Maintenance owns the versioned shapes configuration, validates it, and stores the resulting report as an immutable branch-owned sidecar keyed by the F3 checksum and exact schema identity. Validation does not hold back RDF/SPARQL publication.

Read GET /v1/ontology/conformance to retrieve that report with its validated_at_seq, ontology version, and shapes version. Eventual consistency reads the report attached to the selected immutable base. Strong consistency returns strong_read_pending until an exact-head sidecar exists. The HTTP handler never runs validation or projection over the whole graph.

Ontology and shapes remain one coherent identity. If you evolve an ontology while explicit shapes are active, little big brain atomically restates those same shapes against the new ontology version, so readers cannot observe the new ontology with a stale shape reference.

Common class-targeted schemas stay bounded even on large graphs. Shapes using direct property paths with sh:minCount and sh:maxCount stream over the disk-backed RDF index and stop each cardinality lookup once the answer is known. More expressive SHACL, such as SPARQL constraints, recursive paths, logical combinations, or closed shapes, uses the full validator. If that validator cannot fit the configured maintenance-memory budget, publication reports an explicit unrunnable plan instead of exceeding the limit or publishing without validation.

Publishing is strict about what it activates. POST /v1/schema/publish parses every shape at publish time and rejects, with a clear error, anything the validator could not run: a malformed SPARQL constraint, an sh:ask validator, a SPARQL-based target (sh:target), or an sh:path expression the engine does not recognize. A schema that publishes is a schema that validates. SPARQL constraints declare prefixes the standard way: sh:prefixes pointing at sh:declare declarations resolves those prefixes into the sh:select text.

With schema mode reject, every write validates its hypothetical post-write focus nodes before the graph-head CAS. RDF import, SPARQL Update, and Graph Store preserve source RDF terms for this purpose: a shape targeting <https://example.com/ActiveStudySite> focuses that IRI, not little big brain’s internal storage id. Retractions and field removals are applied to the hypothetical dataset first, so deleting the last required value is rejected and writes no WAL head.

Validation messages support SHACL message templates. An authored sh:message such as "{$this} is missing a value on {$path}" substitutes the focus node, the offending value ({$value}), and the path into each report result. A composite violation, such as a value node failing an sh:node shape, includes nested details results that name the inner constraint that failed.

little big brain’s graph is backed by RDF terms and validated with SHACL. The two standards solve problems that are acute for AI agents: they govern what a model writes and let the system reason over the result.

  • Atomic, self-describing facts. A triple is subject, predicate, object. An agent emits knowledge one claim at a time, and the store composes those claims into a graph. There is no table to design, no columns to pick, and no schema change needed for a relationship that was not modeled before. That fits a model’s incremental output better than a fixed relational schema.
  • Stable global identifiers. An entity is the same entity everywhere it is referenced. Facts from turn 1 and turn 900, or from two different agents, merge on identity instead of accumulating as near-duplicate rows.
  • Open-world and additive. Adding a fact never rewrites the others, and missing information means “unknown” rather than “false”, which is the epistemic state an agent is usually in. It can record what it learns without completing a schema first.
  • Typed literals. Values carry datatypes (numbers, dates, booleans), so downstream questions use comparisons and aggregations instead of string matching.
  • Standards and portability. The same graph is queryable with SPARQL and readable by standard RDF tooling, so an agent’s memory stays in a portable format.

SHACL constrains and corrects agent writes

Section titled “SHACL constrains and corrects agent writes”

An LLM will occasionally write something malformed, incomplete, or hallucinated. SHACL states the requirements for a write as an enforceable contract.

  • Shapes are a contract. Declare what a well-formed Ticket or Customer looks like: required properties, datatypes, value ranges, allowed values (sh:in), cardinality, uniqueness, property paths, closed nodes.
  • Validation runs before the write is stored. Validate an agent’s proposed write against the shapes. A write that does not conform is rejected before it reaches memory.
  • The report is machine-readable feedback. A conformance report names the focus node, the failing constraint, and a message, which is structured enough for the agent to repair its own output and retry.
propose fact ──▶ SHACL validate ──▶ conforms? ──yes──▶ commit
│ │
└─── report ◀── no ─┘
agent reads the violation
(node, constraint, message)
revises the fact ──▶ retry
  • Inference derives what you should not ask the model to re-derive. SHACL-AF rules run to a bounded fixpoint and entail new edges deterministically: transitive relationships, role derivations, classifications. Derived edges accumulate with the facts, from auditable rules instead of LLM reasoning that varies from run to run. Preview the derived edges before storing the rules.
  • Preview-then-publish keeps schema evolution safe. When an agent needs to change the shapes themselves, the gated preview/publish flow (digest plus compatibility verdict) stops a careless change from silently invalidating existing memory.

With both standards, an agent writes cleaner, self-consistent knowledge, and every claim carries provenance and a temporal history.

You want… Use
Counts/aggregates from app code, typed attributes, no RDF Structured query / filterByAttributes
Standard SPARQL, path patterns, external tools SPARQL 1.1 text / /sparql
Validation, shape selection, property paths, inference SHACL
Facts derived from what you wrote Reasoning & inference