Skip to content

Query nodes by their properties

The where predicate filters nodes by exact values in their properties or data JSONB — a structured WHERE clause over the graph. It works on both GraphQL findNodes and MCP hadron_find_nodes, and it composes with keyword, vector, hybrid, and regex retrieval as well as the no-query browse.

This guide builds up from a single leaf to a tree, adds typed casts, facets by collection, and composes where with a search query. 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