Graph Authoring
Grid graphs are immutable, directed or undirected property multigraphs. They retain stable node and edge identity, property schemas, deterministic ordering, and explicit work bounds. A graph…
Graph Authoring
Grid graphs are immutable, directed or undirected property multigraphs. They retain stable node and edge identity, property schemas, deterministic ordering, and explicit work bounds. A graph is a named resident value rather than a workbook cell or spilled array.
Use graphs when topology is part of the problem: routes, dependencies, communities, flows, reachability, propagation, or relationships that cannot be represented honestly as independent table rows.
For ordered grammar-bound structure, see Grammars and Trees.
Declaring A Graph
For complete models, see dispatch routing and service failover. Dispatch combines capacity and closure inputs, permitted route algebra, an objective and optional/negative reachability. Failover previews an atomic connected rewrite, compares incident impact before and after, and keeps the original graph intact.
Use an exact graph named binding:
frame road_rows = READ_PARQUET("roads.parquet")
graph roads = GRAPH(
road_rows AS edges,
"road_id" AS edge_id,
"origin" AS from,
"destination" AS to,
["minutes", "capacity", "build_cost"] AS edge_properties,
TRUE AS directed
)
edges, edge_id, from, and to are required. The edge source may be a
Frame, finite range, or literal row array. Stable edge IDs are mandatory.
Optional nodes and node_id bindings add isolated nodes.
node_properties and edge_properties select retained properties; when they
are omitted, Grid retains non-binding columns.
Parallel edges and self-loops are supported. Construction rejects missing endpoints, duplicate edge IDs, invalid keys, and inconsistent rows.
Node identity is exact and type-sensitive. Strings and checked integers are valid node keys. Blanks, floating-point identity, and implicit text coercion are rejected.
Graph values must use unqualified named bindings. They cannot be assigned to a
cell, range, sheet-qualified target, input, output, default, lazy
assignment, or scheduled assignment.
GRAPH also accepts one native parse_tree source. This overload derives a
directed parent-to-child property Graph at runtime from the exact tree cell
revision:
tree = PARSE_RESULT_TREE(PARSE(source_text, parser))
graph syntax = GRAPH(tree)
Parse-tree Graphs use the common resident Graph algorithms and Predicate
projection path. Their node IDs are deterministic one-based preorder ordinals
within a tree revision; child order is the explicit ordinal edge property,
not adjacency iteration order. See
Grammars And Parsers for the
complete property and provenance schema.
Relating Model Symbols
MODEL.GRAPH contains ordinary formula dependencies plus an authored
relationship layer. Use relates to for a directed relationship and
connects to for one bidirectional logical relationship:
predicate shipment(qty: number)
factory = 1200 relates to warehouse via :shipment(12)
warehouse = 800 connects to retailer via :peer
frame downstream = MODEL.GRAPH.BFS(:factory)
A relationship following an assignment inherits that assignment target as its
source; it does not relate the assigned value. Relationships may also be
standalone statements for symbols declared elsewhere. They are static model
metadata: lazy, conditional, scheduled, default, or state assignment
lifecycle does not make the edge conditional. Repeating a source, target,
direction, and kind creates distinct parallel relationships with stable opaque
IDs.
relates to creates one directed edge. connects to creates a bidirectional
relationship whose two traversal arcs share one logical relationship ID.
Predicate declarations type relationship payloads in the same way they type
unary predicate payloads.
Typed Property Schemas
Use a closed schema when algorithms and property access should be checked before execution:
frame city_rows = READ_PARQUET("cities.parquet")
graph schema Roads
node kind City
edge kind Road: City -> City
node.kind: symbol
node.population: number
edge.kind: symbol
edge.minutes: number
edge.capacity: number
edge.open: boolean
END
graph roads: Roads = GRAPH(
city_rows AS nodes,
"city_id" AS node_id,
["kind", "population"] AS node_properties,
road_rows AS edges,
"road_id" AS edge_id,
"origin" AS from,
"destination" AS to,
["kind", "minutes", "capacity", "open"] AS edge_properties
)
Schema bodies close with END. Schema entries use node.<path>: <type> or edge.<path>: <type>. A trailing
? marks an optional property. Nested paths are allowed. Property types use
normal Grid tags, including semantic and dimensional tags whose values can be
represented as graph properties.
Named node and edge kinds constrain valid endpoints. A schema that declares
kinds must include total node.kind and edge.kind string or symbol
properties. Unknown kinds, missing required values, undeclared properties, and
invalid endpoint combinations are errors.
Selectors, objectives, patterns, comprehensions, rewrites, and fixed points are checked against the declared schema. Numeric objectives require non-optional numeric properties. Transformations that preserve source properties also preserve the source schema.
Bare graph bindings remain available when a closed schema is unnecessary.
Querying A Graph
The following extensions preserve the existing Graph and logical-path syntax. They evaluate reactively through the resident Graph owner; reading a result does not start a job or change the source graph.
Reactive views and property rewrites
View predicates may refer to scalar model inputs or derived scalar bindings:
input MinimumCapacity = 100
input TrafficFactor = 1.25
input SelectedZone = "central"
graph usable = roads DO
edge road where road.open = TRUE
edge road where road.capacity >= MinimumCapacity
END
graph congested = rewrite usable limit 10000:
match ()-[road:Road where road.zone = SelectedZone]->()
set road.minutes = road.minutes * TrafficFactor
Parameters settle at query entry. A changed input invalidates dependent graph results. A rewrite reads one source revision, computes all proposed edits, and publishes one new immutable graph. The source remains available unchanged.
The filter's DO opens its clause body, and END closes it. This is
a Graph-specific clause block, not a general expression DO block: its body
accepts only node … where … and edge … where … predicates.
Ordinary references such as copy = roads and existing GRAPH(...) producer
expressions are unchanged. A typed graph copy = roads still requires a
graph-producing expression, under the existing binding rules.
Older graph schema Roads { … } and graph selected = roads { … } forms remain
accepted. Source formatting preserves the authored delimiter form and comments;
source-free AST rendering emits END schemas and DO … END filters. Both
spellings have identical owning fields, schema checks, reactive behavior and
compiled graph. JSON-style object literals, such as {label: "central"}, keep their
braces. Do not mix { with END, or DO with }.
In matches, views and rewrite predicates, node.id and edge.id select structural
identity. Closed schemas need not declare an id property for these predicates.
To exclude an edge, use an identity predicate in a view or a bounded rewrite
with a matching delete, as in dispatch routing.
Other properties remain schema-checked, and identities do not become numeric
properties for path objectives.
set targets a matched node or edge property. Values accept scalar literals,
model scalar bindings, matched properties, parentheses and +, -, *, /.
All assignments read the original properties, including when another assignment
in the same rewrite changes them. Repeated identical writes to an entity/property
coalesce; different values for that same target reject the entire rewrite.
The limit counts distinct entity/property edits, not matched rows. All matching
rows must be checked within the operation's work budget.
Rich set expressions can also call checked pure helpers and use IF or
DEFAULT. They use the ordinary compiled callable evaluator. The existing scalar
property-arithmetic subset keeps its established numeric semantics, including
fractional model parameters. Row callables retain exact integer properties and
the ordinary compiled rules for mixing exact and floating-point values.
Composite topology rewrites
Composite rewrites can combine add node {id: ...}, add edge {id: ..., from: ..., to: ...}, delete edge binding, delete node binding cascade, and
merge node duplicate into keeper with property assignments. Every condition
and action reads the original snapshot. Identical proposals coalesce;
conflicting writes, missing endpoints, reached expression errors, and edit or
resource overflow reject the whole operation. The edit bound includes the
primitive edge removals or redirects required by a cascade or merge. These
selected row/composite contracts require Graph v10. Rewrites produce an
immutable derived graph; they do not grant permission to resize a fixed-shape
resident handle through the external patch API.
Direct identity assignment, effectful calls and non-total helpers are rejected. Missing operands, arithmetic overflow, nonnumeric arithmetic and division by zero fail atomically. Closed property schemas are preserved and checked on the resulting graph. Connected multi-edge rewrite matches are supported in Graph v6; group and path clauses remain query features.
Optional and negative groups
frame contacts = match organization limit 1000:
(person:Person)
optional (person)-[:WorksAt]->(company:Company)
without (person)-[:SuspendedBy]->(:Policy)
return person.id as person, company.name as employer
Each group starts at an earlier node binding. Its chain and comma-separated
branches form one correlated group. optional emits all matching extensions,
or one row with nulls for its new bindings when the whole group has no match.
without keeps a row only when no complete extension exists; its new bindings
remain private and cannot be projected or referenced by later clauses.
Later clauses may use earlier optional bindings. An absent binding remains absent and cannot be rebound by another group. A group anchored at an absent node has no matches. A required captured path anchored there removes that row. Output limits apply after all clauses. Exhausted work, cancellation or deadlines are errors; they never count as proof of absence.
Capturing bounded walks
path supply_route = Supplies THEN Transfers*
frame deliveries = match network limit 1000:
(origin where origin.id = "warehouse")
path journey from (origin) via supply_route to (destination) limit 8
return destination.id as destination, journey.hops as hops,
journey.nodes as nodes, journey.edges as edges
via uses the same route algebra as logical paths: THEN, OR, *,
REVERSE, parentheses and earlier lexical path aliases. A relation name tests
the edge's kind, case-insensitively. REVERSE reverses both direction and
sequence order. This is separate from the existing optimal path … in …
query: capture returns bounded walks in deterministic depth-first order.
The hop limit is a positive static integer. It bounds each walk, while the
outer match limit bounds final rows. Walks may revisit nodes and edges.
Parallel edge identities remain distinct; multiple alternatives recognizing
the same physical walk produce one row. A nullable route such as Link*
includes the zero-hop walk. There is no implicit simple-path policy.
The path binding exposes only .hops, .nodes and .edges; these produce
an integer and ordered lists. Endpoint patterns may have labels and predicates,
or reuse an earlier node binding. The default Graph iteration budget admits
at most 100 hops; the native contract has an absolute ceiling of 128 and always
respects any tighter host budget. Combined traversal nesting is capped at 256,
and an expanded route at 256 productions and depth 64. Work and scratch limits
also apply. Nested optional/negative groups are not supported.
Graph methods
Method form is preferred:
node_count = roads.NODE_COUNT()
out_degree = roads.OUT_DEGREE("Denver")
connected = roads.HAS_PATH("Denver", "Boulder")
frame fastest = roads.SHORTEST_PATH(
"Denver",
"Boulder",
"minutes" AS weight
)
frame ranks = roads.PAGE_RANK(
"capacity" AS weight,
0.85 AS damping,
1e-8 AS tolerance,
100 AS max_iterations
)
Scalar operations include NODE_COUNT, EDGE_COUNT, DENSITY, DEGREE,
IN_DEGREE, OUT_DEGREE, HAS_PATH, HAS_EULERIAN_PATH, and
TRIANGLE_COUNT.
Frame-producing operations include NODES, EDGES, NEIGHBORS,
IN_NEIGHBORS, BFS, DFS, SHORTEST_PATH, BELLMAN_FORD,
WEAK_COMPONENTS, STRONG_COMPONENTS, TOPOLOGICAL_ORDER, PAGE_RANK,
bounded DIAMETER, ARTICULATION_POINTS, BRIDGES,
CLOSENESS_CENTRALITY, BETWEENNESS_CENTRALITY, K_CORE,
CLUSTERING_COEFFICIENTS, and MAXIMUM_BIPARTITE_MATCHING.
Weighted algorithms name an edge property for each call. SHORTEST_PATH
rejects negative weights; use BELLMAN_FORD when negative weights are part of
the model. Iterative and all-sources operations require convergence or source
bounds where documented.
Projecting A Graph Into Predicate Knowledge
Use KNOWLEDGE GRAPH when a resident property graph should supply extensional
facts to Grid's Predicate engine:
graph knowledge = GRAPH(
relationship_rows AS edges,
"edge_id" AS edge_id,
"subject" AS from,
"object" AS to,
["relation", "confidence"] AS edge_properties,
TRUE AS directed
)
KNOWLEDGE GRAPH knowledge VIA edge.relation WITH PROPERTIES
predicate reaches is transitive
connected = MUST("origin" reaches "destination")
The selected text or symbol edge property names each open binary predicate.
WITH PROPERTIES also exposes typed graph_node, graph_edge,
graph_node_property, and graph_edge_property facts for ordinary Predicate
rules. The projection reads the current graph revision, preserves parallel-edge
identity and proof provenance, and never copies inferred closure back into the
source graph. See Predicates for query, evidence,
and WHY semantics.
This projection is distinct from the built-in views: MODEL.GRAPH describes
model dependencies and explicit model relationships, while
MODEL.PREDICATES describes authored Predicate state.
Declarative Paths
Use a first-class path when the intent is an objective rather than a specific
algorithm:
path fastest = path in roads from "Denver" to "Boulder"
minimize sum(edge.minutes)
limit 40 AS hops
path resilient = path in roads from "Denver" to "Boulder"
maximize min(edge.capacity)
path fewest_transfers = path in roads from origin to destination
minimize hops
Grid selects an appropriate plan from the graph properties, objective, and bounds. The selected plan may be inspected, but it is not part of source meaning.
Path values are bound to the source graph revision. Read scalar and tabular projections explicitly:
cost = fastest.cost
hops = fastest.hops
frame fastest_nodes = fastest.nodes
frame fastest_edges = fastest.edges
path.nodes contains (ordinal, node). path.edges contains (ordinal, edge, from, to) followed by retained edge properties. A missing path produces
blank scalar projections and empty Frames.
Partitions
Connectivity, core decomposition, and community detection produce the same partition value:
partition weak = partition nodes of roads by connectivity(weak AS direction)
partition cores = partition nodes of roads by core_number
partition core_four = partition nodes of roads by k_core(4 AS k)
partition regions = partition nodes of roads by community(
leiden AS method,
modularity(1 AS resolution) AS objective,
edge.capacity AS weight,
symmetrize(sum) AS direction,
0 AS seed,
all AS hierarchy,
50 AS max_passes,
16 AS max_levels,
0.000000001 AS tolerance
)
Leiden is the recommended community method. louvain is available as an
explicit compatibility choice. Directedness is never guessed: choose
directed, undirected, or an explicit symmetrization policy.
Partition projections are Frames:
frame membership = regions.members
frame summaries = regions.summary
Membership contains (node, partition). Summary contains (partition, size, first_node, label). IDs and rows use canonical deterministic order.
Bounded Pattern Matching
Use labels to abbreviate symbol-valued kind predicates:
frame transfers = match accounts limit 100:
(sender:Account)-[payment:Transfer where payment.amount > 10]->(recipient:Account)
return sender.id as sender, recipient.id as recipient, payment.amount as amount
(sender:Account) means (sender where sender.kind = :Account);
[payment:Transfer] means [payment where payment.kind = :Transfer].
Labels preserve the symbol's spelling and case. The same shorthand works for
incoming edges, repeated bindings and bounded rewrite patterns. It composes
with where; the label and additional predicates must all match.
Closed schemas check the existing kind property and its value type. Labels
are exact symbol comparisons, with no inheritance or implicit conversion.
Graphs with string-valued kinds use explicit predicates such as
where sender.kind = "Account". As with explicit equality, a kind with no
matching values yields no rows. Source formatting retains labels; source-free
owning admission retains the equivalent predicates. The current match, view,
rewrite and routed-path forms have source/owning/canonical-rendering coverage;
this is not a promise to render every unrelated module production.
See the complete typed pattern example.
Omit names for entities whose properties the query does not need:
frame advanced_courses = match curriculum limit 100:
(:Course)-[:Prerequisite]->(course:Course)
return course.id as course
frame courses = match curriculum limit 100:
(course:Course)
return course.id as course
() and [] match entities without a kind constraint. Each unnamed occurrence
has its own identity; it does not impose equality with another unnamed
occurrence. Bind an entity explicitly to filter its properties, project it,
require repeated identity, or delete its edge in a rewrite. The compiler's
__grid_graph_anon_ binding prefix is reserved.
A node-only pattern scans nodes, including isolated nodes, under the same mandatory output/work bounds. Results contain one row per match: different paths can project the same value more than once. Use ordinary Frame operations for aggregation or deduplication. See the executable course prerequisite model for isolated-node selection and a two-hop prerequisite join. Bounded captured walks support variable length through the route algebra described above. Unbounded traversal and disconnected pattern joins are not supported.
Graph v4 also admits model parameters, structural identity predicates, and comparisons between bound properties:
input SelectedAccount = "alice"
input MinimumAmount = 20
frame alternatives = match accounts limit 100:
(sender:Account where sender.id = SelectedAccount)
-[payment:Transfer where payment.amount > MinimumAmount and payment.amount > recipient.limit]->(recipient:Account),
(sender)-[other:Transfer where other.id != payment.id]->(alternative:Account)
return payment.id as payment, recipient.id as recipient, alternative.id as alternative
Parameters are bare model references and settle once at query ingress. The
result is reactive; reads start no jobs. Missing/non-scalar inputs fail the
query. Comparisons retain typed equality and ordering without string/number
coercion. Exact integers and floating-point numeric values compare numerically
without rounding the integer through a float. Start-node id = ... selects the native identity seek; symbol,
string and integer identities remain distinct. id is structural on both
nodes and edges and need not be declared as a schema property.
Graph EXPLAIN reports the current resident access and expansion choices,
including retained equality indexes and complete independent-star expansion.
Their combination is reported as node_equality_seek_connected_star_expand.
Complete connected base patterns with 2–12 edges and settled selector predicates
can also use cardinality-guided join reordering, reported as
reordered_connected_join. Cycles, branches and repeated bindings are supported.
The result retains authored canonical order and multiplicity; prefix queries,
rich predicates, groups, paths and queries exceeding planning budgets keep the
existing matcher. Counts refresh from the current source on every admission.
It never constructs an index or runs a query. If the required source or scalar
inputs are unavailable, selection is explicitly deferred with a reason. Rich
expression predicates retain the general matcher and its reached-error order.
A comma restarts expansion at an earlier named node, written as a bare
reference such as (sender). Each subsequent edge introduces or rejoins its
bindings as usual. This supports branching joins without an implicit Cartesian
product. Disconnected branches, anonymous branch starts, and labels/predicates
on the restart reference are rejected; put those filters on its declaration.
Legacy selector comparisons can reference later bindings and are checked on each complete match
before applying the output limit. Missing properties do not match. Existing
work, memory and output bounds still apply; different matches can yield equal
projected rows.
Row expressions and predicates
Inline where also accepts bounded ordinary Boolean expressions, checked pure
helpers, conditionals and DEFAULT. It can continue across lines inside its
node or edge delimiter. Computed return expressions require an explicit alias.
For example, with the named inputs and graph already declared:
qualifies(cost) = cost * Factor > MinimumCost
frame eligible = match network limit 2:
(sender)-[payment:Link where
qualifies(payment.cost)
AND (payment.id = "ab" OR payment.id = "ba")
]->(recipient)
require complete return payment.id AS id
Ordinary expression predicates run where their binding becomes available, before continuation and optional/negative existence decisions. They can read earlier bindings and ingress-frozen scalar inputs, but not future bindings or bindings private to a negative group. A path endpoint can read an earlier captured path; its own path becomes visible only after endpoint acceptance. Reached errors and resource refusal abort the operation. They are not final-row filters. The admitted pure-call subset is bounded; arbitrary callbacks and effectful or non-total helpers are refused.
See the executable reactive transfer example. Views also accept settled model parameters. Connected rewrite matches accept parameters and cross-binding comparisons, including multi-edge branches. Views filter one entity at a time; they do not introduce joined bindings.
Graph patterns always require an output bound:
frame alternatives = match roads limit 1000:
(a)-[first where first.open = TRUE]->(b)-[second where second.open = TRUE]->(c)
return a as origin, b as via, c as destination,
first.minutes as first_minutes, second.minutes as second_minutes
Node and edge bindings are statically scoped. Predicates use typed properties, and projections require unique aliases. Repeating a binding with the same role expresses identity equality:
frame mutual = match roads limit 1000:
(a)-[out]->(b)-[back]->(a)
return a.id as origin, b.id as peer
A binding cannot change between node and edge roles. Pattern output is a Frame; use ordinary relational operations afterward for deduplication, grouping, aggregation, ordering, or windows.
Complete Matches
Use require complete before return when downstream calculations need all
matches rather than a bounded prefix:
frame payments = match transfers limit 100000:
()-[payment:Transfer]->()
require complete
return payment.amount AS amount
total = SELECT SUM(amount) AS amount FROM payments
The limit still bounds returned rows. Exactly that many matches succeed only
after the search establishes exhaustion. A further match fails with
GRID_GRAPH_MATCH_INCOMPLETE, before publishing any rows; no extra output row
is allocated. Duplicate projected values still count as distinct matches.
Optional/negative clauses, captured walks and reusable queries retain this
policy. Work, scratch, cancellation and deadline failures remain errors even
if the search has already found the expected number of rows. Empty complete
results succeed. Graph dialect v7 carries the assertion; older contracts retain
prefix behavior when the field is absent.
Comprehensions And Rewrites
A graph comprehension creates a property-preserving filtered graph:
graph active: Roads = roads DO
node city where city.population > 0
edge road where road.open = TRUE
END
An immutable rewrite applies a bounded set of graph edits and publishes either
the complete revised graph or an error. Use delete to select one edge binding
from a connected match, or set to assign matched node/edge properties. This
example deletes edges from a one-edge match:
graph normalized: Roads = rewrite roads limit 100000:
match (a)-[road where road.open = FALSE]->(b)
delete road
For delete, limit bounds distinct stable edge IDs; for set, it bounds
distinct entity/property edits. Both support connected multi-edge matches and
scan all matching rows to establish the complete edit set. Exceeding the edit
limit or finding conflicting assignments fails without publishing a partial
graph. Optional/negative groups and captured paths remain query-only; mixed
set/delete actions and node/edge creation are not authorable.
Bounded Fixed Points
Use fixpoint for repeated propagation over graph topology:
frame minutes_from_origin = fixpoint in roads:
using min_plus(edge.minutes AS weight)
along AS direction
1000000000000 AS default
0 AS seed origin
until max_delta < 0.000000001
within 100 iterations
Available operators are sum_product, min_plus, max_min, and
boolean_or. Every fixed point requires at least one seed and a positive
literal iteration bound. Direction, defaults, seeds, convergence, and numeric
requirements are validated before execution.
Transformations
Graph-producing operations must be rebound as graphs:
graph reverse_roads = roads.REVERSE()
graph least_cost_network = roads.MINIMUM_SPANNING_TREE("build_cost" AS weight)
graph regional = roads.INDUCED_SUBGRAPH(region_node_ids)
graph quotient = contract roads by regions
Each result is an immutable graph revision. Contraction turns each partition into a node, removes internal edges, and preserves cross-partition edge IDs and properties. The partition must belong to the exact source revision.
Typed Domain Projections
Host APIs can project one exact resident graph revision into domain-owned Geo routing, min-cost-flow, and simulation validators and kernels. Each projection uses an explicit typed schema, rejects missing or incompatible properties, and retains the source graph handle, revision, and fingerprint with its result. There is no untyped intermediate representation and no author-side cast syntax; the host operation defines the required node and edge properties.
Use a closed graph schema when the same graph will cross a domain boundary. That lets Grid reject incompatible topology or property values during graph construction, before a host requests the projection.
Resident Updates And Durable Patches
Graph declarations and graph-producing expressions remain immutable language values. A hosting surface may apply an explicit atomic patch to a primary resident GraphHandle; derived handles are read-only and are invalidated when their primary source changes. Predicate projections and dependent graph results then recompute from the new exact revision.
Accepted hosted patches append to the model-local durable patch journal. Optimistic revision guards and optional idempotency keys make retries deterministic, while an invalid batch leaves the prior snapshot unchanged. This is a host/API mutation surface, not graph formula syntax. The model API defines the patch shapes, supported topology changes, persistence, and recovery contracts.
Results, Ordering, And Bounds
Graph values do not spill into workbook cells. Scalar projections may be bound to cells or names; tabular projections are Frames and require an explicit conversion when workbook cells are needed.
Graph operations use canonical type-sensitive node order, stable edge IDs, and source order as deterministic tie-breakers. Results do not depend on insertion order or worker timing.
Operations enforce limits on nodes, edges, resident size, result rows, work,
iterations, temporary memory, deadlines, and cancellation. Approximate
all-sources operations report or retain the author-supplied source bound.
match … limit N intentionally returns at most the first N matching rows in
canonical traversal order, after clause filtering. It does not certify that
all matches were returned: a downstream aggregate covers that prefix. Add
require complete to reject incomplete enumeration. Work, scratch, deadline and
cancellation failures return errors, including during absence checks; they are
not converted into a successful prefix or evidence of absence. Rewrite edit
limits reject an oversized edit set atomically rather than truncating it.
DENSITY measures the simple loop-free projection: parallel edges count once
and self-loops do not count. Graphs with fewer than two nodes report zero.
For exact signatures, use the functions.md catalog. For
querying Graph-produced Frames, see
relational-authoring.md. For the complete feature
status, see features.md.
Optional paths and permitted routes
A captured path can use the same optional or without modifier as a fixed
pattern. The hop limit remains mandatory and bounds the search, including cycles.
path permitted = Road THEN (Road OR Ferry)*
frame reachable = match network limit 100:
(origin:Depot)
optional path trip from (origin) via permitted to (destination:Shop) limit 12
without path blocked from (origin) via ClosedBy to (restriction) limit 2
return origin.id, destination.id, trip.hops, trip.edges
An optional path emits one row with null endpoint/path projections when no walk
matches. Every matching walk otherwise contributes a row, subject to the result
limit. Existing endpoint bindings remain correlated: an earlier null binding
cannot be rebound by a later clause. A negative path keeps the outer row only
when no matching walk exists within its hop bound. Its endpoint and path names
are private. A nullable route such as Road* can match zero edges; that is a
witness for without, too. Cancellation, deadlines and exhausted work or memory
budgets produce errors; they never establish absence.
Use via on an existing optimal-path declaration to restrict eligible routes:
path fastest = path in network from Origin to Destination
via permitted
minimize sum(edge.minutes)
limit 12 AS hops
TravelMinutes = fastest.cost
frame itinerary = fastest.edges
The route uses existing THEN, OR, *, REVERSE and lexical path aliases.
Its relation atoms match edge kind strings or symbols without case sensitivity.
The planner optimizes over location, route progress and exact hop count. It does
not enumerate all captured walks and sort them. Existing objectives (minimize hops, minimize sum(edge.property), maximize min(edge.property)) and resident
path projections retain their meanings. Routed optimization requires a positive
limit of at most 128 hops, additionally bounded by the operation's iteration
budget (100 by default). Nonnegative edge weights, finite objectives, cancellation and scratch
budgets remain enforced. Equal results are deterministic; fewer hops win equal
objective scores. An accepted empty route at identical endpoints returns the
existing zero-hop, zero-cost path.
Multi-edge immutable rewrites
A rewrite can match a connected chain or branch and update properties on any matched node or edge:
graph adjusted = rewrite network limit 100:
match (origin)-[road:Road]->(hub)-[ferry:Ferry]->(destination)
set road.minutes = road.minutes + ferry.minutes
The right-hand sides read the original graph. The runtime examines every match
before publishing a new graph, coalesces identical edits and refuses conflicting
values for the same property. limit bounds distinct edits; it never truncates
matching rows and thereby hides a conflict. delete road removes a matched edge
with the same all-match and distinct-edit guarantees. The legacy Graph v6 subset contains either set actions or one edge delete.
Graph v10 admits the mixed actions described under
composite topology rewrites. Neither form admits
optional/negative/path clauses. Missing properties, arithmetic errors, schema failures and budget
exhaustion leave the source graph intact.
See the complete composition example. These additions require Graph dialect v6; existing fixed patterns and ordinary path declarations continue through their existing kernels.
Reusable graph queries
An ordinary function declaration can own a bounded match body. Call it in a
named frame assignment, using the same graph and path forms as an inline match:
path upstream = REVERSE(DependsOn)*
impact(g, incident) = match g limit 100:
(failed where failed.id = incident)
path chain from (failed) via upstream to (service:Service) limit 8
optional path ownership from (service) via OwnedBy to (team:Team) limit 1
without path suppression from (service) via SuppressedBy to (policy:Policy) limit 1
return service.id AS affected, team.name AS owner, chain.hops AS distance
frame current_impact = impact(current, IncidentService)
frame preview_impact = impact(preview, IncidentService)
The declaration retains one query template. Each call expands during compilation
into the existing Graph PatternMatch operation and publishes a Frame. There is
no runtime query lookup. Graph arguments name existing graph bindings, including
views and immutable rewrites. Scalar arguments may also be deterministic pure expressions, including arithmetic,
record fields, conditionals and ordinary pure helper calls. The compiler binds
each computed argument occurrence to a private model value once per call site;
row predicates and projections read that value rather than repeat its expression
for every match. The ordinary cell dependency/invalidation rules govern evaluation.
Scalar input changes and graph revisions invalidate
the same resident dependencies as an inline match.
For example, these calls no longer need a temporary scalar assignment:
frame selected = impact(current, selection.id)
frame adjusted = expensive_links(network, base_cost + surcharge)
The graph arguments (current, network) must still name graph bindings.
Computed expressions with volatile or effectful calls, including calls through
imported helpers, are rejected. Name an explicitly managed value first if that
is the intended model behavior. Computed values still need to satisfy the
query's scalar operand contract; this syntax does not convert arrays or records
into scalar keys. Errors retain ordinary expression semantics. Owning artifacts
and formatting retain the authored argument expression, not the private name.
Pattern and path bindings stay local to each query. Parameters cannot reuse their
names. limit values remain static literals, and the resulting graph schema checks
selectors, kinds and property types at each call. Optional outputs remain nullable;
negative clauses export nothing. Definitions may follow their calls. USE imports
retain the definition's expanded named routes; a same-named caller route cannot
replace them. The existing restriction on imported model-value captures applies;
pass those values explicitly.
Query calls require a direct named frame assignment. Recursive query calls,
query-valued callbacks, nested query expressions and dynamic query bounds are not
admitted. Total expansion is bounded before copying templates: 65,536 conservative
node units and 4 MiB of estimated derived storage per module. These admission limits
are separate from the authored match and hop limits.
See the complete reusable query example.
Graph views now accept structural node.id and edge.id comparisons, consistent
with matches and rewrites, including closed schemas. An empty typed graph remains
valid when a view removes every row; rewriting it preserves the schema and returns
an empty result.
Traversal policies
Local validation and bounded performance checks passed.
Use walk AS traversal (the default), trail AS traversal (no repeated stable
edge identity), or simple AS traversal (no repeated original node identity):
path fastest = path in network from Origin to Destination
via permitted
minimize sum(edge.minutes)
simple AS traversal
limit 12 AS hops
frame routes = match network limit 1000:
(origin where origin.id = Origin)
path trip from (origin) via permitted to (destination)
trail AS traversal limit 12
return destination.id AS destination, trip.edges.minutes AS minutes
Identity-constrained optimal paths require an explicit hop bound from 1 through 128, also within the operation's iteration budget. Captured paths keep their existing required bound. Reverse traversal uses the same original edge identity; parallel edges remain distinct. Simple traversal cannot return to its starting node after a nonempty route. An accepted empty route keeps its existing meaning.
trip.edges.minutes returns an ordered list of native property values, retaining
repeated edges and missing values as null. trip.edges still returns edge IDs;
trip.edges.id is the same ordered identity projection. Bounded computed columns
can reduce these values with ordinary pure expressions, such as
SUM(trip.edges.minutes) AS minutes. Missing optional paths supply BLANK to
expressions. Collection operations keep their ordinary compiled semantics: LEN
maps over an array; use a collection reduction such as COUNT for numeric
element counts.
Walk keeps the established optimizer and tie policy. Trail/simple optimal paths
retain visited identities in every search label and order equal-cost answers by
hop count, then canonical neighbor/edge sequences. Work, cancellation and memory
failures refuse the result. Graph v8 carries the new policy; absent policy fields
in older artifacts retain walk behavior. Ranked plural paths authoring uses the separate Frame contract below.
Ranked paths
Local validation and bounded performance checks passed with the traversal-policy work. They do not establish universal route-scale bounds.
frame alternatives = paths in network from Origin to Destination
via permitted
minimize sum(edge.minutes)
simple AS traversal
limit 12 AS hops
limit 3 AS paths
Both bounds are mandatory. Each row has rank (starting at 1), cost, hops,
and ordered nodes/edges identity lists. The objectives are the existing
minimize hops, minimize sum(edge.<property>), and
maximize min(edge.<property>). The policy can be walk, trail or simple.
The result contains the best requested routes, or fewer after proving exhaustion. Hitting a work, memory or cancellation limit fails the whole operation; a partial answer is never published as a ranked result. Different route alternatives that recognize the same physical edge sequence do not duplicate it. Parallel edges with different stable identities remain distinct alternatives.
For walk, rank 1 retains the singular planner's existing tie winner. Remaining equal-cost routes use hop count followed by canonical neighbor/edge sequences. Trail/simple use that canonical ordering throughout. Empty-route acceptance retains the singular contract. The selected search reserves bounded work for compatibility selection and ordered expansion inside one operation ceiling.
Dependency analysis source library
Validation pending: these new source recipes pass parser/import admission, but their added end-to-end LIR tests have not run successfully. The shared native cache changed during validation and no coherent Host dependency set remains identified for this check. Treat this section as a proposed recipe until the acceptance receipt records a passing run.
Supply dependency_queries.gs to a model's source resolver to reuse a bounded incident query:
USE "shared/dependency_queries.gs" AS dependencies
frame current_impact = dependencies.impact(network, Incident)
frame preview_impact = dependencies.impact(preview, Incident)
The complete example
includes a typed service graph and a preview that removes one dependency. The
query follows reverse DependsOn edges, includes the incident itself (zero hops),
returns nullable OwnedBy team names, and excludes services with a SuppressedBy
policy. A missing incident yields no rows. Multiple owners or routes can produce
multiple rows; this is a bounded path query, not a deduplicated service set or
an assertion of complete reachability. The library fixes the match limit at 100
and hop limit at 8; callers needing another policy should author another bounded
query definition rather than assume those limits are exhaustive.
Imports retain the definition's route. Both graph bindings and scalar inputs are explicit arguments. Results use the existing Frame owner and dependency invalidation; querying a preview does not edit the base graph. Graph arguments retain the direct named-binding restriction; scalar arguments may use the pure expressions described above. The library uses the shared computed-scalar argument syntax but adds no query-valued callback and is not compiler-embedded or Registry-published.