Skip to content

Build a market-research agent with structured memory

MCPAPICLIIntermediate~30 min

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 competitor collection with typed fields: name (text, required), stage (enum), fundingUsd (number), lastRoundAt (datetime).
  • A handful of conforming competitor objects captured by an agent.
  • The payoff query in one findObjects call — match (stage) + where (a datetime window) + sort (funding, descending).
  • 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 or the hadron CLI (memory set --schema) for step 1, which declares the schema — the one step the MCP tools don't cover. The capture, query, and audit steps all run over MCP.
  • 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, strict collections, and the well-formedness rules, see Give a memory a structured schema.

Step 2 — Capture competitor records as objects

Now capture a few competitors. The friendly way is the object store: each competitor is a flat record { name, stage, fundingUsd, lastRoundAt } in the competitor collection — no graph-node ceremony. Ask your coding agent to create them:

Create these competitors in acme.com:market as competitor objects:

  • 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_object call:

{
  "tool": "hadron_create_object",
  "memoryUrn": "acme.com:market",
  "type": "competitor",
  "fields": {
    "name": "Acme Corp",
    "stage": "series-a",
    "fundingUsd": 12000000,
    "lastRoundAt": "2026-05-01T00:00:00Z"
  },
  "key": "acme"
}

It returns the flat record { "id": "…", "type": "competitor", "name": "Acme Corp", … }. Because the memory now has a schema, each write is validated: name must be present, stage must be in the enum, and each field must coerce to its type. If the agent tries stage: "pre-seed" or omits name, the write is rejected — fix it and retry. This is the guardrail that keeps the collection clean as it grows.

An object is a node with an objectTypehadron_create_object is the record-shaped projection of hadron_create_node. See Object store API.

Step 3 — Run the payoff query

You want series-A competitors whose last round closed in the last 90 days, sorted by funding descending. With today at 2026-07-18, the cutoff is 2026-04-19. hadron_find_objects expresses the whole sentence in one call — match for the equality, where for the date window, sort for the order:

{
  "tool": "hadron_find_objects",
  "memoryUrn": "acme.com:market",
  "type": "competitor",
  "match": { "stage": "series-a" },
  "where": { "path": ["lastRoundAt"], "as": "datetime", "gte": "2026-04-19T00:00:00Z" },
  "sort": { "fundingUsd": "desc" }
}

It returns { objects, total } — here two objects: Acme ($12M, series-A, 2026-05-01) ahead of Initech ($8M, series-A, 2026-06-20). Globex is series-B, so match filters it out; Umbrella is seed. The datetime cast makes the date comparison chronological rather than lexical, and a record with a missing or unparseable lastRoundAt drops out rather than erroring. (Recompute the cutoff against your own "today" if you're following along on a different date.)

This is the SQL sentence "SELECT … WHERE stage = 'series-a' AND lastRoundAt >= … ORDER BY fundingUsd DESC" — over a memory. Note that sort works right here on MCP: unlike the node-level sortProperty (GraphQL/CLI only), the object store's findObjects carries ordering on every surface.

match is an equality shorthand; where is the full predicate (in, between, exists, …). For the underlying node-level grammar see Query nodes by their properties; for the object store, Object store API.

Step 4 — 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:

{ "tool": "hadron_validate", "memoryUrn": "acme.com:market" }

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 object conforms. If a legacy row shows up, fix it with an hadron_update_object that merges in the missing or mistyped fields, then re-run the audit.

What you built

You now have a memory that behaves like a small database, on one substrate:

  • a competitor collection with typed, validated fields (Memory.schema);
  • records created as objects (hadron_create_object), rejected at write time when malformed;
  • the payoff query — match + where + sort in one findObjects call — Hadron's WHERE and ORDER BY;
  • a conformance audit as the VALIDATE CONSTRAINT backstop.

The rest of the memory stays free-form: uncollected knowledge nodes coexist with the typed competitor records, retrieved by meaning and text as always.

Where to go next