agentleFS
opsmillopsmill/infrahub/.agents/skills/neo4j-cypher-guide/SKILL.md

neo4j-cypher-guide

Skill548 starsChanged 11 days agoApache-2.0View on GitHub

Comprehensive guide for writing modern Neo4j Cypher queries. Essential for text2cypher MCP tools and LLMs generating Cypher queries. Covers removed/deprecated syntax, modern replacements, CALL subqueries for reads, COLLECT patterns, sorting best practices, set-based writes, Quantified Path Patterns (QPP) for efficient graph traversal, and how to choose the entry point of a query so the plan starts with an index seek instead of a label scan.

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 13 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈13

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When generating Cypher queries, immediately avoid these REMOVED features:

    line 12
  2. Distinctive:

    Always filter nulls when sorting: - Add IS NOT NULL checks for sorted properties

    line 24
  3. Distinctive:

    Explicit is better than implicit: - Always use explicit grouping and type checking

    line 25
  4. Distinctive:

    Use standard Cypher patterns with modern syntax:

    line 57
  5. Distinctive:

    Consider Quantified Path Patterns (QPP) for better performance:

    line 67
  6. Distinctive:

    Use COUNT{}, EXISTS{}, and COLLECT{} subqueries:

    line 79
  7. Distinctive:

    Use CALL subqueries for sophisticated data retrieval:

    line 90
  8. Distinctive:

    Before finalizing any generated query:

    line 176
  9. Distinctive:

    Start with indexed properties: - Always anchor patterns with indexed lookups

    line 212
  10. Distinctive:

    Limit expansion depth: - Use reasonable upper bounds in quantifiers

    line 215
  11. Distinctive:

    Profile queries: - Use PROFILE to identify bottlenecks

    line 217
  12. Very specific:

    The node uuids are known: keep the :Node label and pass one list, WHERE n.uuid IN $ids, so the plan starts with a NodeIndexSeek on …

    line 229
  13. Distinctive:

    CALL{} - Execute subqueries for complex reads

    line 294
  14. Distinctive:

    Always prefer modern syntax patterns for better performance and maintainability.

    line 303

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Neo4j Modern Cypher Query Guide

This skill helps generate Neo4j Cypher queries using modern syntax patterns and avoiding deprecated features. It focuses on efficient query patterns for graph traversal, data retrieval, and set-based writes.

Quick Compatibility Check

When generating Cypher queries, immediately avoid these REMOVED features:

  • ❌ id() function → Use elementId()
  • ❌ Implicit grouping keys → Use explicit WITH clauses
  • ❌ Pattern expressions for lists → Use pattern comprehension or COLLECT subqueries
  • ❌ Repeated relationship variables → Use unique variable names
  • ❌ Automatic list to boolean coercion → Use explicit checks

Core Principles for Query Generation

  1. Use modern syntax patterns - QPP for complex traversals, CALL subqueries for complex reads
  2. Optimize during traversal - Filter early within patterns, not after expansion
  3. Always filter nulls when sorting - Add IS NOT NULL checks for sorted properties
  4. Explicit is better than implicit - Always use explicit grouping and type checking
  5. Write set-based, not per-node - One query with a WHERE/property condition beats fetching nodes and mutating them one by one (an N+1). E.g. delete all rows for one owner in one shot:

    MATCH (n:Preference { owner_id: $owner_id })
    DETACH DELETE n

    not "fetch matching nodes, then run one DELETE per node".

Critical Sorting Rule

ALWAYS filter NULL values when sorting:

// WRONG - May include null values
MATCH (n:Node)
RETURN n.name, n.value
ORDER BY n.value

// CORRECT - Filter nulls before sorting
MATCH (n:Node)
WHERE n.value IS NOT NULL
RETURN n.name, n.value
ORDER BY n.value

Query Pattern Selection Guide

For Simple Queries

Use standard Cypher patterns with modern syntax:

MATCH (n:Label {property: value})
WHERE n.otherProperty IS :: STRING
RETURN n

For Variable-Length Paths

Consider Quantified Path Patterns (QPP) for better performance:

// Instead of: MATCH (a)-[*1..5]->(b)
// Use: MATCH (a)-[]-{1,5}(b)

// With filtering:
MATCH (a)((n WHERE n.active)-[]->(m)){1,5}(b)

For Aggregations

Use COUNT{}, EXISTS{}, and COLLECT{} subqueries:

MATCH (p:Person)
WHERE count{(p)-[:KNOWS]->()} > 5
RETURN p.name,
       exists{(p)-[:MANAGES]->()} AS isManager

For Complex Read Operations

Use CALL subqueries for sophisticated data retrieval:

MATCH (d:Department)
CALL (d) {
  MATCH (d)<-[:WORKS_IN]-(p:Person)
  WHERE p.salary IS NOT NULL  // Filter nulls
  WITH p ORDER BY p.salary DESC
  LIMIT 3
  RETURN collect(p.name) AS topEarners
}
RETURN d.name, topEarners

Common Query Transformations

Counting Patterns

// Old: RETURN size((n)-[]->())
// Modern: RETURN count{(n)-[]->()}

Checking Existence

// Old: WHERE exists((n)-[:REL]->())
// Modern: WHERE EXISTS {MATCH (n)-[:REL]->()}
// Also valid: WHERE exists{(n)-[:REL]->()}

Element IDs

// Old: WHERE id(n) = 123
// Modern: WHERE elementId(n) = "4:abc123:456"
// Note: elementId returns a string, not integer

Sorting with Null Handling

// Always add null check
MATCH (n:Node)
WHERE n.sortProperty IS NOT NULL
RETURN n
ORDER BY n.sortProperty

// Or use NULLS LAST
MATCH (n:Node)
RETURN n
ORDER BY n.sortProperty NULLS LAST

When to Load Reference Documentation

Load the appropriate reference file when:

references/deprecated-syntax.md

  • Migrating queries from older Neo4j versions
  • Encountering syntax errors with legacy queries
  • Need complete list of removed/deprecated features

references/subqueries.md

  • Working with CALL subqueries for reads
  • Using COLLECT or COUNT subqueries
  • Handling complex aggregations
  • Implementing sorting with null filtering

references/qpp.md

  • Optimizing variable-length path queries
  • Need early filtering during traversal
  • Working with paths longer than 3-4 hops
  • Complex pattern matching requirements

references/entry-points.md

  • A plan starts with NodeByLabelScan or UnionNodeByLabelsScan where you expected an index seek
  • Writing a query that starts from a relationship's branch index
  • The indexed edge is not the first hop of the pattern

Query Generation Checklist

Before finalizing any generated query:

  1. ✅ No deprecated functions (id, btree indexes, etc.)
  2. ✅ Explicit grouping for aggregations
  3. ✅ NULL filters for all sorted properties
  4. ✅ Appropriate subquery patterns for reads
  5. ✅ Consider QPP for paths with filtering needs
  6. ✅ Use COUNT{} instead of size() for pattern counting
  7. ✅ Variable scope clauses in CALL subqueries
  8. ✅ Unique variable names for relationships

Error Resolution Patterns

"Implicit grouping key" errors

// Problem: RETURN n.prop, count(*) + n.other
// Solution: WITH n.prop AS prop, n.other AS other, count(*) AS cnt
//          RETURN prop, cnt + other

"id() function not found"

// Use elementId() but note it returns a string, not integer

"Repeated variable" errors

// Problem: MATCH (a)-[r*]->(), (b)-[r*]->()
// Solution: MATCH (a)-[r1*]->(), (b)-[r2*]->()

Performance Tips

  1. Start with indexed properties - Always anchor patterns with indexed lookups
  2. Filter early in QPP - Apply WHERE clauses within the pattern
  3. Filter nulls before sorting - Prevent unexpected results and improve performance
  4. Limit expansion depth - Use reasonable upper bounds in quantifiers
  5. Use EXISTS for existence checks - More efficient than counting
  6. Profile queries - Use PROFILE to identify bottlenecks
  7. Leave indexed properties unwrapped - toString(n.uuid) IN $ids cannot use the uuid index

Entry Point of a Branch-Scoped Query

Build one query text per entry point, and check the first operator of each plan with EXPLAIN. Neo4j plans a query text once per parameter type, such as null or a list, and per length range of a list or string, then reuses the plan for every value of that type and range. A $ids IS NULL OR n.uuid IN $ids check plans a NodeByLabelScan instead of the Node(uuid) seek whether $ids is null or a list, and a {branch: $branch_name} seek gets the same plan on a small branch and on the default branch.

  • The node uuids are known: keep the :Node label and pass one list, WHERE n.uuid IN $ids, so the plan starts with a NodeIndexSeek on Node(uuid). Use this entry whenever the query can run on the default branch, where a branch index seek reads nearly every edge of the type.
  • Only the branch is known: match the indexed edge without node labels. A small label, such as a kind label, or :Root or :Node on an instance with few branches, can make the plan start with a NodeByLabelScan. Check the labels after a CALL () { ... } subquery, or after WITH DISTINCT on the start node when a second MATCH expands from it. A label check in the same MATCH, or after a plain WITH, brings the scan back.
  • The branch index exists only for the edge types listed in backend/infrahub/core/graph/index.py. A type union such as HAS_ATTRIBUTE|IS_RELATED {branch: $branch_name} plans one seek per type.
// ❌ A kind label with few vertices makes the plan start with NodeByLabelScan
MATCH (n:InfraDevice)-[r:HAS_ATTRIBUTE {branch: $branch_name}]->(a:Attribute)

// ✅ The seek runs first, and the labels filter its rows
CALL () {
    MATCH (n)-[r:HAS_ATTRIBUTE {branch: $branch_name}]->(a)
    RETURN n, r, a
}
WITH n, r, a
WHERE n:InfraDevice AND a:Attribute

Load references/entry-points.md for why the planner prefers the label scan, which plan operators to look for, and patterns where the indexed edge is not the first hop.

Modern Cypher Features

Label Expressions

WHERE n:Label1|Label2  // OR
WHERE n:Label1&Label2  // AND
WHERE n:!Archived      // NOT

Dynamic Labels and Types

Labels and relationship types accept parameters (Neo4j 5.26+):

MATCH (n:$($label))                    // dynamic node label
MATCH (a)-[r:$($rel_type)]->(b)        // dynamic relationship type

Choosing between a dynamic label and interpolating the label into the query text is a performance tradeoff, not a style rule: each interpolated variant is planned and cached separately, but the planner generally does better with a static label than a dynamic one. If the label is the primary filter of the MATCH, prefer a static/interpolated label; when other indexed predicates carry the filtering, a dynamic label is more likely to be acceptable. When in doubt, PROFILE both.

Type Predicates

WHERE n.prop IS :: STRING
WHERE n.value IS :: INTEGER NOT NULL
WHERE n.data IS :: LIST<STRING>

Subquery Patterns for Reads

  • COUNT{} - Count patterns efficiently
  • EXISTS{} - Check pattern existence
  • COLLECT{} - Collect complex results
  • CALL{} - Execute subqueries for complex reads

Quantified Path Patterns

  • Inline filtering during traversal
  • Access to nodes and relationships in patterns
  • Significant performance improvements (up to 1000x)
  • Support for complex, multi-hop patterns

Always prefer modern syntax patterns for better performance and maintainability.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.