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 | Portal |
|---|---|---|---|---|
Declare Memory.schema |
updateMemory(schema:) |
— | memory set --schema |
Memory Settings → Property schema |
Write Node.objectType / properties |
createNode / updateNode |
hadron_create_node / hadron_update_node |
node add / node update --object-type / --properties |
Node create/edit → Object type + Properties |
Filter by objectType |
filter.objectType |
hadron_find_nodes objectType |
search / node ls --object-type |
Node browser → Object type |
where predicate |
findNodes filter.where |
hadron_find_nodes where |
search / node ls --where |
Node browser → Filter (where, JSON) |
sortProperty |
findNodes sortProperty |
hadron_find_objects sort (one field, collections only); not hadron_find_nodes |
search / node ls --sort-property |
Node browser → Sort by property |
sort enum |
findNodes sort |
— | — | Node browser → Sort dropdown |
| Conformance audit | validateMemory |
hadron_validate |
memory validate --check schema |
— |
Key points to internalise:
whereis on GraphQLfindNodes, MCPhadron_find_nodes, and the CLI (--where).sortPropertyis on GraphQL, the CLI (--sort-property), and the portal. On MCP,hadron_find_nodesexposes neither it norsort, so ordering node results by a property means sorting client-side. The object-store toolhadron_find_objectsdoes sort a collection, with a single-fieldsortshorthand whose cast comes from the schema.- The portal covers the whole surface except the conformance audit: the
schema editor in memory Settings,
objectType/propertieson the node form, and the structured-query panel in the node browser. On an encrypted memory the browser disables thedatacolumn forwhere/sortProperty—datais ciphertext at rest, so there is nothing to compare. - The CLI covers the read surface (
--where/--object-type/--sort-property, hadron-cli#265) and the write surface (memory set --schema,node add/update--object-type/--properties, hadron-cli#268). - Schema definition is GraphQL, the portal, or the CLI (
memory set --schema).objectType/propertiescan be written from GraphQL, the portal, the MCP create/update tools, or the CLI. - The object store (
createObject/findObjectson GraphQL + MCP,hadron objecton the CLI) is a higher-level projection over all of this — flat{ id, type, ...fields }records instead of graph nodes. See Object store (sugar layer) below.
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).
Object store (sugar layer)¶
Everything above operates at the node level — you set objectType, write a
properties bag, and query with where. The object store is a thinner,
record-oriented projection over the same substrate: an object is a node with
an objectType, presented as a flat record { id, type, ...fields }. id and
type are the reserved envelope, the node's typed properties become
top-level fields, and loc / name are auto-derived and hidden.
It's the friendly path for typed records, exposed on every surface:
- GraphQL / MCP —
createObject/object/updateObject/deleteObject/findObjectsand theirhadron_*_objectMCP tools. See Object store API and Store and query records with the object store. - CLI —
hadron object create/get/update/find/delete. See Work with objects from the CLI.
The one semantic difference from the node layer worth internalising is on
update: object update shallow-merges its fields (patch wins, unmentioned
fields kept), whereas node update replaces the whole properties bag.
findObjects reuses the same where grammar (plus a match equality
shorthand and a single-field sort) as this page.
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.
On GraphQL, the CLI (--sort-property), and the portal's node
browser. MCP hadron_find_nodes takes no sort; MCP hadron_find_objects
takes a single-field sort shorthand ({ "fundingUsd": "desc" }) that
builds this sort with the cast taken from the schema.
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¶
validateMemory (GraphQL), hadron_validate (MCP) and hadron memory
validate (CLI) walk a memory and report, 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).
The findings list is capped at 1,000, before any filtering
The server keeps at most 1,000 findings across all checks and counts
the rest. hadron_validate keeps the full 1,000 and prints only those, as
"Found 1000 issue(s)", without mentioning the rest. validateMemory
returns 200 unless you pass limit (up to 1,000); it and the CLI
report the true totalFindings and say when the list was truncated. But
the cap applies before --check schema filters, so on a memory with many
records (each flagged sparse when it has no description, content or
abstract) a schema violation past the cap is not listed on any surface.
Related¶
- Structured vs. unstructured memory — the mental model and the database analogy.
- Object store API — the record-oriented projection
(
createObject/findObjects/ …) over this substrate. - 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.