Build a market-research agent with structured memory¶
A learn-by-doing walkthrough of Hadron's opt-in structured storage. You will stand up a market-research memory that holds competitor records as a typed collection, capture a few of them, then run the payoff query:
Series-A competitors funded in the last 90 days, sorted by funding descending.
By the end you'll have a memory that behaves like a tiny database — declared
columns, WHERE, ORDER BY, and a VALIDATE CONSTRAINT audit — without ever
leaving the graph.
For the mental model behind this, read Structured vs. unstructured memory first (optional). This tutorial cross-references the how-tos where you'd go next for depth.
What you'll build¶
- A
competitorcollection with typed fields:name(text, required),stage(enum),fundingUsd(number),lastRoundAt(datetime). - A handful of conforming competitor nodes captured by an agent.
- A structured retrieval query combining
objectTypefaceting, awherepredicate (stage+ a datetime window), andsortPropertyordering. - A conformance audit pass proving the collection is clean.
Prerequisites¶
- A Hadron memory you can write to, in an organization where you're Admin or
Contributor. A
knowledge-class memory is a good fit for a research corpus. If you're starting from scratch, Getting started sets up an org and an agent. - The Hadron MCP server connected to your coding agent (Claude Code, Cursor, …) so you can create nodes through conversation — see Add Hadron to Claude Code (OAuth).
- Access to the GraphQL API for the schema-definition and sorting steps
(steps 1 and 4). Schema definition and
sortPropertyare GraphQL-only; the MCP tools cover the capture and query steps. - 20–30 minutes.
Throughout, replace <memoryId> with your memory's ID and
acme.com:market with your memory's URN.
Step 1 — Declare the competitor schema¶
A schema declares the collections a memory holds and the typed fields each
carries. Define one objectType, competitor, via updateMemory:
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", "series-c"] }
fundingUsd: { type: number }
lastRoundAt: { type: datetime }
}
}
}
}
) {
id
schema
}
}
Each field's type doubles as its query cast later: fundingUsd is a
number, so you'll query and sort it with as: number; lastRoundAt is a
datetime. name is required, so every competitor must have one. From now
on, any node written with objectType: "competitor" is validated against this
shape.
For the full field-type table,
strictcollections, and the well-formedness rules, see Give a memory a structured schema.
Step 2 — Have the agent capture competitor records¶
Now capture a few competitors. Ask your coding agent to create them as nodes
in the memory, each with objectType: "competitor" and a properties bag
carrying the typed fields. For example:
Create these competitors in
acme.com:market, each as acompetitorobject type:
- Acme Corp — series-a, $12,000,000, last round 2026-05-01
- Globex — series-b, $40,000,000, last round 2026-02-10
- Initech — series-a, $8,000,000, last round 2026-06-20
- Umbrella — seed, $2,000,000, last round 2026-06-28
Under the hood each becomes a hadron_create_node call like:
{
"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"
}
}
Because the memory now has a schema, each write is validated: objectType
must name the competitor collection, name must be present, and each field
must coerce to its type. If the agent tries stage: "pre-seed" (outside the
enum) or omits name, the write is rejected with a Schema violation:
message — fix it and retry. This is the guardrail that keeps the collection
clean as it grows.
Step 3 — Run the payoff query¶
You want series-A competitors whose last round closed in the last 90 days.
With today at 2026-07-18, the cutoff is 2026-04-19. Combine the objectType
facet with a where predicate on MCP:
{
"tool": "hadron_find_nodes",
"memoryUrn": "acme.com:market",
"query": "*",
"objectType": "competitor",
"where": {
"and": [
{ "path": ["stage"], "eq": "series-a" },
{ "path": ["lastRoundAt"], "as": "datetime", "gte": "2026-04-19T00:00:00Z" }
]
}
}
(query: "*" is survey mode — the pure structured filter, no keyword
ranking. On MCP query is always required.)
Two records match: Acme (series-A, 2026-05-01) and Initech (series-A,
2026-06-20). Globex is series-B, so it's filtered out; Umbrella is seed. The
datetime cast makes the date comparison chronological rather than lexical; a
record with a missing or unparseable lastRoundAt simply drops out rather than
erroring. (Recompute the cutoff against your own "today" if you're following
along on a different date.)
For the full operator set (
in,between,exists,contains), the caps, and composingwherewith keyword or vector search, see Query nodes by their properties.
Step 4 — Sort by funding, descending¶
Ordering by a property is sortProperty, which is GraphQL-only. Run the
same filter through findNodes, ordered by fundingUsd descending:
query SeriesARecentByFunding {
findNodes(
filter: {
objectType: "competitor"
where: {
and: [
{ path: ["stage"], eq: "series-a" }
{ path: ["lastRoundAt"], as: datetime, gte: "2026-04-19T00:00:00Z" }
]
}
}
sortProperty: { path: ["fundingUsd"], as: number, direction: desc }
) {
hits { node { loc name properties } }
total
}
}
This returns Acme ($12M) ahead of Initech ($8M). The as: number cast is
essential — without it fundingUsd sorts as text (so "8000000" would land
after "12000000"). Any competitor missing fundingUsd sorts last,
regardless of direction.
This is the SQL sentence *"SELECT … WHERE stage = 'series-a' AND lastRoundAt
= … ORDER BY fundingUsd DESC"* — expressed over the graph. More detail in Sort results by a property value.
Step 5 — Audit conformance¶
Suppose some competitor nodes existed before you declared the schema in step 1 — imported from a spreadsheet, say. Schema enforcement is schema-on-write and non-retroactive: those older rows were never checked. The conformance audit finds them without changing anything:
hadron_validate walks the memory and reports any node whose
objectType/properties violate the schema (alongside its other checks —
broken edges, sparse nodes, stale abstracts). A clean run means every
competitor node conforms. If a legacy row shows up, fix it with an
hadron_update_node that supplies the missing or mistyped fields, then
re-run the audit.
What you built¶
You now have a memory that is structured on opt-in over one substrate:
- a
competitorcollection with typed, validated fields (Memory.schema); - records captured with
objectType, rejected at write time when malformed; - a structured retrieval combining
objectType+where+sortProperty— Hadron'sWHEREandORDER BY; - a conformance audit as the
VALIDATE CONSTRAINTbackstop.
The rest of the memory stays free-form: uncollected knowledge nodes coexist
with the typed competitor rows, retrieved by meaning and text as always.
Where to go next¶
- Structured storage and queries — the complete contract and the surface matrix (what's on GraphQL vs. MCP vs. CLI).
- Structured vs. unstructured memory — when to reach for a schema, and when not to.
- Give a memory a structured schema — strict collections, all field types, and the enforcement rules.