Skip to content

views/ — saved queries, and the two shapes they come in

A realm ships named Cypher queries under views/*.yml. They appear in the console’s Views list and are runnable by name, which is most of what they are for. The part that is easy to miss is that one of the two shapes composes: its name is a label, and a larger query may match it.

views/key-accounts.yml
- name: KeyAccounts
description: Contacts worth more than £100k ARR, whoever owns them.
cypher: |
MATCH (me:AssistantUser)-[:HAS_HUBSPOT_OWNER]->()-[:OWNS_CONTACT]->(c:HubSpotContact)
WHERE c.arr > 100000
RETURN c
-- Used as a label. The view's MATCH clauses are inlined, and `c` is a HubSpotContact like any other.
MATCH (c:KeyAccounts) WHERE c.renewalDate < date('2026-12-01')
MATCH (c)-[:HAS_HUBSPOT_DEAL]->(d:HubSpotDeal)
RETURN c.email, d.name

Node view, or tabular view. The body decides, not a flag.

Body Used as
Node view MATCH … RETURN <one bare node variable> a label or by name. COMPOSES.
Tabular view a projection — RETURN c.email AS email, count(d) AS deals by name only. TERMINAL.

A node view yields nodes of exactly one type and keeps their identity, so the view’s name reads as a named subtype: KeyAccounts ⊆ HubSpotContact. A tabular view’s rows are not nodes, so matching it as a label cannot answer — and is refused at validation rather than quietly returning nothing.

The type may be VIRTUAL, and referencing the view materializes it. This is the composition worth knowing about: a node view whose body traverses a declared join to a virtual type is, as a label, a way to say “these fetched records” once and reuse it. MATCH (a:NdisDisabilityFilers) expands the view, materializes its AisReturns through their producer, and the rest of the query treats them as the §2 execution model always does — transient, rolled back with the read.

Two consequences follow from the fact that a reference is an INLINE of the body, not a subquery:

  • A view’s own variables are private. Only the returned variable becomes the caller’s alias; every other variable the body declares is renamed per use. Two uses of one view in a query never share its internals, and a caller’s c never meets the body’s.
  • The anchor rules still apply, through the view. A view does not grant a virtual label a bound anchor it would not otherwise have: what the body writes is what the engine sees. A body that pins its anchor composes the way the same query would written out longhand.
Field Required Description
name yes What it is run and matched by. A node view may NOT be named like a label the world already holds — matching that label anywhere, including inside other views, would silently come to mean “the nodes this view selects”.
cypher yes, unless goal The body. Standard Cypher, parsed by the real parser; there is no DEFINE VIEW statement and no subquery form.
goal no In place of cypher: the final read query only. The world plans the views and rules that reach it, and pins the compiled statement per world. A realm ships goal: alone — a shipped compiled: or plan: is a validation error. See Virtual Cypher §16.10.
description no What it answers, for the Views list and for a model choosing between views.
params no Declared bind values, each with a type (string, int, number, float, double, boolean, date), a description, and ideally a default so the view stays runnable bare. A param with no default must be supplied, unless it declares required: false: then an invocation may omit it and it is bound to null (so WHERE $p IS NULL OR x.k = $p skips the filter). An explicit null argument is accepted for any param — it takes the default when one is declared, and is bound as null otherwise. default: null alone does not make a param optional; declare required: false. One value each: a view needing a list reads it inside its own body rather than taking IN $p.
outputLabel inferred The type a node view’s rows are. Inferred when the body returns a single bare node variable, which is why most views never declare it. REQUIRED on a materialized view, where there is nothing to infer from until the body runs.
materialized no false (default) expands the body at query time, always fresh. true commits the result and reads the cache until the TTL expires — the interactive query then does no producer calls at all.
ttl no Cache lifetime for a materialized view (30m, 2h, 1d). Default 1h. Ignored when regular.

What is refused, rather than answered wrong: a tabular view matched as a label; a node view named like an existing label; an argument the view never reads; a fractional number for an int parameter; and a label in the body that is a near miss for one the realm knows (GitHubIsue beside GitHubIssue), reported at the view’s file and line.

A label nothing installed declares is reported. A view reading a label that no installed realm declares — as a type, a parent, a derived label, a join’s anchor, a projected: true type or another view’s output — is reported on its realm, which reads degraded, and a run of it carries an UNOWNED_LABEL warning: a plain MATCH over such a label can never return a row, and an empty answer from it means “not installed”, not “no data”.

Optional labels — reading a companion realm that may be absent

Section titled “Optional labels — reading a companion realm that may be absent”

Some views are built to answer on their own realm alone and to read MORE when a companion realm is installed: realm-odoo’s meeting briefing fills in each customer’s ultimate owner from Diffbot when realm-diffbot (and the bridge realm that joins it in) is present, and leaves that column empty when not. The realm says so in realm.yml:

realm.yml
optionalLabels:
- DiffbotOrganization # realm-diffbot's; joined in by realm-odoo-enrichment

and its views read the label only under OPTIONAL MATCH:

MATCH (b:OdooBook)-[:HAS_MEETING]->(m:OdooMeeting)-[:MEETING_WITH]->(c:OdooCustomer)
OPTIONAL MATCH (c)-[:HAS_DIFFBOT_ORG]->(d:DiffbotOrganization)
RETURN c.name AS customer, d.ultimateParentName AS ultimateOwner

Guarantee — an awaited label is a note, not a defect. While no installed realm declares it, the host reports the views as able to read more once a realm declaring the label is installed. That is a note: the realm stays active. A run of such a view carries OPTIONAL_LABEL_ABSENT in place of UNOWNED_LABEL, saying the rows are whole and the columns read through the label are absent, not known to be empty.

Guarantee — the declaration is checked against the Cypher. A label is honoured as optional only in a view that reads it nowhere but inside OPTIONAL MATCH clauses (their WHERE, and subqueries written there, included). A view that also reads it in a plain MATCH cannot answer without it, so it is reported as unowned, and the report says the declaration did not apply. A body the host cannot parse honours nothing.

Neither half is enough alone. An OPTIONAL MATCH without the declaration is still reported: whether an absent optional side is harmless is the author’s knowledge, not the query’s — OPTIONAL MATCH (a)-[:TRACKED_AS]->(k:CrmAccount) … WHERE crmRecords = 0 answers “every account is missing from the CRM” when CrmAccount is merely unowned. Declare a label optional only when its absence leaves columns empty and changes no row.