The property graph model

This section documents the current Neo4j 2026.x calendar-versioned line — Neo4j moved from semantic to calendar versioning (YYYY.MM) in 2025, and the same line applies to the Graph Data Science library — as published at the Neo4j documentation, which is the reference these pages are written and verified against. No specific monthly patch is pinned. Some areas (Aura’s internal infrastructure, the Raft consensus implementation details, and the GDS Pregel API’s low-level internals) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Neo4j iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Everything in Neo4j is one of four primitives: a node, a relationship connecting two nodes, a label attached to a node, and a property attached to either a node or a relationship. There is no table, no row, and no foreign key — a relationship is a first-class, stored pointer, not a value joined at query time.

A :Person node with labels :Person and :Actor and properties name

Nodes and relationships

A node represents an entity; a relationship connects exactly two nodes (or a node to itself) and is always directed and always typed — every relationship has one direction and one relationship type fixed at creation time, and both are stored, not inferred.

// https://neo4j.com/docs/getting-started/data-modeling/
CREATE (p:Person:Actor {name: "Ada Lovelace", born: date("1815-12-10")})
CREATE (m:Movie {title: "Loom", released: date("2026-01-01")})
CREATE (p)-[:ACTED_IN {roles: ["Lead"], since: date("2026-01-01")}]->(m)

Direction matters for how a relationship is stored and traversed — Neo4j keeps a dense, doubly-linked list of relationships per node so that following -[:ACTED_IN]→ from p or ←[:ACTED_IN]- into m are both O(1) per hop, unlike a foreign-key join. A Cypher pattern, however, can ignore direction by omitting the arrow, matching the relationship in either stored direction:

// undirected pattern: matches the relationship regardless of which way it was created
MATCH (p:Person)-[:ACTED_IN]-(m:Movie)
WHERE p.name = "Ada Lovelace"
RETURN m.title;

Labels

A label groups nodes for indexing, constraints and pattern matching — roughly Neo4j’s analogue of a table name, except a node can carry any number of labels at once. \(p:Person:Actor \{name: "Ada Lovelace"\}) above creates one node with both labels; labels can also be added or removed from an existing node without touching its properties or relationships:

MATCH (p:Person {name: "Ada Lovelace"})
SET p:Director
RETURN labels(p);

Properties and their types

Both nodes and relationships carry properties — typed key/value pairs. A relationship’s properties describe the connection itself (roles, since above), not either endpoint, which is why "who played what role, and since when" belongs on the :ACTED_IN relationship rather than being duplicated onto :Person or :Movie.

Temporal types

Cypher has dedicated temporal types — DATE, TIME, LOCAL TIME, DATETIME, LOCAL DATETIME and DURATION — constructed with functions of the same name and stored as native property values, not as strings:

// https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/
RETURN date("2026-09-14")                       AS today,
       datetime("2026-09-14T10:15:00Z")         AS asOf,
       duration({months: 2, days: 3})           AS runtimeGap;

The spatial Point type

POINT stores 2D or 3D coordinates, either geographic (WGS-84 latitude/longitude) or Cartesian, and supports native distance calculation with point.distance():

// https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/
MATCH (p:Person {name: "Ada Lovelace"})
SET p.homeBase = point({latitude: 51.5074, longitude: -0.1278})
RETURN p.homeBase;

LIST properties, and why MAP is not one

A property value can be a LIST of a single primitive type — languages and roles above are both lists. MAP is a first-class Cypher value used for parameters, query results and map projections (p \{.name, .born\}), but Neo4j does not store a map directly as a node or relationship property: a property is a primitive or a homogeneous list of primitives, never a nested structure. A value that genuinely needs map shape is either flattened into scalar/list properties, or modeled as its own node reached by another relationship if it needs independent identity and querying.

MATCH (p:Person {name: "Ada Lovelace"})
SET p.languages = ["English", "French"]
WITH p
MATCH (p)-[r:ACTED_IN]->(m:Movie)
RETURN p {.name, .born, credits: collect(m.title)} AS profile;   // a MAP value, built at query time

A vector embedding is stored the same way as a list of floats, but is common enough, and carries enough of its own ANN indexing and query surface, to warrant its own page — see Vector search & GenAI and the reference Vector type doc.

Schema-optional, not schema-free

None of the above requires an upfront schema: a label can be attached to one node and never used again, and two nodes with the same label can carry entirely different property sets. Neo4j calls this schema-optional rather than schema-free, because constraints can still be layered on to enforce uniqueness, existence or property type once a shape stabilizes — see Indexes & constraints for how to add them without giving up the freedom to evolve the model incrementally.

Contrasting with the document model

A document store keeps one JSON-like document per entity and models relationships by embedding or by an application-level key lookup — see MongoDB Schema Design and Couchbase documents, keys & metadata. The property graph model inverts that: relationships are stored, first-class, independently queryable records rather than embedded sub-structures or resolved keys, which is what makes multi-hop traversals (friend-of-a-friend, shortest path, variable-length pattern matching) a graph strength that a document or relational join has to reconstruct at query time.