Skip to main content
Tutorial
Get Started runs one query that creates two users, connects them with a relationship, and reads that relationship back. This page takes the same query apart: what each operation contributes, what it becomes on the wire, and what the server sends back.
Every tab builds the same request. The JSON tab is the body sent to POST /v2/query, and the v3 SDKs are typed builders for exactly that body.

The whole query

Four named entries run in order, and later entries reuse earlier results by name:

1. Pick the batch type

A batch is a list of named entries plus the names to return. A write batch is the only batch that accepts a mutating traversal. Rust and TypeScript reject addN in a read batch at compile time, and Go and Python reject it while the request is being built, so a stray write in a read path never reaches the server. All entries in one batch commit or roll back together, which is what lets entry 4 read the edge that entry 3 has only just created.

2. Create the two nodes

varAs adds one entry and gives it a name. g() starts an empty traversal, so addN here is a source operation with no input and creates exactly one node. Given an input stream instead, addN creates one node per incoming row. Properties travel as ordered [name, value] pairs, and each value carries its type tag (string, i64, f64, bool, date_time, bytes, and the array and object variants). The { "value": … } wrapper distinguishes a literal from an { "expr": … } reference such as a parameter. The name alice is local to this transaction. It is not a stored variable, a route, or a server-side binding, and it is gone once the response is sent.

3. Connect them with a directed edge

Unlike addN, addE needs an input stream: the nodes in that stream become the edge sources, and to is the target. n(NodeRef.var("alice")) turns entry 1’s result back into a stream, which is why add_e carries an input of { "nodes": { "reference": { "var": "alice" } } }. Direction comes from the operation, not the label: Alice is the source and Bob is the target, so traversing out from Alice reaches Bob. If the input stream held several nodes, addE would create one edge per source node. since is stored as a plain string here because the value is written as a string. Use a date-time property when you need range comparisons or ordering on it.
follow is not listed in returning, so it never appears in the response. The entry still runs. Leave write-only steps out of returning to keep the payload small.

4. Follow the new edge in the same transaction

This entry is a read inside a write batch. It starts at Alice again, walks out along FOLLOWS, and projects two fields from whatever it lands on. The edge from entry 3 is uncommitted at this point but still visible, because the batch is one transaction. Each chained operation consumes the previous stream and wraps it as input, so the AST nests in the opposite order to the builder chain: the last operation you write is the outermost JSON object, and the source sits at the innermost position. out returns the destination nodes of outgoing edges. Passing a label does more than filter the results — it narrows which edges are read in the first place, so always pass one when the schema provides it. valueMap is a terminal operation: it selects properties by name and ends the traversal, so nothing can be chained after it. $id and $label expose identity and label alongside ordinary properties, and omitting the property list returns every property.

5. Choose what comes back

returning picks which named entries appear in the response body. Names it omits still execute; they are simply not serialized back to the client.

6. Name the request

Rust’s #[query] macro rewrites the function to return a request and sets query_name from the function name. TypeScript and Python convert a batch with toQueryRequest / to_query_request, and Go’s WriteQuery(name) took the name up front. query_name is optional diagnostic metadata for gateway logs and query diagnostics. It does not create a stored endpoint or deploy anything; a missing or null name is reported as __dynamic__.

7. Run it

8. Read the response

The response is a flat object keyed by the names in returning:
The shape of each value depends on how its entry ended: Node and edge IDs are unsigned 64-bit integers, and projected properties come back as plain JSON values rather than the tagged form used in the request. Returning a raw stream is rarely what a caller wants. End an entry with the terminal that produces the shape you need — each of these replaces valueMap on the friends entry:

9. Read it back in a later request

Once the write has committed, the same traversal works from a read batch — Alice is now found by property instead of by a name from an earlier entry:
It returns the same rows the write batch already reported:
Two details are worth noting. nWithLabel becomes a nodes_where source predicate on $label, and the general .where(…) filter wraps it — the label chooses what is scanned, the filter narrows the rows that come out of it. And dedup() guards against the multigraph case, where several FOLLOWS edges between the same pair would otherwise yield the same node more than once. A label scan reads every User. Add a secondary index on name and use nWhere instead, so the lookup is pushed down to the index.

Next steps

Writing data

Use a write batch for graph mutations.

Reading data

Choose an efficient source for a traversal.

Traversals

Follow relationships and keep correlated values.

Add parameters

Separate runtime values from the stable AST.