Give a memory a structured schema¶
Make a memory hold records of a fixed shape. For example, every competitor has a name, a funding stage from a list you choose, and an amount raised. Once the shape is set, Hadron refuses any record that doesn't fit, whether a person, an agent or a script wrote it. You set it up once, in the portal, with no terminal.
By default a memory is unstructured: notes are free-form, and you find them by meaning and by keyword. A schema is for a memory that holds many records of the same shape, such as competitors, invoices or incidents. It names one or more collections and gives each a set of typed fields. For the model behind it, see Structured vs. unstructured memory.
Before you start¶
- Write access to the memory. You need to be an Admin or Contributor on the memory's organization.
- Who can save a schema. An agent connected to Hadron over MCP, such as
Claude Desktop or a chat app, can draft a schema and read one, but it
can't save one. You save it in the portal. A coding agent that has the
hadronCLI can also save it for you (see below).
Step 1: Have your agent draft the schema¶
A schema is a short piece of JSON. You don't have to write it yourself. Describe your records in plain words, and paste in the example shape the portal shows (Step 2), so your agent copies the format:
I'm setting up a Hadron memory to track competitors. The memory's Settings
page has a Property schema box that shows this example:
{
"objectTypes": {
"competitor": {
"fields": {
"name": { "type": "text", "required": true },
"stage": { "type": "enum", "values": ["seed", "series-a"] },
"firstSeen": { "type": "datetime" }
}
}
}
}
Field types are text, number, datetime, boolean, and enum (with values).
Write me the schema for a competitor: its name (always required), its funding
stage (seed, series A or series B), how much it has raised in dollars, the
date of its last round, and whether it's publicly traded. Give me only the
JSON to paste.
What you get back: one block of JSON, with one entry per field you described. The names it picks will vary; this is the shape:
{
"objectTypes": {
"competitor": {
"fields": {
"name": { "type": "text", "required": true },
"stage": { "type": "enum", "values": ["seed", "series-a", "series-b"] },
"fundingUsd": { "type": "number" },
"lastRoundAt": { "type": "datetime" },
"isPublic": { "type": "boolean" }
}
}
}
}
What to check before you paste it:
- The field names are the ones you want to keep. Every record, filter and
sort will use them, so settle them now:
fundingUsd, not a name you'll want to rename next month. - Each field has the right type. Amounts are
number, dates aredatetime, and yes/no answers areboolean. A field's type also decides how it compares and sorts later, so a number stored astextsorts"9000000"after"12000000". - Every list of choices is an
enumwith itsvalues. Anenumwith novaluesis refused.
Step 2: Save it in the portal¶
- Open the memory and choose its Settings tab.
- Find the Property schema section. The Schema (JSON) box shows the example shape from Step 1 in grey until you type into it.
- Paste your schema and choose Save schema.
The portal checks the schema before saving it. A malformed one never saves: the error appears under the box, for example "field "stage" (enum) requires a non-empty "values" array". Once the schema saves, the section shows Schema saved., with your collections listed under Collections.
The box takes only the JSON itself, starting at {. It does not take the
GraphQL version further down this page.
Two things worth knowing:
- Emptying the box removes the schema. Records in the collection stop being checked when they are written. That is a real downgrade, so the portal asks you to confirm before it clears an existing schema.
- Saving a schema doesn't touch existing records. It applies to writes from then on; see Step 4.
Step 3: Add records that fit¶
In the portal: open the memory, open the ⋯ menu (Memory actions) and choose Add node. Set Object type to your collection; the dropdown lists the collections your schema declares. The form then shows one typed input per field, with the required ones marked. A record that doesn't fit the schema is refused when you save it. The node's own fields are covered in Add nodes to a memory.
Through an agent: an agent connected over MCP can create and query records in the collection. See Store and query records with the object store. Your agent needs to know the collection's fields; the schema is shown when it lists your memories, so tell it that if it can't find them.
Either way, a record that breaks the schema is refused, not saved half-right.
That happens when it names a collection the schema doesn't declare, leaves out
a required field, or gives a field a value of the wrong type ("a lot" for a
number, or "pre-seed" for a stage). The refusal names the field:
Schema violation: objectType "competitor": field "stage" must be enum (one of: seed, series-a, series-b)
Step 4: Check records written before the schema¶
A schema never invalidates records that already exist. It checks only writes made after you save it, so turning a schema on is safe for a memory that already holds data. Nothing breaks.
To find older records that don't fit, ask your agent:
What you get back: a report of problems in the memory. Records that break the schema are listed as schema problems, each with the reason. Fix one by editing the record, in the portal or through your agent, so it supplies the missing or mistyped field.
The check stops at 1,000 problems
The check covers the whole memory, not only the schema. It also reports
other things, such as a record with no description, and a memory full of
small records produces a lot of those. It lists the first 1,000 problems
and stops there. Your agent's report doesn't say how many it left out. If
it says it found exactly 1,000, schema problems may be among the ones left
out, so treat a clean result on a large memory as unconfirmed. The CLI's
hadron memory validate <memory> --check schema does report the true
total, and says when the list was cut off. It can't see past the cutoff
either.
With the API or the CLI¶
Everything above can also be done with the API or the hadron CLI.
The schema's shape¶
A schema is a JSON object with one key, objectTypes, which maps each
collection name to its field definitions.
- Field types are
text,number,datetime,booleanandenum. Anenumfield must carry a non-emptyvaluesarray; the other types must not. required: trueforces a field on every write.- Names: collection and field names must match
[A-Za-z0-9_-]{1,64}.
A malformed schema is rejected as BAD_USER_INPUT, and nothing is saved until
it is well-formed. That covers an unknown key, an enum with no values, or a
bad name.
The declared type is the query cast
A field's type doubles as its cast for
querying and
sorting. Declare fundingUsd as number
and you query and sort it with as: number, with no second mapping to
maintain.
Strict collections¶
By default a record may carry extra properties the schema doesn't declare. Set
strict: true on a collection to reject any property key that isn't declared:
{
"objectTypes": {
"competitor": {
"strict": true,
"fields": { "name": { "type": "text", "required": true } }
}
}
}
With GraphQL¶
Set the schema with updateMemory(schema:). This is the GraphQL literal form,
with unquoted keys and bare enum identifiers, so it is not the JSON the portal
takes:
mutation DefineCompetitorSchema {
updateMemory(
id: "<memoryId>"
schema: {
objectTypes: {
competitor: {
description: "A company we track in the market."
fields: {
name: { type: text, required: true }
stage: { type: enum, values: ["seed", "series-a", "series-b"] }
fundingUsd: { type: number }
lastRoundAt: { type: datetime }
isPublic: { type: boolean }
}
}
}
}
) {
id
schema
}
}
With the CLI¶
hadron memory set takes the same JSON the portal does, either inline with
--schema or from a file with --schema-file. See
Install the hadron CLI and the
CLI reference.
Tagging a node with objectType from MCP¶
A node joins a collection when its objectType is set to the collection name.
That is separate from nodeType. nodeType is the node's retrieval role
(info, record and so on); objectType says which collection it's a record
in. A node with no objectType is an ordinary node in no collection.
objectType can be written from GraphQL, the portal, or the MCP create and
update tools:
{
"tool": "hadron_create_node",
"loc": "competitors:acme",
"name": "Acme Corp",
"objectType": "competitor",
"properties": {
"name": "Acme Corp",
"stage": "series-a",
"fundingUsd": 12000000,
"lastRoundAt": "2026-05-01T00:00:00Z",
"isPublic": false
}
}
The typed fields live in the properties bag. That is the JSONB the schema
governs and that where queries. A rejected write comes back as a
Schema violation: tool error on MCP, and as BAD_USER_INPUT on GraphQL.
The record check in Step 4 is hadron_validate on MCP
({ "memoryUrn": "acme.com:market" }), validateMemory on GraphQL, and
hadron memory validate acme.com:market --check schema on the CLI. See
the audit section.
Next steps¶
- Query nodes by their properties: pull out the records that match a condition.
- Sort results by a property value: order records
by a field such as
fundingUsd. - Build a market-research agent with structured memory: this schema in a full worked example.
- Structured vs. unstructured memory: when a schema is worth the constraint.