Skip to content

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:

  • where is on GraphQL findNodes, MCP hadron_find_nodes, and the CLI (--where).
  • sortProperty is on GraphQL, the CLI (--sort-property), and the portal. On MCP, hadron_find_nodes exposes neither it nor sort, so ordering node results by a property means sorting client-side. The object-store tool hadron_find_objects does sort a collection, with a single-field sort shorthand whose cast comes from the schema.
  • The portal covers the whole surface except the conformance audit: the schema editor in memory Settings, objectType/properties on the node form, and the structured-query panel in the node browser. On an encrypted memory the browser disables the data column for where/sortProperty — data is 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 / properties can be written from GraphQL, the portal, the MCP create/update tools, or the CLI.
  • The object store (createObject / findObjects on GraphQL + MCP, hadron object on 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 fields map, an optional description, and an optional strict flag.
  • strict: true (default false) — 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; objectType is free-form.
  • objectType null/unset → not a collection member; no property check.
  • objectType set + schema present → objectType must name a declared collection; every required field must be present; every present field's value must coerce to its type; a strict collection rejects undeclared keys. A violation is rejected (BAD_USER_INPUT on GraphQL; a Schema 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:

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 path plus 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 as BAD_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 where leaf addressing (path / field / as).
  • number / datetime route through the same DB guards, so a missing or unparseable value sorts LAST regardless of direction; loc ascending breaks ties for stable pagination.
  • Overrides the sort enum 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 sort enum.

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.