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.
- 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.nameNode 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
cnever 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:
optionalLabels: - DiffbotOrganization # realm-diffbot's; joined in by realm-odoo-enrichmentand 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 ultimateOwnerGuarantee — 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.

