Section 7: match() and optional()

So far, every query you have written has been a single chain — one start node, one sequence of hops, one end node. match() and optional() extend this model significantly.

match() lets you combine multiple traversals into a single query, where the traversals share named nodes as binding points — finding the intersection of multiple path conditions.

optional() lets you extend a query with path elements that may or may not exist — returning results whether or not those elements are present.

match(): finding path intersections

The problem it solves

Suppose you have this graph: two A-B-C paths share a common B node, and B is also connected to X and Y nodes.

Graph showing A1→B1→C1 and A2→B2→C2 paths

You want to find only the C nodes that are connected via a B node that also has X→Y connections. You want C2, not C1.

A single traversal cannot express this. You would have to walk the entire path A→B→C→…​→X→Y, which may not be structurally possible, or run two separate queries and intersect the results in your script.

match() solves this directly.

The syntax

match(
  node(type='A').out().node(type='B', name='b').out().node(type='C', name='target'),
  node(type='X').out().node(name='b').out().node(type='Y')
)
Diagram showing both traversal paths and the shared b node highlighted as the intersection point

match() takes two or more complete traversal expressions, separated by commas, and returns only the result rows where all traversals are simultaneously satisfiable.

The critical element is the shared name=: both traversals reference name='b'. The query engine finds rows where the same physical node satisfies the B position in the first traversal and the middle position in the second traversal. Rows where that node is different in each traversal are excluded.

This is the graph query equivalent of an inner join on a shared value.

How the binding works

In the second traversal within the match(), notice that the middle node is written as just node(name='b') — with no type filter.

node(type='X').out().node(name='b').out().node(type='Y')

When a node already has a name= binding from an earlier traversal in the same match(), you do not need to re-specify its type. The engine already knows exactly which node b is — it was resolved in the first traversal. Repeating the type would be redundant. What matters is that the second traversal also finds a valid path through that already-identified node.

When would you use match()? The clearest real-world scenario: finding virtual networks whose routing zone also has a routing policy attached.

match(
  node('system', role=is_in(['leaf', 'access']), system_id=not_none(),
       deploy_mode=is_in(['deploy', 'drain']), name='system')
    .out('hosted_vn_instances')
    .node('vn_instance')
    .out('instantiates')
    .node('virtual_network', name='vn')
    .in_('member_vns')
    .node('security_zone', name='routing_zone')
    .out('policy')
    .node('routing_policy', name='routing_policy')
)

This is the DC - Virtual Networks with their routing policies predefined catalog query. It is a single chained traversal, not a multi-path intersection, but it demonstrates how match() wraps a complete traversal. The wrapper is necessary when using optional() alongside it, which you will see in the next section.

A genuine multi-path match example

The DC - All fabric interfaces query combines two traversals to find only leaf/spine links that have a confirmed remote system:

match(
  node('system', name='system', deploy_mode='deploy', role=is_in(['leaf', 'spine']))
    .out('hosted_interfaces')
    .node('interface', name='iface', if_name=not_none())
    .out('link')
    .node('link', link_type='ethernet', name='link')
    .in_('link')
    .node('interface', name='remote_iface')
    .in_('hosted_interfaces')
    .node('system', role=is_in(['spine', 'access', 'superspine']), deploy_mode='deploy',
          name='remote_system')
)

This single match() traversal walks the complete link path and names five nodes — system, iface, link, remote_iface, remote_system — all in a single result row. Every result is a fully qualified link from a deployed leaf or spine to a deployed spine, access, or superspine.

optional(): conditional path elements

The problem it solves

When you write a traversal A.out().B.out().C, every result must contain A, B, and C. If C does not exist for a given A-B pair, that pair is excluded entirely.

But sometimes you want: "find all A-B pairs, and if C also exists, include it — but don’t drop A-B pairs just because C isn’t there".

That is optional().

The motivating example: LAG and ESI topology

A server can be connected to the fabric in three ways:

  1. Single-homed: a single physical link from the server NIC to one leaf switch port.

  2. LAG-homed: multiple physical links from the server bonded into a port-channel, connecting to one leaf switch.

  3. ESI-homed: multiple physical links from the server bonded into a port-channel, connecting to two different leaf switches (an ESI pair).

In the graph, a single-homed server connection looks like:

system → interface → link → interface → system

A LAG-homed connection adds two levels of abstraction:

system → interface(AE) → interface → link → interface(AE) → system

An ESI-homed connection adds a redundancy_group node as well.

If you write a traversal that assumes the full ESI path, you will miss all single-homed and LAG-homed connections. optional() lets you find the base path and capture the extra nodes if they exist.

The syntax

match(
  node(type='A').out().node(type='B').out().node(type='C', name='t1'),
  optional(
    node(name='t1').out().node(type='D', name='t2')
  )
)
Diagram showing A→B→C as the required path (t1). D is shown as an optional extension that may or may not be present. Results include C regardless of whether D exists.

The optional() block starts from a named node that was already found in the required traversal (name='t1'). It tries to extend the path from that node. If the extension exists, the result includes t2. If it does not exist, the result still includes t1 — it is not dropped.

Nesting optional blocks

Optional blocks can be nested to handle multiple levels of conditional depth:

match(
  node(type='A').out().node(type='B').out().node(type='C', name='t1'),
  optional(
    node(name='t1').out().node(type='D', name='t2'),
    optional(
      node(name='t2').out().node(type='E', name='t3')
    )
  )
)
Diagram showing the three-level optional chain: t1 always present

This reads:

  • Always find C (label it t1).

  • If D exists beyond C, include it (label it t2).

  • If E exists beyond D, include it as well (label it t3).

In the API response, a result for a simple connection contains only t1. A result for a LAG connection contains t1 and t2. A result for an ESI connection contains t1, t2, and t3.

When would you use nested optional? The ESI link discovery use case above is the canonical example. You are walking the connection topology of servers in the fabric and need to handle all three attachment types in a single query, producing a result that tells you which type each server uses.

The DC - All ESI links predefined catalog query brings together match(), multiple named nodes, a lambda, and a .where() cross-node comparison:

match(
  node('system', name='system', role=is_in(['leaf', 'access']), deploy_mode='deploy')
    .in_('composed_of_systems')
    .node('redundancy_group', name='esi_pair', rg_type='esi')
    .out('hosted_interfaces')
    .node('interface', name='virtual_pc', if_type='port_channel')
    .out('composed_of')
    .node('interface', if_type='port_channel')
    .out('composed_of')
    .node('interface', name='intf', if_name=not_none(), operation_state='up')
    .out('link')
    .node('link', name='link')
    .in_('link')
    .node('interface')
    .in_('hosted_interfaces')
    .node('system', name='remote_system', role=is_in(['leaf', 'access', 'generic']))
    .where(lambda system, remote_system: system.role != remote_system.role),
  node('system', name='system')
    .out('hosted_interfaces')
    .node('interface', name='intf', if_name=not_none())
)

Break this down:

  1. The first traversal inside match() walks: deployed leaf/access system → ESI redundancy group → virtual port-channel → member port-channel → physical interface → link → remote interface → remote system. The .where() lambda ensures the remote system has a different role from the local system — filtering out inter-leaf ESI links where both ends are the same role.

  2. The second traversal inside match() starts from the same system (shared name='system' binding) and simply confirms it also has hosted interfaces with assigned names.

  3. The two traversals share the name='system' and name='intf' bindings — only rows that satisfy both traversals simultaneously are returned.

This query is a good capstone for everything covered in this guide so far. Run it in the Graph Explorer and spend a few minutes reading through the results.

Checkpoint

Find all security zones that have at least one routing policy attached:

match(
  node(type='security_zone', name='routing_zone')
    .out('policy')
    .node(type='routing_policy', name='policy')
)

Find all deployed systems and their VN instances (if any exist):

match(
  node(type='system', role=is_in(['leaf', 'access']), deploy_mode='deploy', name='system'),
  optional(
    node(name='system')
      .out('hosted_vn_instances')
      .node(type='vn_instance', name='vn_instance')
  )
)

Confirm that systems with no VN instances still appear in the results — the result row just contains system with no vn_instance key.

Continue to Section 8: Practical Queries to see everything in this guide applied to real operational scenarios.