Virtual Cypher federated graph queries with the model inside · the spec is the authority

What is new here Three things ordinary Cypher does not do. Everything below is detail.

One MATCH spans stored and live

A virtual label is not in the database. It is fetched the moment the query reaches it, spliced in for the life of that query, and rolled back. You do not know which labels are which.

MATCH (me:AssistantUser)-[:EMAILED]->(p:Person)
MATCH (p)-[:HAS_GITHUB]->(g:GitHubIdentity)
MATCH (g)-[:RAISED]->(i:GitHubIssue)
RETURN p.name, i.title

Your graph, your CRM and a REST API in one pattern — no export, no sync, no mirror to go stale. Federation is the point; the rest of this page is how it is declared and paid for.

onceEach producer is called ONCE with every anchor key at a time — batched, never N+1.
goneMaterialization runs in a transaction that always rolls back. Nothing fetched persists.

The model is inside the query

Not a step before it or after it. A judgement no property and no embedding captures is written where the predicate goes, and the query keeps its shape.

MATCH (c:Concept {value:'Acme'})-[:RELEVANT_TO]->(d:Document)
WHERE ai.relevant(d, 'our funding round, not the sector')
RETURN d.title, ai.classify(d, 'sentiment')
ORDER BY ai.score(d, 'bears on renewal risk') DESC
PositionReads as
WHEREfilter — is this row actually about it?
ORDER BYrerank — which fits best?
RETURNlabel — where does it sit on this dimension?
edgeRetrieval mode is chosen AT the edge: no via is vector, via:'keyword' lexical, via:'agentic-rag' a bounded judging loop. Three questions, one shape.
neverai is a reserved engine primitive, never data. A realm cannot ship a property called ai.

A query can run for minutes

It is a fetch, not a scan. A cold join waits on somebody else's API, and an agentic-rag edge waits on a model loop. Tens of seconds is ordinary, and not a fault.

So it is bounded on every axis, and a bound that bites is always surfaced — never a silently short answer.

BoundDefaultWhat it protects
maxAnchors200fan-out from an unpinned anchor
maxFanoutTotal5 000materializing more than you meant
cost:per bucketsomebody else's rate limit
beforeRefusals happen BEFORE anything runs, and carry advice — pin the anchor, push a predicate, add a LIMIT.
cachecache: is the lever that makes the second ask fast. Declare freshness per producer; it is the difference between a live source and a slow one.
neverA zero-row result that meant "the source failed" — every producer failure is classified and returned as a warning.
Writing a query Ordinary Cypher, and why one gets refused.

The shape of a query

Ordinary Cypher. A virtual label is reached by traversing to it from something already bound.

MATCH (me:AssistantUser)
      -[:HAS_GITHUB]->(g:GitHubIdentity)
MATCH (g)-[:RAISED]->(i:GitHubIssue)
WHERE i.state = 'open'
RETURN i.title, i.repository
LIMIT 20
mustBind an anchor first — a real node, or one pinned by its identity.
neverMATCH (i:GitHubIssue) RETURN i — a naked virtual scan has nothing to fetch from.

Why a query is refused

Refusals happen before anything runs, and always say which alias is at fault.

RefusalWhat it means
Naked virtual scanThe label was matched bare, with no anchor to fetch it for.
Unbound anchorThe anchor exists but nothing narrows it — it would fetch for everyone.
Too many anchorsThe probe bound more than maxAnchors (default 200).
Fan-out too largeMaterialization would exceed maxFanoutTotal (default 5 000).
fixPin the anchor, push a predicate to the source, or add a LIMIT.
Reducing rows Turning many rows into the one thing you asked for.

Aggregations — a group → one cell

Neo4j's grouping supplies the group for free; the function reduces it. The model sibling of count().

MATCH (t:ResearchTopic {name:'RAG'})
      -[:HAS_NEWS]->(n:NewsItem)
RETURN t.name,
       summarize(n.description,
                 'what is newest') AS digest
FunctionReturns
summarize(text[, instruction])prose overview
synthesize(text, goal)prose, argued toward a goal
classify(text, 'a,b,c')one label from the set
extract(text, what)list of distinct things
themes(text[, focus][, n])recurring topics (labels only)
cluster(text[, k])groups with counted sizes + examples
score(text, rubric)one number 0–1
holds(text, claim)true / false / null
relevant(text, criterion)only the matching items
argmax(key, text, criterion)the winner
neverORDER BY score(...) — an aggregation is finalized after ordering, so it cannot be ordered by.

cluster — groups you can count

Returns {label, size, share, examples} per group. Its cost does not grow per batch the way the other reductions do, so it suits large groups.

MATCH (s:DiseaseScope {registryQuery:'sleep'})
      -[:HAS_TRIAL_SEARCH]->(r:TrialSearchRun)
MATCH (r)-[:RETURNED]->(t:ClinicalTrial)
RETURN cluster(t.title, 6) AS clusters
truesize is a count of the rows, not a model's estimate.
trueSame rows → same groups, every run.
pickthemes when membership doesn't matter; cluster when "how many" does.

Per-row judgement — the ai namespace

Judges rows the query already fetched. Reserved: never a stored property.

MATCH (p:Person)-[:AUTHORED]->(d:Document)
WHERE ai.relevant(d, 'a decision, not a status')
RETURN d.title, ai.classify(d, 'urgency')
PositionCall
filterWHERE ai.relevant(n, '…')
rerankORDER BY ai.score(n, '…') DESC
labelRETURN ai.classify(n, '…')
steer a fetch{ai: {hint: '…'}} on the edge
noteNon-deterministic and fail-open: a model error keeps rows rather than dropping them.
Narrowing and cost Asking the source for less, and asking it less often.

Your WHERE reaches the source

Write the filter you mean. Where the source can answer it, the fetch is narrowed there instead of after — so a filtered query costs less, not the same.

MATCH (s:DiseaseScope {registryQuery:'sleep'})
      -[:RETURNED]->(t:ClinicalTrial)
WHERE t.overallStatus = 'RECRUITING'
  AND 'OLDER_ADULT' IN t.ageGroups
RETURN t.title, t.phase
truePushdown changes cost, never rows. Anything the source cannot answer is still filtered here, so the same query returns the same answer either way.
care'X' IN n.list asks whether the record's list holds X. n.prop IN [...] asks whether its value is in a list you wrote. Different questions.
neverFetch everything and filter in your client. That is the one shape this engine is built to avoid.

Asking twice, and asking fresh

A realm declares how long an answer stays good. The second identical ask is usually free — which is why a slow first query is not a slow surface.

MATCH (c:Concept {value:'Acme'})
      -[:RELEVANT_TO {ai: {fresh: true}}]->(d:Doc)
RETURN d.title
You getBecause the realm declared
a reused fetchcache: ttl — an answer up to N seconds old is fine
rows already in the graphcache: graph — the fetch was kept
a re-fetchnothing, or your fresh: true
carefresh costs a real call every time. Reach for it when staleness would be wrong, not by habit.
Similarity When the join IS the resemblance.

Vector edges — similarity IS the join

The key is embeddable text rather than an id; the score rides on the edge.

MATCH (p:Person {name:'Ada Lovelace'})
      -[r:RELEVANT_TO]->(t:EmailThread)
RETURN t.subject, r.score
ORDER BY r.score DESC
careA minScore floor can return nothing when the anchor text is short. Prefer top-k + ORDER BY r.score.
What holds The promises that survive every path above.

What you are promised

trueNothing is mirrored: fetched rows live for the query and are rolled back.
trueA refusal happens before any source is called, and names the alias.
trueA capped or partial result says so — it is never silently truncated.
noteAnything model-backed (ai.*, reductions, keyTransform) is not bit-identical run to run, except cluster, which is.
Where these labels come from Not written in a query — declared in a realm. Shut by default; open it if you are authoring one.

A realm declares it; you traverse it

Nothing on this page needed you to know how GitHubIssue becomes reachable. That is the design: a realm author writes the declaration once, in YAML, and every query after it is ordinary Cypher.

MATCH (p:Person)
      -[:HAS_HUBSPOT_CONTACT]->(c:Contact)
RETURN c.company

That traversal works because a realm declared a virtual join — which anchor it hangs off, which field is the key on each side, and which producer fetches it — plus the producer's own paging, pushdown rules and cache.

If you are…Read
writing queriesthis page, then the guide
authoring a realmREADME.md — the declarative surface
settling an argumentthe spec
oneThe record's key must equal the anchor's keyField, or no edge forms and every row vanishes. The commonest realm bug, and it looks like an empty source.

Nothing matches that filter.