@neo4j-contrib/neo4j-modeling-skill

@neo4j-contrib/neo4j-modeling-skill — AI coding skill

View in AI SkillSafe app
0 downloads
0 stars
0 demos
SKILL.md
nameneo4j-modeling-skill
descriptionDesign, review, and refactor Neo4j graph data models. Use when choosing node
version1.0.6
allowed-toolsWebFetch Bash

When to Use

  • Designing graph model from scratch (domain → nodes, rels, props)
  • Reviewing existing model for anti-patterns
  • Deciding node vs property vs relationship vs label
  • Migrating relational or document schema to graph
  • Designing intermediate nodes for n-ary or complex relationships
  • Detecting and mitigating supernode / high-fanout problems
  • Choosing and creating constraints + indexes for a model

When NOT to Use

  • Writing or optimizing Cypherneo4j-cypher-skill
  • Spring Data Neo4j (@Node, @Relationship)neo4j-spring-data-skill
  • GraphQL type definitionsneo4j-graphql-skill
  • Importing data (LOAD CSV, APOC import)neo4j-import-skill

Inspect Before Designing

On existing database, run first — never propose changes without current state:

CALL db.schema.visualization() YIELD nodes, relationships RETURN nodes, relationships;
SHOW CONSTRAINTS YIELD name, type, labelsOrTypes, properties RETURN name, type, labelsOrTypes, properties;
SHOW INDEXES YIELD name, type, labelsOrTypes, state WHERE state = 'ONLINE' RETURN name, type, labelsOrTypes;

If APOC available:

CALL apoc.meta.schema() YIELD value RETURN value;

MCP tool map:

Operation Tool
Inspect schema get-schema
SHOW CONSTRAINTS, SHOW INDEXES read-cypher
CREATE CONSTRAINT ... IF NOT EXISTS write-cypher (show + confirm first)

Defaults — Apply to Every Model

  1. Use-case first — list 5+ queries the model must answer before designing
  2. Nodes = entities (nouns) with identity; rels = connections (verbs) with direction
  3. Labels PascalCase; rel types SCREAMING_SNAKE_CASE; properties camelCase
  4. Every node type used in MERGE has a uniqueness constraint on its key property
  5. Add property type constraints (REQUIRE n.prop IS :: STRING) where the type is known — helps the query planner and catches bad writes early
  6. No generic labels (:Entity, :Node, :Thing); no generic rel types (:RELATED_TO, :HAS)
  7. Security labels (used for row-level access control) should start with a common prefix (e.g. Sec) so application code can reliably filter them out of the domain schema
  8. Rel direction encodes semantic meaning — not arbitrary
  9. Inspect schema before proposing any change on an existing database
  10. All constraint/index DDL uses IF NOT EXISTS — safe to rerun
  11. On Neo4j 2026.06+ (Enterprise/Aura, GA): declare the full model in one block with ALTER CURRENT GRAPH TYPE SET { … }, extend it with ALTER CURRENT GRAPH TYPE ADD { … }, instead of individual CREATE CONSTRAINT statements — see neo4j-cypher-skill/references/graph-type.md. On 2026.02–2026.05 the same syntax is preview.

Key Patterns

Node vs Relationship vs Property — Decision Table

Question Answer Model as
Is it a thing with identity, queried as entry point? Yes Node
Is it a connection between two things with direction? Yes Relationship
Does the connection have its own properties or multiple targets? Yes Intermediate node
Is it a scalar always returned with its parent, never filtered alone? Yes Property on parent
Is it a category used for type-based filtering or path traversal? Yes Label (not a property)
Does the same attribute value repeat across many nodes (low cardinality)? Yes Label, not a property node
Is it a fact connecting >2 entities? Yes Intermediate node

Property vs Label — Decision Table

Use label when Use property when
Values are few, fixed, used as traversal filters (WHERE n:Active) Values are many, dynamic, or unique per node
You traverse by type (MATCH (n:VIPCustomer)) You filter by value (WHERE n.tier = 'vip')
Category drives index selection Fine-grained value drives range scans
Example: :Active, :Verified, :Premium Example: status, score, email

Rule: adding a label is cheap; scanning all :Label nodes is fast. Never model high-cardinality values as labels.


Intermediate Node Pattern

Use when a relationship needs its own properties, connects >2 entities, or is independently queryable.

Before (relationship with property — limited):

(Person)-[:ACTED_IN {role: "Neo"}]->(Movie)
// Cannot query roles independent of movies

After (intermediate node — queryable, extensible):

(Person)-[:PLAYED]->(Role {name: "Neo"})-[:IN]->(Movie)
// MATCH (r:Role) WHERE r.name STARTS WITH 'Neo' RETURN r

Employment overlap example:

// Find colleagues who overlapped at same company
MATCH (p1:Person)-[:WORKED_AT]->(e1:Employment)-[:AT]->(c:Company)<-[:AT]-(e2:Employment)<-[:WORKED_AT]-(p2:Person)
WHERE p1 <> p2
  AND e1.startDate <= e2.endDate AND e2.startDate <= e1.endDate
RETURN p1.name, p2.name, c.name

Promote relationship to intermediate node when:

  • Relationship has >2 properties
  • Relationship is the subject of another query
  • Multiple entities share the same connection context
  • You need to connect >2 entities in one fact

Relational → Graph Migration Table

Relational construct Graph equivalent Notes
Table row Node One label per table (add more as needed)
Column (scalar) Node property
Primary key Uniqueness constraint property Use tmdbId, not id (too generic)
Foreign key Relationship Direction: from dependent → referenced
Many-to-many junction table Intermediate node Especially if junction has own columns
Junction table (no own columns) Direct relationship Simpler; upgrade to intermediate node later
NULL FK (optional relation) Absent relationship No node created; absence is the signal
Polymorphic FK (Rails-style) Multiple labels or relationship types Split into type-specific rels
Self-referential FK Same-label relationship :Employee {managerId}(e)-[:REPORTS_TO]->(m)
Audit/history columns Intermediate versioning node See References for versioning pattern

Supernode Detection and Mitigation

Detect:

// Find top-10 highest-degree nodes
MATCH (n)
RETURN labels(n) AS labels, elementId(n) AS id, count{ (n)--() } AS degree
ORDER BY degree DESC LIMIT 10

Node with degree >> median for its label = supernode candidate. Any node with >100K relationships will degrade traversal queries that pass through it.

Causes:

  • Domain supernodes: airports, celebrities, popular hashtags — unavoidable
  • Modeling supernodes: gender, country, status modeled as nodes with millions of edges — avoidable

Mitigation strategies (in priority order):

Strategy When to use Implementation
Query direction Directional asymmetry exists Query from low-degree side; exploit direction
Relationship type split Supernode serves multiple roles :FOLLOWS + :FAN instead of single :RELATED_TO
Label segregation Supernode conflates entity types :Celebrity vs :User → query only relevant subtype
Bucket pattern Time-series or high-volume event nodes See below
Avoid modeling Low-cardinality categoricals Use label instead of node (:Active not (:Status {name:"Active"}))
Join hint Query tuning last resort USING JOIN ON n in Cypher

Bucket pattern (time-series / high-volume):

// Instead of: (:User)-[:VIEWED]->(:Page) (millions of rels per user)
// Bucket by hour:
(u:User)-[:VIEWED_IN]->(b:ViewBucket {userId: u.id, hour: '2025-04-28T14'})-[:VIEWED]->(p:Page)

// Query last hour's views without traversing full history:
MATCH (u:User {id: $uid})-[:VIEWED_IN]->(b:ViewBucket {hour: $hour})-[:VIEWED]->(p)
RETURN p.url

Naming Conventions

Element Convention Good Bad
Node label PascalCase, singular noun :Person, :BlogPost :person, :blog_posts, :Entity
Relationship type SCREAMING_SNAKE_CASE, verb phrase :ACTED_IN, :WORKS_FOR :actedin, :relatedTo, :HAS
Property key camelCase firstName, createdAt FirstName, first_name
Constraint name snake_case descriptive person_id_unique constraint1
Index name snake_case descriptive person_name_idx index2

Schema Enforcement — What to Create for Each Element

Run all DDL with IF NOT EXISTS. Apply before importing data.

// 1. Uniqueness constraint — every node type used in MERGE
CREATE CONSTRAINT person_id_unique IF NOT EXISTS
  FOR (p:Person) REQUIRE p.id IS UNIQUE;

// 2. Existence constraint (Enterprise) — mandatory properties
CREATE CONSTRAINT person_name_exists IF NOT EXISTS
  FOR (p:Person) REQUIRE p.name IS NOT NULL;

// 3. Property type constraint (Enterprise) — enforce data type
CREATE CONSTRAINT person_born_integer IF NOT EXISTS
  FOR (p:Person) REQUIRE p.born IS :: INTEGER;

// 4. Key constraint (Enterprise) — unique + exists in one
CREATE CONSTRAINT movie_tmdbid_key IF NOT EXISTS
  FOR (m:Movie) REQUIRE m.tmdbId IS NODE KEY;

// 5. Range index — equality and range filters on properties
CREATE INDEX person_name_idx IF NOT EXISTS
  FOR (p:Person) ON (p.name);

// 6. Fulltext index — CONTAINS, STARTS WITH, free text search
CREATE FULLTEXT INDEX person_fulltext IF NOT EXISTS
  FOR (n:Person) ON EACH [n.name, n.bio];

// 7. Vector index — embedding similarity search
CREATE VECTOR INDEX chunk_embedding_idx IF NOT EXISTS
  FOR (c:Chunk) ON (c.embedding)
  OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' } };

// 8. Relationship index — filter on rel properties
CREATE INDEX acted_in_year_idx IF NOT EXISTS
  FOR ()-[r:ACTED_IN]-() ON (r.year);

After creating indexes, poll until ONLINE:

SHOW INDEXES YIELD name, state WHERE state <> 'ONLINE' RETURN name, state;

Do NOT use an index until state = ONLINE.


Vector / Embedding Property Modeling

Store embeddings on dedicated :Chunk nodes, never on business nodes:

(:Document)-[:HAS_CHUNK]->(c:Chunk {text: "...", embedding: [...]})

Rules:

  • Chunk node: text (source text), embedding (float array), chunkIndex (int)
  • Parent document: metadata only (title, url, createdAt)
  • Vector index on c.embedding only
  • Chunk size 200–500 tokens with 20% overlap is production default [field]
  • Do NOT put embedding on :Document — makes the node too large and pollutes traversal

Anti-Patterns Table

Anti-pattern Problem Fix
Generic labels :Entity, :Node No filtering benefit; all nodes scan Use domain labels :Person, :Product
Generic rel types :RELATED_TO, :HAS Can't filter by relationship type Use semantic types :PURCHASED, :AUTHORED
Low-cardinality value as node Supernode (:Status {name:"active"} → millions of edges) Use label :Active instead
Property as label (n.type = 'VIP' + :VIP label both exist) Inconsistency, duplication Pick one; prefer label if used in traversal
Storing embeddings on business node Node bloat, slow traversal Dedicated :Chunk node
MERGE without uniqueness constraint Duplicate nodes silently created Add constraint before any MERGE
Missing relationship direction meaning Arbitrary direction; confusing model Direction = semantic flow of action
Junction table modeled as bare property Loses history and extensibility Intermediate node with its own properties
id as property name id(n) is a deprecated Cypher function (use elementId(n)); bare id is fine as a property name in practice, but domain-qualified names (personId, movieId) are clearer and avoid any future ambiguity Prefer personId, movieId, tmdbId where it aids readability
All dates as strings No range queries; no temporal operators Use Neo4j date() or datetime() type

Output Format — Schema Assessment

When reviewing an existing model:

## Schema Assessment

### Compliant
- [constraint / pattern that is correct]

### Issues Found
#### [Title] — Severity: ERROR / WARNING / INFO
- **Current**: what the model does
- **Problem**: why it is an issue
- **Fix**: specific Cypher DDL or model change

## Recommended Schema
### Node Labels
- :Label {key: TYPE, prop: TYPE, ...}  → constraints: [list]

### Relationships
- (:LabelA)-[:TYPE {prop: TYPE}]->(:LabelB)

### Constraints to Create
[CREATE CONSTRAINT ... statements]

### Indexes to Create
[CREATE INDEX ... statements]

Severity semantics:

Severity Meaning Action
ERROR Model correctness failure (duplicates possible, data loss risk) Stop; fix before proceeding
WARNING Performance or extensibility risk Report; ask user before proceeding
INFO Style or convention deviation Surface; continue

Provenance Labels

  • [official] — stated directly in Neo4j docs
  • [derived] — follows from documented behavior
  • [field] — community heuristic; treat as default but validate

Checklist

  • Use cases (≥5 queries) defined before modeling
  • Schema inspected on existing database before changes proposed
  • Every MERGE-target node label has a uniqueness constraint
  • No generic labels (:Entity, :Node, :Thing)
  • No generic relationship types (:RELATED_TO, :HAS, :CONNECTED_TO)
  • Relationship direction encodes semantic meaning
  • N-ary or propertied relationships use intermediate nodes
  • High-cardinality values stored as properties, not nodes
  • Low-cardinality categoricals used as labels, not property nodes
  • Embeddings on dedicated :Chunk nodes, not business nodes
  • Supernode candidates identified and mitigated
  • All DDL uses IF NOT EXISTS
  • Indexes polled to ONLINE before use
  • Assessment output follows the structured format above
  • Every prohibition paired with a concrete fix

References

Load on demand:

Embed badges

Add these to your README to show the skill's verification status.

SkillSafe verified badge
Verified badge
[![SkillSafe verified badge](https://api.skillsafe.ai/v1/badge/@neo4j-contrib/neo4j-modeling-skill/verified)](https://skillsafe.ai/skill/@neo4j-contrib/neo4j-modeling-skill/)
Installs badge
Installs badge
[![Installs badge](https://api.skillsafe.ai/v1/badge/@neo4j-contrib/neo4j-modeling-skill/installs)](https://skillsafe.ai/skill/@neo4j-contrib/neo4j-modeling-skill/)
Scan badge
Scan badge
[![Scan badge](https://api.skillsafe.ai/v1/badge/@neo4j-contrib/neo4j-modeling-skill/scan)](https://skillsafe.ai/skill/@neo4j-contrib/neo4j-modeling-skill/)
Eval pass rate badge
Eval pass rate
[![Eval pass rate badge](https://api.skillsafe.ai/v1/badge/@neo4j-contrib/neo4j-modeling-skill/eval)](https://skillsafe.ai/skill/@neo4j-contrib/neo4j-modeling-skill/)