Browse documentation
Docs/Language

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…

Watch it in Grid

See the concept in Grid.

Use the film for the product interaction and this article for the complete contract and reference detail.

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.