Skip to content

Query nodes by their properties

PortalCLIMCPAPIIntermediate~10 min

Pull out exactly the records that match a condition, and nothing else: every competitor at series A, every competitor that raised more than 10 million, and every worklog entry since Monday, oldest first. This is a filter on the records' fields, not a search by meaning, so nothing is missed because it was worded differently and nothing close-but-wrong slips in. Ask your agent, or use the filter in the portal. Neither needs a terminal.

It works best on a memory with a schema, where the records have declared fields. It also works on any node that carries properties.

Ask your agent

What you need: an agent connected to Hadron, such as Claude Desktop, Claude Code, Codex or Cursor (see Install Hadron in Claude Desktop and its siblings), and read access to the memory.

Say which memory, which records, and the condition, in plain words:

In acme.com:market, show me the competitors at series A or series B that
have raised more than 10 million dollars, largest first. Its schema is shown
when you list my memories.
In acme.com:market, which competitors had their last funding round on or
after 2026-06-01? Oldest first.

The last sentence of the first prompt matters. Your agent needs the collection's field names to build the filter. It can read them from the memory's schema, but agents don't always know to look there, so tell it.

What you get back: the matching records, with their fields, in the order you asked for.

What to check before you rely on it:

  • The filter used your fields. Ask the agent which fields and values it filtered on. A field name it guessed matches nothing, so the result is empty instead of wrong. That looks exactly like "there are none".
  • Counts are counted, not reported. Hadron does not give your agent a total yet, so an agent that says "there are 286" got there by paging through every result. Ask it how it counted, and whether it reached the last page.

In the portal

The memory's Nodes tab has a Properties filter, so you can explore a collection without writing anything by hand:

  • Object type is a dropdown of the memory's declared collections. On a memory with no schema, you type the collection name instead.
  • Filter (where, JSON) takes the condition as JSON, the same predicate described below. It is checked as you type: a non-object is refused with "Filter must be a JSON object." and unparseable text with "Filter is not valid JSON.", so a typo shows up before you run anything.

Picking an Object type also suggests the collection's declared field names for the filter and sort inputs. That is the quickest way to remember what you can filter on. Clear resets the panel.

Ordering the result is the neighbouring Sort by property control; see Sort results by a property value, including the encrypted-memory caveat on the data column.

If writing the JSON is the hard part, ask your agent to write it: describe the condition, name the fields, and ask for "a Hadron where predicate as JSON".

With the API: the where predicate

The rest of this page is the predicate itself. It is what your agent sends and what the portal's filter box takes.

The where predicate filters nodes by exact values in their properties or data JSONB. It is a structured WHERE clause over the graph. It works on GraphQL findNodes, on MCP hadron_find_nodes, and inside the object store's hadron_find_objects. It composes with keyword, vector, hybrid and regex retrieval, and with the no-query browse. For the full grammar, see Structured storage and queries.

The shape of a predicate

A predicate is either a branch or a leaf:

  • Branch — exactly one of and / or / not.
  • Leaf — a path (array of object keys into the column) plus exactly one operator (eq, ne, in, lt, lte, gt, gte, between, exists, contains).

Two optional leaf modifiers: field picks the column (properties, the default, or data), and as types the value before comparison (text, the default, or number / datetime / boolean).

1. A single eq leaf

Find nodes whose stage property equals series-a. On MCP:

{
  "tool": "hadron_find_nodes",
  "memoryUrn": "acme.com:market",
  "query": "*",
  "where": { "path": ["stage"], "eq": "series-a" }
}

On MCP, query is required — pass "*" (or "") for survey mode: no ranking, just the nodes the where predicate keeps. The GraphQL equivalent passes where inside filter and can omit the query entirely:

query SeriesA {
  findNodes(
    filter: {
      memoryIds: ["<memoryId>"]
      where: { path: ["stage"], eq: "series-a" }
    }
  ) {
    hits { node { loc name properties } }
    total
  }
}

With survey mode (query: "*") — or, on GraphQL, no query at all — this is an unscored browse: a filtered list, score null on each hit.

2. Combine leaves with and / or / not

Wrap leaves in a branch. Series-A or series-B competitors, excluding public ones:

{
  "and": [
    { "or": [
      { "path": ["stage"], "eq": "series-a" },
      { "path": ["stage"], "eq": "series-b" }
    ] },
    { "not": { "path": ["isPublic"], "as": "boolean", "eq": true } }
  ]
}

The or above is equivalent to a single in leaf, which is shorter:

{ "path": ["stage"], "in": ["series-a", "series-b"] }

3. Cast for numbers and dates

Values in JSONB compare as text by default — lexical, not numeric or chronological. For ordered comparisons on numbers and dates, set as.

Competitors that raised more than 10 million:

{ "path": ["fundingUsd"], "as": "number", "gt": 10000000 }

A temporal "in the last N days" query — last round on or after a cutoff you compute client-side (here, 90 days back):

{ "path": ["lastRoundAt"], "as": "datetime", "gte": "2026-04-19T00:00:00Z" }

number and datetime casts route through DB guard functions: a value that can't be parsed drops the row rather than raising an error. So a malformed lastRoundAt simply won't match — it never 500s the query.

4. Facet by collection with objectType

If the memory mixes collections, narrow to one with the objectType facet (alongside where, not inside it). On MCP it's a top-level argument:

{
  "tool": "hadron_find_nodes",
  "memoryUrn": "acme.com:market",
  "query": "*",
  "objectType": "competitor",
  "where": { "path": ["stage"], "eq": "series-a" }
}

On GraphQL it's filter.objectType:

findNodes(filter: {
  objectType: "competitor"
  where: { path: ["stage"], eq: "series-a" }
}) { hits { node { loc name } } }

5. Compose where with a search query

where filters, then the chosen mode ranks the survivors. Add a query and mode to combine structured filtering with keyword or semantic retrieval — "the most relevant series-A competitors to developer tooling":

{
  "tool": "hadron_find_nodes",
  "memoryUrn": "acme.com:market",
  "query": "developer tooling",
  "mode": "vector",
  "objectType": "competitor",
  "where": { "path": ["stage"], "eq": "series-a" }
}

How composition works by mode:

  • Keyword / regex / browse — the predicate is applied in-query.
  • Vector / hybrid — the predicate post-filters the ranked candidate set; the semantic rank order is preserved among survivors.

Caps and error behavior

The predicate is bounded: 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, or exceeding a cap — is rejected as BAD_USER_INPUT. (Unparseable values under a number/datetime cast are not errors; they just drop the row.)

Next steps