Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
neo4j-contrib avatar

Neo4j Cypher Skill

  • 841 installs
  • 101 repo stars
  • Updated August 3, 2026
  • neo4j-contrib/neo4j-skills

neo4j-cypher-skill is a Claude Code skill that generates, optimizes, and validates Cypher 25 queries for Neo4j 2025.x and 2026.x for developers who need correct graph patterns without slow or schema-blind queries.

About

neo4j-cypher-skill version 1.0.1 targets Neo4j 2025.x and 2026.x with Cypher 25 as the required first token on every query. It enforces a schema-first protocol using project schema JSON or live SHOW INDEXES/CONSTRAINTS inspection, default LIMIT 25 on exploratory reads, MERGE only on constrained keys, and a write execution gate requiring EXPLAIN before mutations. Coverage spans MATCH, MERGE, QPE path expressions, CALL IN TRANSACTIONS bulk loads, vector SEARCH clause 2026.01+, fulltext indexes, and six on-demand references including a 40+ entry syntax-traps table. Developers reach for neo4j-cypher-skill when writing new graph queries, debugging slow patterns, or validating batch writes against a real schema instead of guessing labels and relationship types.

  • Cypher 25: MATCH/OPTIONAL MATCH, MERGE constrained-key rules, CALL IN TRANSACTIONS, UNWIND, FOREACH
  • Subqueries: EXISTS, COUNT, COLLECT, CALL (x) { }, OPTIONAL CALL
  • Path expressions: QPEs, match modes, path selectors (SHORTEST 1, ALL SHORTEST)
  • Performance: Eager operator detection, parallel runtime, index hints, anti-patterns by severity
  • Search: vector SEARCH (2026.01+) with procedure fallback; fulltext via db.index.fulltext

Neo4j Cypher Skill by the numbers

  • 841 all-time installs (skills.sh)
  • +60 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #99 of 911 Databases skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-cypher-skill

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs841
repo stars101
Security audit3 / 3 scanners passed
Last updatedAugust 3, 2026
Repositoryneo4j-contrib/neo4j-skills

How do you write and optimize Cypher 25 for Neo4j 2025.x?

Generate, tune, and validate Cypher 25 for Neo4j 2025.x/2026.x without shipping slow or incorrect graph queries.

Who is it for?

Backend engineers on Neo4j 2025.x or 2026.x who need production-safe Cypher with schema guards, performance checks, and modern path-expression syntax.

Skip if: Teams needing Neo4j driver migration, server administration, or hybrid vector+fulltext ranking should use neo4j-migration-skill, neo4j-cli-tools-skill, or neo4j-vector-index-skill instead.

When should I use this skill?

The user writes, optimizes, or debugs Cypher queries, graph pattern matching, vector search, or batch Neo4j writes.

What you get

Parameterized Cypher 25 queries, EXPLAIN/PROFILE plans, schema validation notes, and version-gated SEARCH or QPE patterns.

  • Parameterized Cypher queries
  • EXPLAIN/PROFILE output
  • Schema validation checklist

By the numbers

  • Skill version 1.0.1 for Neo4j >= 2025.01
  • Includes 40+ documented Cypher syntax traps
  • Ships 6 on-demand reference guides

Files

SKILL.mdMarkdownGitHub ↗

When to Use

  • Writing, optimizing, or debugging Cypher queries
  • Graph pattern matching, QPEs, variable-length paths
  • Vector/fulltext search, subqueries, batch writes, LOAD CSV

When NOT to Use

  • Driver migration/API changesneo4j-migration-skill
  • DB admin (users, config, backups) → neo4j-cli-tools-skill
  • Hybrid search that combines vector with fulltext or other ranked sourcesneo4j-vector-index-skill

GQL conformance note: LET, FINISH, FILTER, and INSERT are valid Cypher 25 clauses (introduced via GQL conformance, mostly in Neo4j 2025.06). On older versions, fall back to WITH / (omit RETURN) / WHERE / CREATE. INSERT requires &-separated multi-labels and does not support dynamic labels/types.

---

Pre-flight

?KnownUnknown
<db-name>-schema.json found in projectUse it directly — skip live inspection
Schema (from context or live DB)Use directlyRun Schema-First Protocol
Neo4j versionUse version featuresDefault to 2025.01 safe set
Executing (not generating)?Use EXPLAIN + write gateState query is unvalidated

Schema unknown + no tool → produce non-executable sketch outside a code block:

(<SOURCE_LABEL> {<KEY>: $value})-[:<REL_TYPE>]->(<TARGET_LABEL>)

Never fill guessed names — realistic guesses get copied blindly.

---

Defaults — apply every query

1. CYPHER 25 — first token; never repeat after UNION or inside subqueries 2. Schema first — inspect before writing; if schema in prompt, use it directly 3. MERGE on constrained key only; rel MERGE on already-bound endpoints only 4. Label-free MATCH (n) forbidden unless bound or followed by WHERE n:$($label) 5. LIMIT 25 default on all exploratory reads; push WITH n LIMIT before high-cardinality operations (variable-length traversals, fan-out MATCH, Cartesian products) 6. Comments: // only — -- is SQL, invalid 7. REPEATABLE ELEMENTS / DIFFERENT RELATIONSHIPS go after MATCH, not end of pattern 8. SHOW commands: YIELD before WHERE; combinable with general Cypher clauses incl. UNION/RETURN [2026.05] — SHOW DATABASES still requires system db (use USE system) 9. Inline node predicates (:Label WHERE p=x) — valid in MATCH only 10. WHERE cannot follow bare UNWIND — use WITH x WHERE 11. (a)-[:R]-(b) — undirected matches both directions, double-counts; use directed unless unknown 12. DETACH DELETE — plain DELETE throws if node has relationships

---

Style

ElementConvention
Node labelsPascalCase :Person
Rel typesSCREAMING_SNAKE_CASE :KNOWS
Properties/varscamelCase firstName
ClausesUPPERCASE MATCH
Booleans/nulllowercase true false null
Stringssingle-quoted; double only if contains '
Schema is truth. :Person, :KNOWS, name in examples are illustrative — substitute real names from schema.

---

Schema-First Protocol

Priority order:

1. <db-name>-schema.json anywhere in project → read directly, state file name + schema_retrieved_at, skip live inspection. If significantly outdated and DB reachable, offer re-fetch. Full rules: references/schema-guardrail.md.

  • Existence — labels/rel-types/properties must be in schema; try synonym resolution before asking
  • Property type — reason about intent first (e.g. string vs INTEGER may be null check); ask only if unclear
  • Relationship direction — wrong direction → correct silently and note
  • Synonym mapping — unambiguous → resolve silently; ambiguous → pick most likely, note; ask if unresolvable

Scripts: generate_schema.py (live DB + APOC), define_schema.py (no DB), import_neo4j_schema.py (converts neo4j-graphrag-python, graph-schema-introspector, graph-schema-json-js-utils, mcp-neo4j-data-modeling).

2. Schema in context → use it, skip inspection.

3. Schema missing → run:

CALL db.schema.visualization() YIELD nodes, relationships RETURN nodes, relationships;
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state WHERE state = 'ONLINE';
SHOW CONSTRAINTS YIELD name, type, labelsOrTypes, properties;
SHOW PROCEDURES YIELD name RETURN split(name,'.')[0] AS namespace, count(*) AS procedures;

Property types per label — check APOC first:

// If APOC available (preferred — use this):
CALL apoc.meta.schema() YIELD value RETURN value;

// No APOC AND database ≤ 100k nodes/rels only (expensive on large graphs):
CALL db.schema.nodeTypeProperties() YIELD nodeType, propertyName, propertyTypes, mandatory;
CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes, mandatory;

Validate before returning any query: label exists · rel type+direction correct · property on that label · index ONLINE.

---

Key Patterns

MERGE

// MERGE on constrained key; set extras in ON CREATE/ON MATCH
CYPHER 25
MATCH (a:Person {id: $a}) MATCH (b:Person {id: $b})
MERGE (a)-[r:KNOWS]->(b)
  ON CREATE SET r.since = date()
  ON MATCH  SET r.lastSeen = date()

SET n = {} replaces all props. SET n += {} merges (safe partial update). Use += for updates.

WITH scope

CYPHER 25
MATCH (a:Person)-[:KNOWS]->(b:Person)
WITH a, count(*) AS friends   // b dropped here
WHERE friends > 5
RETURN a.name, friends ORDER BY friends DESC

Every var not listed in WITH is dropped. WITH * carries all forward.

Subqueries — cheat sheet

EXISTS { (a)-[:R]->(b) }                           // boolean check
COUNT  { (a)-[:R]->(b) WHERE a.x > 0 }             // count
COLLECT { MATCH (a)-[:R]->(b) RETURN b.name }       // collect list (full MATCH+RETURN required)
CALL (p) { MATCH (p)-[:ACTED_IN]->(m) RETURN m }    // correlated subquery (explicit import)
OPTIONAL CALL (p) { ... }                           // nullable subquery

CALL { WITH x ... } deprecated → CALL (x) { ... }. COLLECT {} returns exactly one column.

CALL IN TRANSACTIONS (bulk writes)

CYPHER 25
LOAD CSV WITH HEADERS FROM 'file:///data.csv' AS row
CALL (row) {
  MERGE (p:Person {id: row.id}) SET p += row
} IN TRANSACTIONS OF 1000 ROWS ON ERROR CONTINUE REPORT STATUS AS s

Input stream must be outside subquery. Auto-commit only — never wrap in beginTransaction(). PERIODIC COMMIT deprecated.

QPE basics

CYPHER 25
MATCH SHORTEST 1 (a:Person {name:'Alice'})(()-[:KNOWS]->()){1,}(b:Person {name:'Bob'})
RETURN b.name

// ACYCLIC [2026.03] — no repeated nodes within a path (prevents cycles)
CYPHER 25
MATCH p = ACYCLIC (start:Router {name: $from})-[:LINK]-+(end:Router {name: $to})
RETURN [n IN nodes(p) | n.name] AS route
ORDER BY length(p) LIMIT 5

Quantifier outside group: (pattern){N,M}. Groups start+end with node. REPEATABLE ELEMENTS needs bounded {m,n}. ACYCLIC implies nodes cannot repeat within a path (stronger than default DIFFERENT RELATIONSHIPS).

Match mode — add after MATCH:

  • DIFFERENT RELATIONSHIPS (default) — each rel traversed once per path
  • REPEATABLE ELEMENTS [2025.x] — nodes/rels revisitable; use for circular routes, weight-optimized paths, constrained backtracking; requires bounded {m,n}

Conditional CALL subqueries [2025.06]

CYPHER 25
MATCH (move:Item {id: $id})
OPTIONAL MATCH (insertBefore:Item {id: $before})
OPTIONAL MATCH (insertAfter:Item  {id: $after})
CALL (move, insertBefore, insertAfter) {
  WHEN insertBefore IS NULL THEN {
    MATCH (last:Item) WHERE NOT (last)-[:NEXT]->() AND last <> move
    CREATE (last)-[:NEXT]->(move)
  }
  WHEN insertAfter IS NULL THEN {
    CREATE (move)-[:NEXT]->(insertBefore)
  }
  ELSE {
    CREATE (insertAfter)-[:NEXT]->(move)
    CREATE (move)-[:NEXT]->(insertBefore)
  }
}

Use WHEN…THEN…ELSE for if-else-if write logic; mutually exclusive (first match wins). Not available pre-2025.06.

Dynamic relationship types [2025.x]

// Create/match/merge with dynamic rel type (must resolve to exactly one STRING)
CYPHER 25 CREATE (a:Node)-[:$($relType)]->(b:Node)
CYPHER 25 MATCH  (a:Node)-[:$($relType)]->(b:Node) RETURN a.name, b.name

Spatial / Point

// WGS84 geographic point
SET n.coords = point({longitude: $lon, latitude: $lat})

// Distance in metres; requires POINT index for performance
MATCH (a:Place {name: $origin}) MATCH (b:Place)
RETURN b.name, point.distance(a.coords, b.coords) AS distM
ORDER BY distM LIMIT 10

// Bounding-box pre-filter (uses POINT index) then distance
MATCH (b:Place)
WHERE point.withinBBox(b.coords,
        point({longitude: $west, latitude: $south}),
        point({longitude: $east, latitude: $north}))
RETURN b.name, point.distance(b.coords, $origin) AS distM

Create POINT index: CREATE POINT INDEX name IF NOT EXISTS FOR (n:Place) ON (n.coords)

Aggregation grouping keys

Non-aggregating expressions in RETURN/WITH are implicit grouping keys — no GROUP BY needed:

// actor + director are grouping keys; count(*) is the aggregate
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person)
RETURN a.name, d.name, count(*) AS collaborations
ORDER BY collaborations DESC

count(n) counts non-null; count(*) counts rows including nulls. collect(DISTINCT expr) deduplicates. count() is faster than size(collect()) — count() reads the internal store; collect() builds a list first.

---

Common Syntax Traps (top causes of broken queries)

WrongRight
ORDER BY n.prop AS x DESCORDER BY n.prop DESC
ORDER BY preAggVar after agg RETURNUse RETURN alias
count(r WHERE r.x=5)sum(CASE WHEN r.x=5 THEN 1 ELSE 0 END)
UNWIND list AS x WHERE x>5UNWIND list AS x WITH x WHERE x>5
least(a,b) / greatest(a,b)CASE WHEN a<b THEN a ELSE b END
-- comment// comment
shortestPath((a)-[*]->(b))SHORTEST 1 (a)(()-[]->()){1,}(b)
id(n)elementId(n)
[:REL*1..5](()-[:REL]->()){1,5}
CALL { WITH x ... }CALL (x) { ... }
COLLECT { (a)-[:R]->(b) }COLLECT { MATCH ... RETURN b }
SET n = {k:v} partial updateSET n += {k:v}
DELETE n with relationshipsDETACH DELETE n
WHERE n.x = nullWHERE n.x IS NULL
toInteger(null) throwstoIntegerOrNull(null)
n.$key dynamic propertyn[$key]
SET n:$labelSET n:$($label)
ZONED DATETIME >= date(...) → 0 rowsUse datetime(...) or .year accessor
ISO string with Z suffix stored/compared as UTC`Z` ≠ UTC in Neo4jZ is parsed as an offset, not the UTC timezone; planner and range indexes treat them differently. Explicitly coerce: datetime({datetime: datetime('2025-09-10T03:43:00Z'), timezone: 'UTC'}) (neo4j#13519)
FOREACH ... RETURNUNWIND ... RETURN

Full trap table → references/syntax-traps.md

---

Output Mode and Write Gate

Default: parameterized queries. *Return named properties, not full nodes or `RETURN `.**

// RIGHT: agent gets named fields it can reason over
CYPHER 25 MATCH (n:Organization {name: $name}) RETURN n.name, n.founded, n.industry LIMIT 10

// WRONG: full node object wastes tokens, leaks all properties, agent can't extract fields cleanly
CYPHER 25 MATCH (n:Organization {name: $name}) RETURN n LIMIT 10

Exception: schema/diagnostic queries (CALL db.schema.visualization(), SHOW INDEXES YIELD *, EXPLAIN) where the object is the point.

Validation workflow: 1. EXPLAIN before any write — catches syntax errors, missing indexes 2. New read: test with LIMIT 1 first 3. Write: verify read half as RETURN before replacing with SET/CREATE/DELETE 4. PROFILE to measure db hits; check for AllNodesScan, CartesianProduct, Eager

Query API v2 (no driver needed — works for schema inspection, EXPLAIN, reads, writes):

curl -X POST https://<instance>.databases.neo4j.io/db/<database>/query/v2 \
  -u <user>:<password> -H "Content-Type: application/json" \
  -d '{"statement": "EXPLAIN MATCH (n:Person {name: $name}) RETURN n", "parameters": {"name": "Alice"}}'
# Local: http://localhost:7474/db/<database>/query/v2
# Response: {"data": {"fields": [...], "values": [...]}}  — prefix EXPLAIN to plan without executing

Write execution gate — only when agent executes (MCP/cypher-shell/HTTP), NOT when generating for code/scripts/user to run: 1. Run EXPLAIN → report estimated rows affected 2. Wait for user confirmation before executing

---

Version Gates

Default to 2025.01-safe features when version unknown.

FeatureMin versionFallback
CYPHER 25, QPEs, CALL (x) {}2025.01require 2025+
Match modes (DIFFERENT RELATIONSHIPS, REPEATABLE ELEMENTS)2025.01require 2025+
Dynamic labels $($expr), coll.sort()2025.01APOC or app-side
CONCURRENT TRANSACTIONS, REPORT STATUS2025.01drop / omit
SEARCH clause (vector/fulltext)2026.01CALL db.index.vector.queryNodes(...) (deprecated 2026.04)
ACYCLIC path mode (no repeated nodes in path)2026.03post-filter with size(nodes(p)) = size(apoc.coll.toSet(nodes(p)))
string.indexOf(), string.join(), string.regexReplace()2026.05apoc.text.* or app-side
GQL aliases: FOR=UNWIND, PROPERTY_EXISTS=IS NOT NULL, IS [NOT] LABELED=n:Label; function aliases (local_time, zoned_datetime, duration_between, collect_list, etc.)2026.02–04GQL compliance only — use Cypher equivalents; full list → references/cypher-syntax.md
GRAPH TYPE schema DDL (ALTER CURRENT GRAPH TYPE SET, EXTEND GRAPH TYPE WITH, DROP GRAPH TYPE ELEMENTS, SHOW CURRENT GRAPH TYPE)2026.02 — PREVIEWUse individual CREATE CONSTRAINT / CREATE INDEX

---

Performance

EXPLAIN/PROFILE red flags: AllNodesScan CartesianProduct NodeByLabelScan Eager

Fix Eager — three approaches (choose simplest that works): 1. Add specific labels to MATCH nodes to eliminate read/write ambiguity: MATCH (x:CallingPoint) instead of bare MATCH (x) when writing :City nodes 2. Collect first, then write: WITH collect(u) AS users UNWIND users AS u ... 3. CALL IN TRANSACTIONS: isolates each batch in its own transaction

Label inference — when planner underestimates selectivity on multi-label queries: [Neo4j 5]

CYPHER inferSchemaParts = most_selective_label
MATCH (admin:Administrator {name: $name}), (resource:Resource {name: $res})
MATCH p=(admin)-[:MEMBER_OF]->()-[:ALLOWED_INHERIT]->(company)
RETURN count(p)

Index anchors: every MATCH/MERGE/WHERE on a property needs an index on the lookup property or Neo4j scans all nodes. Index only activates when the node has a labelMATCH (n {prop: $v}) never uses an index; MATCH (n:Label {prop: $v}) does. MERGE without a constraint has no atomicity guarantee (two concurrent MERGEs can create duplicates). CONTAINS/ENDS WITH → TEXT index (RANGE does not support them). Force a plan with USING INDEX n:Label(prop) when EXPLAIN shows a scan. Chained OPTIONAL MATCH for nested data → replace with COLLECT { MATCH ... RETURN }. Dynamic labels ($($label)) → AllNodesScan+Filter; use static labels when possible.

Full anti-patterns → references/performance.md

---

Failure Recovery

  • 0 results: check param types, remove WHERE predicates one-by-one, EXPLAIN for index use
  • TypeErrors: use toIntegerOrNull()/toFloatOrNull(); guard with IS NOT NULL
  • Variable out of scope: not listed in WITH → use count(*) not count(droppedVar)
  • Timeouts: fix AllNodesScan → add early LIMITCALL IN TRANSACTIONS OF 1000 ROWS
  • Long-running query progress [2026.03]: SHOW TRANSACTIONS YIELD currentQuery, status, currentQueryProgress
  • DateTime mismatch: ZONED DATETIME >= date(...) → 0 rows; use datetime() or .year
  • Z suffix ≠ UTC timezone: ISO strings with Z are stored as a UTC-offset, not the UTC zone — range queries across Z and UTC stored values return 0 rows. Coerce on write: datetime({datetime: datetime($isoStr), timezone: 'UTC'})
  • Duration: .inDays/.inMonths don't exist; use .days/.months
  • Cannot merge node using null property value: MERGE key resolved to null — validate params first
  • IndexNotFoundError: SHOW INDEXES YIELD name, state WHERE state <> 'ONLINE'

---

References

Load on demand:

  • references/indexes.md — index types (RANGE/TEXT/FULLTEXT/POINT/COMPOSITE/LOOKUP), constraints, MERGE lock semantics, fulltext Lucene syntax, import pre-flight
  • references/cypher-syntax.md — full syntax reference: WITH, DELETE, ORDER BY, CASE, null, lists, strings, dates, spatial/point, LOAD CSV, subqueries, QPEs, dynamic labels, SEARCH; conditional CALL (WHEN/THEN/ELSE); label pattern expressions; allReduce; NEXT clause; compact CASE WHEN; normalize(); index/constraint types table; functions annotated with version introduced
  • references/syntax-traps.md — 40+ syntax trap table
  • references/performance.md — anti-patterns, text vs fulltext indexes, Eager (3 fix strategies), label inference, batching best practices, parallel runtime
  • references/advanced-patterns.md — REPEATABLE ELEMENTS patterns, allReduce stateful traversal, multi-stop QPE, route planning simulation, DAG critical path, temporal fraud detection component graph, cycle detection, OPTIONAL CALL
  • references/apoc.md — APOC Core: refactoring, virtual graph, merge helpers, path expanders, triggers, collections, conditional execution
  • references/graph-type.mdPREVIEW (2026.02+) GRAPH TYPE DDL: ALTER CURRENT GRAPH TYPE SET, EXTEND GRAPH TYPE WITH, DROP GRAPH TYPE ELEMENTS, property types, constraints, label implications, relationship type enforcement

WebFetch

NeedURL
Clause semanticshttps://neo4j.com/docs/cypher-manual/25/clauses/{clause}/
Function signatureshttps://neo4j.com/docs/cypher-manual/25/functions/{type}/
QPE / pathshttps://neo4j.com/docs/cypher-manual/25/patterns/
Spatial/point functionshttps://neo4j.com/docs/cypher-manual/25/functions/spatial/
Index/constraint referencehttps://neo4j.com/docs/cypher-manual/25/indexes/
Full cheat sheethttps://neo4j.com/docs/cypher-cheat-sheet/25/all/

---

Checklist

  • [ ] Schema inspected or confirmed in context
  • [ ] CYPHER 25 prefix on every top-level query
  • [ ] $parameters used (not literals)
  • [ ] LIMIT on exploratory reads (default 25)
  • [ ] EXPLAIN run; red flags resolved
  • [ ] Write half verified as RETURN before executing
  • [ ] Write execution gate applied if agent is executing (not generating)
  • [ ] MERGE on constrained key only
  • [ ] No label-free MATCH (n)
  • [ ] Schema ops not inside explicit transaction

Related skills

How it compares

Use neo4j-cypher-skill for Cypher authoring and tuning; switch to sibling skills for driver migrations, DB admin, or hybrid vector ranking.

FAQ

Which Neo4j versions does neo4j-cypher-skill support?

The neo4j-cypher-skill skill targets Neo4j 2025.x and 2026.x with Cypher 25 syntax, defaulting to a 2025.01-safe feature set when the server version is unknown and documenting version gates for SEARCH, ACYCLIC paths, and GQL aliases.

How does neo4j-cypher-skill prevent bad graph writes?

The neo4j-cypher-skill skill requires schema inspection or project schema JSON, mandates EXPLAIN before writes, verifies read halves with LIMIT 1, and blocks execution until the developer confirms estimated rows affected through the write execution gate.

Is Neo4j Cypher Skill safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Databasesdatabases

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.