neo4j-graphql-skill

$npx mdskill add neo4j-contrib/neo4j-skills/neo4j-graphql-skill

Build and configure a GraphQL API for Neo4j using @neo4j/graphql v7 (current) or v5 (LTS)

  • Create a GraphQL API backed by a Neo4j graph schema with support for custom resolvers.
  • @neo4j/graphql is used to build the API, including handling security and auto-generated queries/mutations.
  • The skill configures security rules using JWT or JWKS-based authorization directives.
  • Results are delivered through an Apollo Federation setup, integrating seamlessly with existing GraphQL environments.

SKILL.md

.github/skills/neo4j-graphql-skillView on GitHub ↗
---
name: neo4j-graphql-skill
description: Build and configure a GraphQL API backed by Neo4j using @neo4j/graphql v7 (current) or v5 (LTS).
  Covers Neo4jGraphQL constructor, getSchema(), assertIndexesAndConstraints(), type definitions with @node,
  @relationship (IN/OUT/UNDIRECTED), @cypher for custom resolvers, @authorization/@authentication for JWT/JWKS
  security, auto-generated queries/mutations, OGM programmatic access, subscriptions via CDC, and Apollo
  Federation. Use when writing typeDefs, securing fields, or wiring Neo4j to Apollo Server.
  Does NOT handle raw Cypher outside resolvers — use neo4j-cypher-skill.
  Does NOT cover Spring Data Neo4j entity mapping — use neo4j-spring-data-skill.
version: 1.0.1
allowed-tools: Bash WebFetch
---

## When to Use

- Creating a GraphQL API from a Neo4j graph schema with `@neo4j/graphql`
- Writing type definitions with `@relationship`, `@cypher`, `@authorization` directives
- Using OGM for server-side programmatic Neo4j access (bypasses GraphQL auth)
- Configuring auto-generated queries, mutations, subscriptions
- Securing types/fields with JWT or JWKS-based `@authorization` rules
- Migrating from v5/v6 to v7 (breaking changes below)

## When NOT to Use

- **Raw Cypher queries outside GraphQL resolvers** → `neo4j-cypher-skill`
- **Spring Data Neo4j / Java entity mapping** → `neo4j-spring-data-skill`
- **Generic GraphQL without Neo4j** — outside scope

---

## Version Matrix

| Version | Status | Notes |
|---|---|---|
| v7 | Current | `@node` required; `options` removed; explicit `eq` syntax |
| v5 | LTS | Older syntax; `options: {limit, offset, sort}` still valid |

Default to v7 unless codebase is on v5.

---

## Step 1 — Install

```bash
npm install @neo4j/graphql neo4j-driver graphql @apollo/server
```

For subscriptions (CDC required):
```bash
npm install ws graphql-ws express body-parser cors
```

---

## Step 2 — Minimal Server Setup

```javascript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';

const typeDefs = `#graphql
  type Movie @node {
    id: ID! @id
    title: String!
    actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
  }

  type Person @node {
    id: ID! @id
    name: String!
    movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
  }
`;

const driver = neo4j.driver(
  process.env.NEO4J_URI,
  neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

// assertIndexesAndConstraints syncs @id → UNIQUE constraints; wrap in try/catch
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });

const server = new ApolloServer({ schema: await neoSchema.getSchema() });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({ token: req.headers.authorization }),
  listen: { port: 4000 },
});
```

`assertIndexesAndConstraints` throws if constraints missing. Use `{ create: true }` to auto-create, or run `CREATE CONSTRAINT` manually and retry.

---

## Key Directives

### @node (v7 required)

Every GraphQL type representing a Neo4j node must have `@node`. Without it, v7 ignores the type.

```graphql
type Product @node {
  id: ID! @id
  name: String!
}

# Custom label (default = type name)
type Article @node(labels: ["Post", "Content"]) {
  title: String!
}
```

### @relationship — Full Syntax

```graphql
type Person @node {
  # direction: OUT = (this)-[:KNOWS]->(other)
  friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)

  # direction: IN = (other)-[:ACTED_IN]->(this)
  actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)

  # direction: UNDIRECTED = matches both directions (use sparingly — double-counts)
  colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)

  # Relationship with properties — reference an @relationshipProperties interface
  reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}

interface ReviewedProps @relationshipProperties {
  rating: Int!
  date: Date
}
```

Direction rule: `OUT` = arrow leaves this node. `IN` = arrow enters this node. Both sides of a relationship must declare opposite directions.

### Querying Relationship Properties — Connection API

For each relationship with `properties:`, a `{field}Connection` field is auto-generated. Access rel properties via `actorsConnection.edges.properties`, not via `actors`:

```graphql
query {
  movies(where: { title: { eq: "The Matrix" } }) {
    title
    actorsConnection {
      edges {
        properties { role }   # maps to @relationshipProperties interface
        node { name }
      }
    }
  }
}
```

### @cypher — Custom Resolver

```graphql
type Person @node {
  name: String!

  # columnName must exactly match the RETURN alias — mismatch returns null silently
  friendCount: Int
    @cypher(
      statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
      columnName: "friendCount"
    )

  recommendedMovies: [Movie!]!
    @cypher(
      statement: """
        MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
        WHERE NOT (this)-[:WATCHED]->(rec)
        RETURN rec
      """
      columnName: "rec"
    )
}

# @cypher on Query field — custom top-level query
type Query {
  topRatedMovies(limit: Int = 10): [Movie!]!
    @cypher(
      statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
      columnName: "m"
    )
}
```

`this` refers to the current node in field-level @cypher. Parameters are passed as `$paramName`.

### @cypher — Field Arguments and extend type

```graphql
# extend type adds computed fields without modifying the base type definition
extend type Movie @node {
  avgRating: Float
    @cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")

  # Field arguments passed as Cypher params; always provide default to avoid null
  recommended(limit: Int = 3): [Movie!]!
    @cypher(
      statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
      columnName: "rec"
    )
}
```

### @id and @timestamp

```graphql
type Post @node {
  id: ID! @id                          # auto-generates UUID; creates UNIQUE constraint
  createdAt: DateTime! @timestamp(operations: [CREATE])
  updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
  title: String!
}
```

### @alias — Map GraphQL field to Neo4j property

```graphql
type User @node {
  id: ID! @id
  email: String! @alias(property: "emailAddress")  # GraphQL: email → DB: emailAddress
}
```

---

## Security — @authentication and @authorization

### Step 1: Configure JWT in constructor

```javascript
// Symmetric secret
const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: {
    authorization: { key: process.env.JWT_SECRET },
  },
});

// JWKS endpoint (production)
const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: {
    authorization: {
      key: { url: 'https://myapp.com/.well-known/jwks.json' },
    },
  },
});
```

### Step 2: Pass token in context

```javascript
context: async ({ req }) => ({ token: req.headers.authorization }),
// Or pass pre-decoded JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),
```

### Step 3: Apply @authentication and @authorization

```graphql
# Require auth on all operations for a type
type Post @node
  @authentication
  @authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
  title: String!
  author: User! @relationship(type: "AUTHORED", direction: IN)
}

# requireAuthentication: false = allow public access without JWT
type Article @node
  @authorization(filter: [
    { requireAuthentication: false, where: { node: { published: { eq: true } } } }
    { where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
  ]) {
  title: String!
  published: Boolean!
}

# validate (throws error) vs filter (silently hides data)
type BankAccount @node
  @authorization(validate: [{
    when: [BEFORE],
    where: { node: { owner: { id: { eq: "$jwt.sub" } } } }
  }]) {
  balance: Float!
}

# Role-based with custom JWT claims
type JWT @jwt {
  roles: [String!]! @jwtClaim(path: "myApp.roles")
}

type AdminReport @node
  @authentication(operations: [READ], jwt: { roles: { includes: "admin" } }) {
  data: String!
}
```

**filter vs validate**: `filter` silently removes unauthorized data. `validate` throws an error. Use `validate` when data existence should not be revealed to unauthorized users.

**BEFORE vs AFTER**: `CREATE` supports only `AFTER`; `READ` supports only `BEFORE`.

---

## Auto-Generated Operations

For each `@node` type, the library generates:

| Operation | Generated Name | Example |
|---|---|---|
| Query all | `{plural}` | `movies(where, sort, limit, offset)` |
| Cursor pagination | `{plural}Connection` | `moviesConnection(first, after, where, sort)` |
| Create | `create{Plural}` | `createMovies(input: [MovieCreateInput!]!)` |
| Update | `update{Plural}` | `updateMovies(where, update)` |
| Delete | `delete{Plural}` | `deleteMovies(where, delete)` |

### v7 Filter Syntax (explicit `eq`)

```graphql
# v7: explicit eq required
query {
  movies(where: { title: { eq: "The Matrix" } }) {
    title
    actors { name }
  }
}

# Sort and paginate (v7: direct args, not options wrapper)
query {
  movies(sort: [{ title: ASC }], limit: 10, offset: 0) {
    title
  }
}
```

### Nested Mutations

```graphql
mutation {
  createMovies(input: [{
    title: "Inception"
    actors: {
      create: [{ node: { name: "Leonardo DiCaprio" } }]
      connect: { where: { node: { name: { eq: "Joseph Gordon-Levitt" } } } }
    }
  }]) {
    movies { id title }
  }
}
```

`connectOrCreate` was removed in v7. Use `connect` + `create` separately.

---

## OGM — Programmatic Access

OGM bypasses GraphQL authorization — use only in trusted server-side contexts.

```javascript
import { OGM } from '@neo4j/graphql-ogm';

const ogm = new OGM({ typeDefs, driver });
await ogm.init();  // must await before using models

const Movie = ogm.model('Movie');

// find
const movies = await Movie.find({
  where: { title: { eq: 'The Matrix' } },
  selectionSet: `{ id title actors { name } }`,
});

// create
const { movies: created } = await Movie.create({
  input: [{ title: 'Dune', actors: { create: [{ node: { name: 'Timothée Chalamet' } }] } }],
});

// update
await Movie.update({
  where: { id: { eq: movieId } },
  update: { title: { set: 'Dune: Part One' } },
});

// delete
await Movie.delete({ where: { id: { eq: movieId } } });
```

Install separately: `npm install @neo4j/graphql-ogm`

---

## Subscriptions (CDC Required)

Requires Neo4j CDC enabled in `FULL` mode. See [CDC docs](https://neo4j.com/docs/cdc/current/).

```javascript
const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: { subscriptions: true },
});
```

Subscriptions auto-generate for each type:
```graphql
subscription {
  movieCreated(where: { title: { eq: "The Matrix" } }) {
    createdMovie { title }
  }
}
# Also: movieUpdated, movieDeleted
```

---

## Schema Control Directives

```graphql
type ReadOnlyData @node @mutation(operations: []) { value: String! }  # disable mutations

type HeavyDoc @node {
  id: ID! @id
  content: String! @filterable(byValue: false) @sortable(enabled: false)  # perf guard
  title: String!
}

type Series @node @plural(value: "seriesList") { title: String! }  # irregular plural fix
```

---

## Common Errors

| Error | Cause | Fix |
|---|---|---|
| `Type 'X' not found` | Missing `@node` on type (v7) | Add `@node` to every node type |
| `@cypher` field returns null | `columnName` mismatch with RETURN alias | Match `columnName` exactly to RETURN alias |
| Relationship direction mismatch | Both sides declare same direction | Inverse: if A has `direction: OUT`, B must have `direction: IN` |
| `assertIndexesAndConstraints` throws | `@id` constraint not in DB | Add `{ options: { create: true } }` or run `CREATE CONSTRAINT` manually |
| Auth not applied | JWT not in context | Pass `token: req.headers.authorization` in context function |
| 0 results with valid data | v7 filter missing `eq` | Use `{ field: { eq: value } }` not `{ field: value }` |
| `connectOrCreate not found` | Removed in v7 | Use `connect` + `create` separately |
| Memory errors on large mutations | Complex Cypher generation | Batch mutations; increase `server.memory.heap.max_size` |
| `@subscription` not generating | v7 requires explicit enable | Add `features: { subscriptions: true }` to constructor |

---

## v6 → v7 Breaking Changes Summary

| v6 | v7 |
|---|---|
| `@node` optional | `@node` required on every node type |
| `options: { limit, sort }` | `limit`, `sort` as direct args |
| `{ field: value }` filter | `{ field: { eq: value } }` |
| `connectOrCreate` nested mutation | Removed — use `connect` + `create` |
| `directed` arg on queries | `queryDirection` in `@relationship` |
| Single rel fields `actor: Person` | Must use list `actors: [Person!]!` |
| `@private` directive | Removed |
| `@unique` directive | Removed |

---

## References

- [Neo4j GraphQL Docs](https://neo4j.com/docs/graphql/current/) — full directive reference, migration guides
- [GraphAcademy: GraphQL Basics](https://graphacademy.neo4j.com/courses/graphql-basics/) — hands-on course
- [GitHub: @neo4j/graphql](https://github.com/neo4j/graphql) — changelog, issues
- [CDC Setup](https://neo4j.com/docs/cdc/current/) — required for subscriptions

---

## Checklist

- [ ] `@node` on every GraphQL type representing a Neo4j node (v7 hard requirement)
- [ ] `@id` on identity fields (triggers `CREATE CONSTRAINT` via `assertIndexesAndConstraints`)
- [ ] `assertIndexesAndConstraints` called on startup with try/catch
- [ ] `@relationship` direction correct: `OUT` = arrow leaves this node, `IN` = arrow enters
- [ ] Both sides of relationship declared with inverse directions
- [ ] `@cypher` `columnName` matches RETURN alias exactly
- [ ] JWT secret or JWKS URL in `features.authorization.key`; token passed in context
- [ ] `@authorization` filter vs validate chosen deliberately (silent hide vs thrown error)
- [ ] v7: filters use explicit `{ field: { eq: value } }` syntax
- [ ] v7: `limit`/`sort` passed as direct query args (not `options` wrapper)
- [ ] OGM: `await ogm.init()` called before any `ogm.model()` usage
- [ ] Subscriptions: CDC enabled in FULL mode before enabling `features.subscriptions`
- [ ] `.env` holds credentials; `.env` in `.gitignore`

More from neo4j-contrib/neo4j-skills

SkillDescription
neo4j-agent-memory-skillAuthoritative reference for the neo4j-agent-memory Python package — a graph-native memory system for AI agents built on Neo4j — and for the hosted service (NAMS) at memory.neo4jlabs.com. Use this skill whenever the user mentions neo4j-agent-memory, agent memory with Neo4j, context graphs, the POLE+O model, MemoryClient/MemorySettings, the memory MCP server, or any of the framework integrations (LangChain, PydanticAI, CrewAI, AWS Strands, Google ADK, Microsoft Agent Framework, OpenAI Agents, LlamaIndex). Also use when the user mentions the hosted service at memory.neo4jlabs.com, NAMS, the Neo4j Agent Memory Service, the `nams_` API key prefix, or the hosted MCP endpoint. Also use when writing documentation, blog posts, tutorials, PRDs, or code samples for the project, when comparing agent memory approaches, or when positioning graph-native memory against vector-only approaches — even if the user doesn't explicitly name the package.
neo4j-aura-agent-skillManages Neo4j Aura Agents via the v2beta1 REST API — create, list, get, update, delete,
neo4j-aura-graph-analytics-skillServerless Aura Graph Analytics (AGA) GDS Sessions — covers GdsSessions,
neo4j-aura-provisioning-skillProvisions and manages Neo4j Aura instances via CLI (aura-cli v1.7+) or REST API.
neo4j-cli-tools-skillUse when working with Neo4j command-line tools — neo4j-cli (modern unified
neo4j-cypher-skillGenerates, optimizes, and validates Cypher 25 queries for Neo4j 2025.x and 2026.x.
neo4j-document-import-skillIngests unstructured and semi-structured documents into Neo4j as a knowledge graph.
neo4j-driver-dotnet-skillNeo4j .NET Driver v6 — IDriver lifecycle, DI registration (singleton), ExecutableQuery
neo4j-driver-go-skillCovers the Neo4j Go Driver v6 — driver lifecycle, ExecuteQuery, managed and
neo4j-driver-java-skillNeo4j Java Driver v6 — driver lifecycle, Maven/Gradle setup, executableQuery,