Automated Setup with NQE
A synthetic device can have many connections: an internet node might span dozens of sites, or an L3 VPN a large CE mesh. Adding those one at a time is impractical, so you can generate them from an NQE query instead. Forward runs the query, turns each result row into a connection, and keeps the set in sync as the query or the network changes.
These are called dynamic or NQE-based connections, as opposed to the manual ones you enter by hand. The two coexist on one device: a device carries at most one attached query and any manual connections you add alongside it, and Forward merges both sets. Each connection is labeled by source — NQE (dynamic) or Manual (static) — in the connections table.
Adding NQE-Based Connections
- On the synthetic device's setup step, choose Batch-add NQE based connections.
- Select an existing NQE query, or click + Add new query from template to scaffold one. That opens the NQE Library with a starter query for this device type. Edit it in the NQE editor and commit it.
- Return to the Synthetic Devices page, select the query, and add it.
Each row the query returns becomes one connection. Only one NQE query can be attached at a time. To add more connections, edit the query so it returns more rows.
Forward recomputes dynamic connections automatically when either of these happens:
- You edit the committed query.
- Forward processes a new snapshot and re-runs the query against it.
The updated connections apply to the next processed snapshot.
Deleting the query is not the same as editing it, and Forward doesn't stop you from deleting one a synthetic device still uses. In the NQE Library, committing the delete raises a warning and relabels the commit button Commit anyway, but lets you proceed; over the API the delete just goes through. Once it commits, Forward clears the attachment on every device that used the query and drops the dynamic connections it produced. The device and its manual connections stay. Attach a query again to regenerate the dynamic connections.
Start from + Add new query from template — the template already returns the record type this device expects (see The Connection Schema), so you only fill in the row-selection logic. On SaaS deployments, NQE Query Generator AI Assist can draft that logic from a plain-language prompt inside the editor. For the query language itself, see the NQE Library and the NQE Language guides.
Query Compatibility
Each device type expects a specific result type, and Forward enforces it in two places:
- When you select a query: the picker lists only queries whose output is compatible with the device type. Incompatible queries don't appear; if none match, you'll see "No compatible query available."
- When connections are computed: if the query's row type doesn't match, computation fails with a message that the query is invalid for this synthetic-device connection type.
Start from the template (+ Add new query from template) to get a query whose shape already matches the type.
The Connection Schema
Each device type maps to an NQE record type. Your query's @query block must return a list of that type.
Internet node and Intranet node — InetConnection
InetConnection has no vrf field, so an NQE query places every connection in the default VRF. To put an intranet-node
connection in a non-default VRF, set it in the connection editor or the REST API instead.
type InetConnection =
{ /** The uplink device's name and interface name. */
uplinkInterface: IfaceReference
, /** The gateway device's name and interface name. If null, the uplink interface doubles as the gateway. */
gatewayInterface [nullable]: IfaceReference
, /** The VLAN of traffic from the uplink interface. null if untagged. */
vlan [nullable]: Int
, /** An optional name for this connection, used as the interface name created on the synthetic device. */
connectionName [nullable]: String
, /** An optional name for the site in which this connection's gateway device resides. */
site [nullable]: String
, /** How advertised subnets are inferred from the gateway device's routing table. */
subnetDiscoveryMethod: SubnetDiscoveryMethod
, /** Advertised subnets. Must be non-empty if subnetDiscoveryMethod is `none`. */
subnets: List<IpSubnet>
, /** L3 interfaces that provide backdoor connectivity between sites. */
backdoorInterfaces: List<IfaceReference>
};
type IfaceReference =
{ deviceName: String
, interfaceName: String
};
SubnetDiscoveryMethod is one of:
SubnetDiscoveryMethod.none
SubnetDiscoveryMethod.ipRoutes({ advertisesDefaultRoute: false })
SubnetDiscoveryMethod.bgpRoutes({ peerIps: [] })
SubnetDiscoveryMethod.interfaceAddresses
bgpRoutes takes a peerIps: List<IpAddress>. Pass an empty list to infer the eBGP peers, or list specific advertising
peers to restrict discovery to them.
L3 VPN — L3VpnConnection
The same fields as InetConnection except there's no site, plus a nullable vrf. If you adapt an
InetConnection query, drop the site field: L3VpnConnection doesn't define one, so any value you set there is
ignored.
, /** The VRF this connection's routing domain belongs to. */
vrf [nullable]: String
Adjacent network — L3VpnConnection
An adjacent network uses the same L3VpnConnection row type as the L3 VPN, so the template you scaffold for it is the
L3 VPN template. Leave vrf unset: adjacent networks don't use VRFs, and a row that sets one is rejected when
connections are computed. Like L3VpnConnection generally, there's no site field; set a connection's site in the
connection editor if you need one.
L2 VPN — L2VpnConnection
L2 VPN connections don't route, so there's no gateway, subnet discovery, or backdoor fields:
type L2VpnConnection =
{ edgeInterface: IfaceReference
, vlan [nullable]: Int
, connectionName [nullable]: String
};
WAN Circuit
WAN circuits work differently from the per-device model above: each row is a whole device (a name plus its two connections), so one query generates a set of circuit devices rather than adding connections to a single device. They have exactly two connections and use a separate query shape:
type WanCircuit =
{ deviceName: String
, connection1: Connection
, connection2: Connection
};
type Connection =
{ interface: IfaceReference
, vlan [nullable]: Int
, connectionName [nullable]: String
};
Common Mistakes
The NQE type checker catches some common mistakes up front (wrong field names or value types); others pass the type
check and fail later when Forward computes the connections (an out-of-range VLAN, a bad site name, an empty subnets
list). The ones that come up repeatedly:
- Field names must match exactly. It's
backdoorInterfaces(plural), notbackdoorInterface. It'ssubnetDiscoveryMethod, notsubnetAutoDiscovery. - VLAN can't be
0. Usenullfor untagged traffic. Valid tagged values are1–4095. - Site names are restricted. Only letters, digits,
.,-, and_. Spaces, commas, and&are rejected. subnetsmust be non-empty whensubnetDiscoveryMethodisnone. With any discovery method, it can be empty.peerIpsis aList<IpAddress>. Pass[](or a list of addresses), not another collection type.- Field value types must match the schema. A string where an
IfaceReferenceis expected, or a bare device name instead of{ deviceName, interfaceName }, fails the type check.
When Connections Come Back Empty or Wrong
A query that fails at run time produces no error in the topology; the synthetic device simply ends up with no connections. If a device that should have connections has none, check the query result:
- Open the synthetic device and inspect its stored configuration (or
GETit via the API). A failed query leaves aqueryResultholding anerror. The status names the failure kind:QUERY_RUN_ERRORfor a runtime failure,COLUMN_DATATYPE_MISMATCHfor a row that isn't the expected connection type,MISSING_REQUIRED_COLUMNSfor an absent field. - Re-run the query in the NQE editor against the same snapshot and confirm it returns rows of the expected type.
- Check the field-name and type mistakes above.
See Troubleshooting & FAQ for more.