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
1. Pick the batch type
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
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
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
#[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 inreturning:
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: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.