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
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:

  • where is on GraphQL findNodes and MCP hadron_find_nodes.
  • sortProperty and the sort enum 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. objectType can 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 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 presentobjectType 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).

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" }
  ]
}

NodePropertySortsortProperty

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 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

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).