Skip to content

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 competitor collection 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 objectType faceting, a where predicate (stage + a datetime window), and sortProperty ordering.
  • 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 sortProperty are 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, strict collections, 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 a competitor object 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 composing where with 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:

{ "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 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 competitor collection with typed, validated fields (Memory.schema);
  • records captured with objectType, rejected at write time when malformed;
  • a structured retrieval combining objectType + where + sortProperty — 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 rows, retrieved by meaning and text as always.

Where to go next