Skip to main content
skill CX Codex

neo4j-modeling-skill — Claude Code skill for Neo4j graph models

Designs, reviews and refactors Neo4j data models: nodes, relationships, properties, constraints. Useful once a graph schema starts to sprawl.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-modeling-skill -a claude-code
Open on GitHub

What neo4j-modeling-skill does

The skill walks through a graph model in order. First it asks for the queries the model has to answer, then sorts entities into nodes, connections into relationship types, and attributes into properties. Decision tables cover node vs relationship vs property, and property vs label. There is also an anti-pattern pass: generic labels like :Entity, supernodes with high fan-out, missing constraints.

A separate section handles intermediate nodes. When a relationship carries more than two properties, or becomes the subject of its own query, the skill suggests promoting it to a node: (Person)-[:ACTED_IN {role: "Neo"}]->(Movie) turns into (Person)-[:PLAYED]->(Role)-[:IN]->(Movie), and roles become searchable on their own. A migration table maps relational constructs to graph ones — table row to node, foreign key to relationship, junction table to intermediate node.

On a live database the skill insists you inspect first: CALL db.schema.visualization(), SHOW CONSTRAINTS, SHOW INDEXES, and CALL apoc.meta.schema() if APOC is installed. Two limits worth knowing before you start. Property type constraints (REQUIRE n.prop IS :: STRING) are Enterprise only, so on Community you validate types in the application. The ALTER CURRENT GRAPH TYPE SET syntax is not available on Community either. Cypher authoring, Spring Data mapping, GraphQL types and data import live in separate skills — this one does not cover them.

What it helps with

  • decide whether an attribute should be a label or a property
  • spot supernodes and high-fanout relationships in an existing model
  • move a relational schema with foreign keys into a graph
  • promote a relationship with several properties into an intermediate node
  • pick constraints and indexes for key properties

Example prompts for the agent

  • Inspect the current schema and propose a product catalog model: nodes, relationships, properties
  • Review my model for anti-patterns — generic labels and supernodes
  • My ACTED_IN relationship has a role field and I need to query roles on their own. Rework it as an intermediate node

How to install neo4j-modeling-skill

1Skills CLI (Vercel)

npx skills add neo4j-contrib/neo4j-skills --skill neo4j-modeling-skill -a claude-code

2Manually: put the files in place

~/.claude/skills/neo4j-modeling-skill/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/neo4j-modeling-skill/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about neo4j-modeling-skill

How do I install neo4j-modeling-skill in Claude?

Run npx skills add neo4j-contrib/neo4j-skills --skill neo4j-modeling-skill -a claude-code. The skill file then lands at ~/.claude/skills/neo4j-modeling-skill/SKILL.md.

What does neo4j-modeling-skill do?

It designs a Neo4j graph model from scratch, checks an existing one for anti-patterns, and migrates relational or document schemas into a graph. It also recommends constraints and indexes.

Does it write Cypher queries?

No. Cypher authoring and tuning belong to neo4j-cypher-skill. This skill stays on the data model: nodes, relationships, properties, labels, constraints.

Do property type constraints work on Community Edition?

No, they are Enterprise only. On Community the skill suggests sticking to uniqueness constraints and validating types in the application.

Source text

SKILL.md · MIT
---
name: neo4j-modeling-skill
description: Design, review, and refactor Neo4j graph data models. Use when choosing node
  labels vs relationship types vs properties, migrating relational/document schemas to
  graph, detecting anti-patterns (generic labels, supernodes, missing constraints),
  designing intermediate nodes for n-ary relationships, enforcing schema with constraints
  and indexes, or assessing an existing model against graph modeling best practices.
  Does NOT handle Cypher query authoring — use neo4j-cypher-skill.
  Does NOT handle Spring Data Neo4j entity mapping — use neo4j-spring-data-skill.
  Does NOT handle GraphQL type definitions — use neo4j-graphql-skill.
  Does NOT handle data import — use neo4j-import-skill.
version: 1.0.7
allowed-tools: WebFetch 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 Cypher** → `neo4j-cypher-skill`
- **Spring Data Neo4j (@Node, @Relationship)** → `neo4j-spring-data-skill`
- **GraphQL type definitions** → `neo4j-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:

```cypher
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:
```cypher
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 type known — catches bad writes early. **Enterprise only**; Community: use uniqueness constraints, validate types in app
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 — details in `neo4j-cypher-skill` (GRAPH TYPE reference). Not on Community (`ALTER CURRENT GRAPH TYPE SET` → "not supported in community edition"): use individual constraints. On 2026.02–2026.05 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.

Low-cardinality state such as `status`: label (`:Active`) if stable category used for traversal/filter; property if changes often or only returned with node. Never a `(:Status)` node.

---

### 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:**
```cypher
// 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 elementId(p1) < elementId(p2)
  AND e1.startDate <= coalesce(e2.endDate, date())
  AND e2.startDate <= coalesce(e1.endDate, date())
RETURN DISTINCT 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 | Prefer `tmdbId`/`personId` over bare `id` |
| 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:**
```cypher
// 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):**
```cypher
// 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.

```cypher
// 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;

// 4b. Community alternative to NODE KEY — composite uniqueness (existence not enforced)
CREATE CONSTRAINT movie_title_released_unique IF NOT EXISTS
  FOR (m:Movie) REQUIRE (m.title, m.released) IS UNIQUE;

// 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 — ranked free-text search via `db.index.fulltext.queryNodes` / `SEARCH` (Lucene syntax, wildcards like `An*`); not used for CONTAINS / STARTS WITH predicates
CREATE FULLTEXT INDEX person_fulltext IF NOT EXISTS
  FOR (n:Person) ON EACH [n.name, n.bio];

// 6b. Text index — serves CONTAINS / STARTS WITH / ENDS WITH (range index preferred for STARTS WITH when present)
CREATE TEXT INDEX person_bio_text IF NOT EXISTS
  FOR (n:Person) ON (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:
```cypher
SHOW INDEXES YIELD name, state WHERE state <> 'ONLINE' RETURN name, state;
```
Do NOT use an index until state = `ONLINE`.

---

### Vector / Embedding Property Modeling

Prefer dedicated `:Chunk` nodes for long text; short-text embeddings can live on the entity:

```
(: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]
- Chunked docs: do NOT put embedding on `:Document` — makes node too large, 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:
- [Neo4j Data Modeling Guide](https://neo4j.com/docs/getting-started/data-modeling/guide-data-modeling/)
- [Neo4j Modeling Tips](https://neo4j.com/docs/getting-started/data-modeling/modeling-tips/)
- [GraphAcademy: Graph Data Modeling Fundamentals](https://graphacademy.neo4j.com/courses/modeling-fundamentals/)
- [Super Nodes — All About Super Nodes (David Allen)](https://medium.com/neo4j/graph-modeling-all-about-super-nodes-d6ad7e11015b)

© neo4j-contrib · MIT · neo4j-contrib/neo4j-skills

Similar skills

All skills for Data and databases →

From the same set18

neo4j-cypher-skill Writes, optimizes and validates Cypher 25 queries for Neo4j 2025.x and 2026.x. Useful when a slow MATCH needs fixing or a batch write has to be assembled. CX Codex +1 1,500installs neo4j-graphrag-skill A skill that walks you through building GraphRAG retrieval pipelines on Neo4j with the neo4j-graphrag package: retriever choice, Cypher fragments, LLM wiring. CX Codex +1 1,100installs neo4j-driver-python-skill Tells your agent how to write Python against the Neo4j v6 driver: driver lifecycle, transactions, async, UNWIND batching. CX Codex +1 976installs neo4j-query-tuning-skill Reads a Neo4j execution plan, spots the operators that slow a Cypher query down, and tells you what to change. Useful when a query drags and PROFILE output is hard to read. CX Codex +1 908installs neo4j-vector-index-skill A skill for building and querying Neo4j vector indexes: index creation, nearest-neighbor search, batch embedding updates. CX Codex +1 905installs neo4j-getting-started-skill Walks you from an empty folder to a running Neo4j app in eight stages, from tool checks to a notebook or service. CX Codex +1 888installs neo4j-gds-skill Guides graph projections, algorithms, and pipelines for the embedded Neo4j GDS plugin. Useful for Python and Cypher graph analytics workflows. CX Codex +1 865installs neo4j-security-skill This skill walks an agent through Neo4j user, role and privilege management in Cypher against the system database. Useful when access has to be provisioned by script instead of by hand. CX Codex +1 824installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor