Structured storage and queries¶
Opt-in structured storage turns a memory into a lightweight document store:
declare collections (objectType) with typed fields (Memory.schema),
then query them with a structured where predicate and order them with
sortProperty. It composes with every retrieval mode — keyword, vector,
hybrid, regex, and the no-query browse.
This page is the dry contract. For the mental model see Structured vs. unstructured memory; for recipes see the how-tos linked at the bottom.
Specs: cor:api:090:02 (filter /
where), cor:api:090:04 (sort / sortProperty), cor:dmo (Node / Memory
shape).
Surface matrix¶
Not every surface exposes every part. Check this before you build:
| Capability | GraphQL | MCP | CLI |
|---|---|---|---|
Declare Memory.schema |
updateMemory(schema:) |
— (portal / GraphQL) | — |
Write Node.objectType |
createNode / updateNode |
hadron_create_node / hadron_update_node |
— |
Filter by objectType |
filter.objectType |
hadron_find_nodes objectType |
— |
where predicate |
findNodes filter.where |
hadron_find_nodes where |
— |
sortProperty |
findNodes sortProperty |
— | — |
sort enum |
findNodes sort |
— | — |
| Conformance audit | — | hadron_validate |
— |
Key points to internalise:
whereis on GraphQLfindNodesand MCPhadron_find_nodes.sortPropertyand thesortenum are GraphQL-only — the MCP tool exposes neither. To order MCP results by a property you sort client-side.- The CLI has none of this yet (tracked in hadron-cli#265).
- Schema definition is GraphQL / portal.
objectTypecan be written from GraphQL, the portal, or the MCP create/update tools.
Memory.schema¶
A per-memory, opt-in JSONB column declaring the collections a memory holds
and the typed fields each carries. NULL means the memory is unstructured
(free-form objectType). Authored via GraphQL updateMemory(schema:) or the
portal; well-formedness is validated server-side (a malformed schema is
rejected as BAD_USER_INPUT).
Shape¶
{
"objectTypes": {
"competitor": {
"description": "A company we track in the market.",
"strict": false,
"fields": {
"name": { "type": "text", "required": true },
"stage": { "type": "enum", "values": ["seed", "series-a", "series-b"] },
"fundingUsd": { "type": "number" },
"lastRoundAt": { "type": "datetime" },
"isPublic": { "type": "boolean" }
}
}
}
}
objectTypes— a non-empty map of collection name → definition.- Each definition has a non-empty
fieldsmap, an optionaldescription, and an optionalstrictflag. strict: true(defaultfalse) — a node of this collection may carry only declared fields; an undeclared property key is a violation.
Field definition¶
| Key | Required | Meaning |
|---|---|---|
type |
yes | One of text, number, datetime, boolean, enum. |
required |
no | When true, the field must be present (non-null) on every write. Default false. |
values |
enum only | Non-empty array of allowed strings. Required for enum, forbidden for every other type. |
description |
no | Human note; not enforced. |
The type vocabulary is the where-cast set (text / number / datetime
/ boolean) plus enum. A field's declared type is its query cast — a
number field is queried and sorted with as: number.
Field-type coercion¶
Validation is coercion-based, matching the where-cast semantics so a value
that validates is also queryable:
| Type | Accepts |
|---|---|
text |
string, finite number, or boolean (all stringify). |
number |
a finite number, or a numeric string ("42", "0.9"). |
datetime |
a string parseable as a date (Date.parse). |
boolean |
a boolean, or the strings "true" / "false". |
enum |
a string in the field's values. |
Names and caps¶
- Collection and field names match
[A-Za-z0-9_-]{1,64}. - Bounds (defense-in-depth): ≤ 200 object types per schema, ≤ 200 fields per collection, ≤ 200 enum values per field.
Enforcement¶
Memory.schema governs writes as follows:
- No schema → no enforcement;
objectTypeis free-form. objectTypenull/unset → not a collection member; no property check.objectTypeset + schema present →objectTypemust name a declared collection; everyrequiredfield must be present; every present field's value must coerce to its type; astrictcollection rejects undeclared keys. A violation is rejected (BAD_USER_INPUTon GraphQL; aSchema violation:tool error on MCP).
Enforcement is schema-on-write and never retroactive — see the conformance audit for pre-existing rows.
Node.objectType¶
A collection discriminator: which domain object this node is (competitor,
insight). NULL for an ordinary node. Orthogonal to nodeType (which
governs retrieval role — see Node types). An empty or
whitespace-only objectType normalises to NULL (an ordinary node, not an
undeclared collection).
Written via createNode / updateNode (GraphQL) or hadron_create_node /
hadron_update_node (MCP). Filtered via filter.objectType (GraphQL) or the
objectType argument (MCP).
NodeWhereInput — the where grammar¶
A recursive structured predicate over a node's properties or data JSONB.
A node is either a branch or a leaf:
- Branch — exactly one of
and/or/not. and: [NodeWhereInput!],or: [NodeWhereInput!]— arrays of sub-predicates.not: NodeWhereInput— a single negated sub-predicate.- Leaf — a
pathplus exactly one operator.
Leaf addressing¶
| Field | Default | Meaning |
|---|---|---|
path: [String!] |
— | Object keys into the column, e.g. ["stage"] or ["round", "closedAt"]. |
field: NodeWhereColumn |
properties |
Which JSONB column — properties or data. |
as: NodeWhereCast |
text |
Value typing before comparison — text, number, datetime, boolean. |
number and datetime comparisons route through DB guard functions: an
unparseable value drops the row rather than erroring. Cast deliberately —
comparing a datetime as text compares strings lexically.
Leaf operators¶
Exactly one per leaf:
| Operator | Meaning |
|---|---|
eq |
equals |
ne |
not equals |
in: [JSON!] |
value in the given set |
lt, lte, gt, gte |
ordered comparison (respects as) |
between: [JSON!] |
two-element [low, high] range |
exists: Boolean |
the path is present (true) / absent (false) |
contains: JSON |
JSONB containment (@>) of the value at the path |
Caps and errors¶
- Depth ≤ 4, ≤ 32 leaves total, path ≤ 8 segments.
- A malformed or oversized tree (a branch with more than one of
and/or/not, a leaf with more than one operator, exceeding a cap) is rejected asBAD_USER_INPUT.
Composition with modes¶
where filters, then the chosen mode ranks the survivors:
- Lexical modes (keyword, regex) and the no-query browse — the predicate is applied in-query.
- Vector / hybrid — the predicate post-filters the ranked candidate set; rank order is preserved.
Example¶
Series-A competitors whose last round closed on or after a date:
{
"and": [
{ "path": ["stage"], "eq": "series-a" },
{ "path": ["lastRoundAt"], "as": "datetime", "gte": "2026-04-19T00:00:00Z" }
]
}
NodePropertySort — sortProperty¶
Order findNodes results by the value at a properties / data JSON path.
GraphQL-only.
sortProperty: {
path: ["fundingUsd"] # required — object-key path into the column
field: properties # optional, default properties
as: number # optional, default text
direction: desc # optional, default asc
}
- Reuses the
whereleaf addressing (path/field/as). number/datetimeroute through the same DB guards, so a missing or unparseable value sorts LAST regardless of direction;locascending breaks ties for stable pagination.- Overrides the
sortenum when present. - On vector / hybrid modes it re-orders the retrieved candidate window
(the ranking runs against the vector index, not the JSONB) — the same
caveat as the
sortenum.
Conformance audit¶
hadron_validate (MCP) walks a memory and reports, among other issues, any
node whose objectType / properties violate the memory's declared schema.
It is a non-destructive audit — it reports, never mutates. It exists
because write enforcement only governs new writes; the audit catches nodes
that reached a non-conforming state another way:
- written before the schema was defined or tightened, or
- written through a path outside the write seam.
Run it after adding or tightening a schema on a memory that already holds data. See MCP tools for the tool's other checks (broken edges, sparse nodes, stale abstracts, embedding failures).
Related¶
- Structured vs. unstructured memory — the mental model and the database analogy.
- Give a memory a structured schema
- Query nodes by their properties
- Sort results by a property value
- Node types — the orthogonal
nodeTypeaxis. - RAG vector index — the modes
wherecomposes with. - GraphQL API — the generated schema reference.