Emacs generic graph store.
netz is a Lisp-native directed property graph store for Emacs. Graphs live in memory and can be persisted to disk. Nodes, edges, query rows, and paths are all ordinary plist-friendly data structures so they are easy to inspect, transform, and use from other Emacs packages.
netz provides a directed property graph core and a Cypher-inspired query DSL. The documented behavior is covered by tests.
A graph is a cl-defstruct named netz-graph with separate storage and indexes:
(cl-defstruct netz-graph
name
path
nodes
edges
out-index
in-index)Nodes are property lists with a required :id:
(:id "person:tosh" :label "Person" :name "Tosh")Edges are directed property lists with required :id, :source, and :target:
(:id "works:tosh:netz"
:source "person:tosh"
:target "project:netz"
:type "WORKS_ON")Edges have their own IDs, so multiple distinct relationships between the same nodes are supported:
(:id "e1" :source "a" :target "b" :type "LIKES")
(:id "e2" :source "a" :target "b" :type "KNOWS")Indexes are maintained automatically:
out-index: source node id -> outgoing edge idsin-index: target node id -> incoming edge ids
(require 'netz)
(require 'netz-query)
(setq graph (netz-create-graph :example))
(netz-add-node graph '(:id "person:tosh" :label "Person" :name "Tosh"))
(netz-add-node graph '(:id "project:netz" :label "Project" :name "netz" :stars 12))
(netz-add-edge graph '(:id "works:tosh:netz"
:source "person:tosh"
:target "project:netz"
:type "WORKS_ON"))
(netz-query graph
(:match
(:node person :label "Person")
(:edge person project :type "WORKS_ON" :direction :out)
(:node project :label "Project"))
(:where
(> (netz-prop project :stars) 10))
(:return person project))Returns plist binding rows:
((:person (:id "person:tosh" ...)
:project (:id "project:netz" ...)))Graph arguments can generally be graph objects or registered graph names.
Create and register an empty graph.
(netz-create-graph :notes)
(netz-create-graph :notes :path "/tmp/notes.graph" :save t)netz-make-graph is an alias for netz-create-graph.
Return a registered graph by name.
(netz-get-graph :notes)Save a graph to its path.
(netz-save-graph graph)
(netz-save-graph :notes)Load and register a graph from disk. Indexes are rebuilt on load.
(netz-load-graph "/tmp/notes.graph")Reload a graph from its path.
Copy nodes and edges into a registered graph named new-name.
Add a node plist. Existing nodes are merged, with incoming properties winning.
(netz-add-node graph '(:id "note:1" :label "Note" :title "Hello"))(netz-get-node graph "note:1")Return a list of all node plists.
Delete a node. Nodes with incident edges require :detach t.
(netz-delete-node graph "note:1" :detach t)Add a directed edge plist.
(netz-add-edge graph '(:id "e1" :source "a" :target "b" :type "LINKS_TO"))(netz-get-edge graph "e1")Return a list of all edge plists.
(netz-delete-edge graph "e1")Return edge ids for a node. Direction is one of :out, :in, or :any.
(netz-node-edge-ids graph "a" :direction :out)netz-query is a macro over binding rows. A query usually contains :match, optional :where, and :return clauses.
(netz-query graph
(:match
(:node person :label "Person")
(:edge person project :type "WORKS_ON" :direction :out)
(:node project :label "Project"))
(:return person project))You can also query by registered graph name:
(netz-query :notes
(:match (:node note :label "Note"))
(:return note))(:node binding &rest properties)Examples:
(:node note :label "Note")
(:node start :id "note:1")
(:node _ :label "Tag") ; anonymous bindingRepeated bindings are constrained to the same entity.
(:edge source target &rest properties)By default edges match in any direction. Use :direction for explicit traversal:
(:edge a b :type "LINKS_TO") ; any direction
(:edge a b :type "LINKS_TO" :direction :out)
(:edge a b :type "LINKS_TO" :direction :in)Bind the matched edge with :as:
(:edge person project :as rel :type "WORKS_ON" :direction :out):where is ordinary Elisp evaluated with query variables lexically bound.
(netz-query graph
(:match (:node project :label "Project"))
(:where (> (netz-prop project :stars) 10))
(:return project))Binding rows:
(:return person project)
;; => ((:person <node> :project <node>) ...)Count:
(:return :count)IDs:
(:return :ids person project)Pluck a property:
(:return :pluck person :name)Return a graph containing matched row entities:
(:return :graph :new-graph-name)Path queries find reachable nodes within a depth range.
(netz-query graph
(:match
(:node start :id "person:tosh")
(:path start target
:direction :out
:depth (1 3)))
(:return target))Options:
:path source target
:edge (:type "LINKS_TO")
:direction :out | :in | :any
:depth (min max)
:as routeUse :as to bind the full path route:
(netz-query graph
(:match
(:node start :id "person:tosh")
(:path start target
:as route
:direction :out
:depth (1 3))
(:node target :label "Tag"))
(:return target route))A route is a plist:
(:start <start-node>
:end <end-node>
:nodes (<ordered-node-plists>)
:edges (<ordered-edge-plists>)
:steps ((:from <node> :edge <edge> :to <node> :traversed :out) ...)
:length 2
:graph <netz-graph>)Order guarantee:
:nodesare in traversal order:edgesare in traversal order- edge at index
iconnects node at indexito node at indexi+1 :stepsrecords the exact traversal, including whether an edge was traversed:outor:in
The embedded :graph contains the route's nodes and edges so existing graph tools can operate on the path as a graph.
(netz-query graph
(:match
(:node a :id "person:tosh")
(:node b :id "tag:emacs")
(:path a b :as route :direction :any :depth (1 5)))
(:return route))Returns route rows if connected, or nil if no path exists.
For yes/no:
(> (netz-query graph
(:match
(:node a :id "person:tosh")
(:node b :id "tag:emacs")
(:path a b :direction :any :depth (1 5)))
(:return :count))
0)Mutation clauses use the same pattern language.
Creates entities and errors on duplicate ids.
(netz-query graph
(:create
(:node person :id "person:tosh" :label "Person" :name "Tosh")
(:node project :id "project:netz" :label "Project" :name "netz")
(:edge person project :id "works:tosh:netz" :type "WORKS_ON"))
(:return person project))Find-or-create by :id.
(netz-query graph
(:merge
(:node person :id "person:tosh" :label "Person"))
(:set person :name "Tosh" :active t)
(:return person))Update properties on a bound node or edge.
(:set person :name "Tosh" :active t)
(:set rel :since 2026)Delete matched entities.
(netz-query graph
(:match (:node tmp :label "Temp"))
(:delete tmp))Delete matched nodes and their incident edges.
(netz-query graph
(:match (:node note :id "note:1"))
(:detach-delete note))- Queries scan nodes/edges for property matching beyond id/index lookups.
- Path queries return every route found within the depth range and can grow quickly on dense graphs.
- Mutation queries mutate directly.
See docs/query-dsl.md for the query DSL reference.