Section 5: Output Labels and Lambdas
In Section 2 you learned that name= labels a node for JSON output and that node(name='node') returns the full record for every node in the result.
This section builds on that foundation: how to name multiple nodes in a single traversal, how to use descriptive keys that make your code readable, and how to use .where(lambda …) for filtering logic that property matchers cannot express.
Naming multiple nodes in a traversal
Consider a query that walks from a leaf switch across a link to a remote interface and remote system.
When the results come back, you need to know which node is the local interface and which is the remote interface — they are both interface nodes.
node(type='system', role='leaf', name='local_system')
.out('hosted_interfaces')
.node(type='interface', if_name=not_none(), name='local_intf')
.out('link')
.node(type='link', name='fabric_link')
.in_('link')
.node(type='interface', name='remote_intf')
.in_('hosted_interfaces')
.node(type='system', name='remote_system')
Each name= value is a unique key in the result dictionary.
A single result item from this query looks like:
{
"local_system": { "label": "leaf001", "role": "leaf", ... },
"local_intf": { "if_name": "xe-0/0/0", "operation_state": "up", ... },
"fabric_link": { "link_type": "ethernet", "speed": "10G", ... },
"remote_intf": { "if_name": "xe-0/0/4", ... },
"remote_system": { "label": "spine001", "role": "spine", ... }
}
Each traversal hop is clearly labelled.
Your script accesses result['local_system']['label'] and result['remote_system']['label'] without any ambiguity.
When would you use multiple names?
Any time your traversal crosses a link — where both ends are interface nodes — you must name them differently if you need data from both sides.
The same applies to any traversal that visits the same node type more than once.
Nodes you do not name are not in the output
You only need to name the nodes you intend to read data from.
In the query above, if you only need the local_system and remote_system, remove the name= attributes from the interface and link nodes:
node(type='system', role='leaf', name='local_system')
.out('hosted_interfaces')
.node(type='interface', if_name=not_none())
.out('link')
.node(type='link')
.in_('link')
.node(type='interface')
.in_('hosted_interfaces')
.node(type='system', name='remote_system')
The traversal still walks through the interfaces and link — they constrain the path — but only local_system and remote_system appear in the JSON output.
.where() and lambdas
.where() appends a Python lambda expression to a query to apply filtering logic that property matchers cannot express.
It runs on the server side — results that do not satisfy the lambda are excluded before the response is sent.
The syntax is:
<query>.where(lambda named_node: <condition>)
The lambda receives a reference to a named node in the query.
This is why name= is required on any node you want to filter with a lambda.
Simple value comparison
node(type='vn_instance', name='x')
.where(lambda x: x.vlan_id >= 100 and x.vlan_id <= 199)
This is equivalent to node(type='vn_instance', vlan_id=_and(ge(100), le(199))).
The lambda form becomes useful for logic that cannot be expressed with the available property matchers.
Nested JSON access
The predefined query DC - All managed devices by ASIC model uses a lambda to reach inside a nested dictionary:
match(
node('system', name='system')
.out('interface_map')
.node('interface_map')
.out('device_profile')
.node('device_profile', name='DP')
.where(lambda DP: DP.hardware_capabilities['asic'] == 'PE')
)
The hardware_capabilities property of a device_profile node is a JSON object.
The lambda accesses it using standard Python dictionary notation: DP.hardware_capabilities['asic'].
Property matchers like has_items() can check for the key-value pair without a lambda, but the lambda form is clearer when the access path is more complex.
When would you use this?
The device_profile node embeds detailed hardware information that is not directly filterable with simple matchers.
This pattern is the standard way to target queries at specific ASIC types or specific hardware vendors — for example, to scope an IBA probe to only Juniper QFX devices.
Cross-node comparison
This is the lambda form’s strongest use case and one that has no equivalent in property matchers.
The predefined query DC - All ESI links uses a lambda to compare two different nodes in the same traversal result:
.where(lambda system, remote_system: system.role != remote_system.role)
This filters the result set to only keep rows where the role property of system is different from the role property of remote_system.
It eliminates traversal results where both ends of a link connect to the same role — keeping only genuine leaf-to-access or leaf-to-spine pairs.
No property matcher can compare one node’s properties against another node’s properties. A lambda is the only mechanism for this.
When would you use this? Any time you want to filter a traversal result based on a relationship between two nodes' values — for example:
-
Find all links where the two systems on either end are in different racks.
-
Find all VN instances where the VLAN ID on one leaf differs from the VLAN ID on another leaf (indicating a misconfiguration).
-
Find all interface pairs where one side is up and the other is down.
The lambda receives all named nodes in scope
If your query names multiple nodes, the lambda can reference any of them:
node(type='system', name='system')
...
.node(type='system', name='remote_system')
.where(lambda system, remote_system: system.role != remote_system.role)
The parameter names in the lambda must match the name= values in the query exactly.
Safety guardrails on lambdas
Because lambdas are literally Python code executed on the Apstra server, there are restrictions on what you can write. File system access, network calls, imports, and other potentially unsafe operations are blocked.
If you write a lambda that violates the guardrails, the query will return an error rather than executing. Keep lambdas to attribute access and comparison operations, and you will not run into this.
Checkpoint
Find all deployed spine systems and return their full data:
node(type='system', role='spine', deploy_mode='deploy', name='spine')
Run this via the API (or the Code tab) and confirm each item in items contains a "spine" key with the full system record.
Return both sides of every leaf-to-spine link:
node(type='system', role='leaf', name='leaf_sys')
.out('hosted_interfaces')
.node(type='interface', if_name=not_none())
.out('link')
.node(type='link', link_type='ethernet')
.in_('link')
.node(type='interface')
.in_('hosted_interfaces')
.node(type='system', role='spine', name='spine_sys')
Confirm each result item contains both "leaf_sys" and "spine_sys" keys.
Continue to Section 6: Traversal Surprises to understand why traversals sometimes return more results than you expect — and how to prevent it.