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.
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
| Position | Reads as |
|---|---|
WHERE | filter — is this row actually about it? |
ORDER BY | rerank — which fits best? |
RETURN | label — where does it sit on this dimension? |
via is vector, via:'keyword' lexical, via:'agentic-rag' a bounded judging loop. Three questions, one shape.ai 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.
| Bound | Default | What it protects |
|---|---|---|
maxAnchors | 200 | fan-out from an unpinned anchor |
maxFanoutTotal | 5 000 | materializing more than you meant |
cost: | per bucket | somebody else's rate limit |
LIMIT.cache: 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.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
MATCH (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.
| Refusal | What it means |
|---|---|
| Naked virtual scan | The label was matched bare, with no anchor to fetch it for. |
| Unbound anchor | The anchor exists but nothing narrows it — it would fetch for everyone. |
| Too many anchors | The probe bound more than maxAnchors (default 200). |
| Fan-out too large | Materialization would exceed maxFanoutTotal (default 5 000). |
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
| Function | Returns |
|---|---|
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 |
ORDER 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
size is a count of the rows, not a model's estimate.themes 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')
| Position | Call |
|---|---|
| filter | WHERE ai.relevant(n, '…') |
| rerank | ORDER BY ai.score(n, '…') DESC |
| label | RETURN ai.classify(n, '…') |
| steer a fetch | {ai: {hint: '…'}} on the edge |
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
'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.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 get | Because the realm declared |
|---|---|
| a reused fetch | cache: ttl — an answer up to N seconds old is fine |
| rows already in the graph | cache: graph — the fetch was kept |
| a re-fetch | nothing, or your fresh: true |
fresh 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
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
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 queries | this page, then the guide |
| authoring a realm | README.md — the declarative surface |
| settling an argument | the spec |
keyField, or no edge forms and every row vanishes. The commonest realm bug, and it looks like an empty source.Nothing matches that filter.