Use when building Spring Boot applications with Neo4j using Spring Data Neo4j (SDN 7.x/8.x): @Node entity mapping, @Relationship, @RelationshipProperties, Neo4jRepository, ReactiveNeo4jRepository, @Query annotations, application.yml configuration, projections, Neo4jClient, Neo4jTemplate, transactions, auditing, or Spring AI Neo4jVectorStore vector search. Does NOT handle raw Java driver code without Spring — use neo4j-driver-java-skill. Does NOT handle Cypher query authoring — use neo4j-cypher-skill. Does NOT handle driver version upgrades — use neo4j-migration-skill.
npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-spring-data-skill
spring-boot-starter-data-neo4j)@Node entity classes and @Relationship/@RelationshipProperties mappingsNeo4jRepository or ReactiveNeo4jRepository interfaces@Query annotations with Cypher on repository methodsapplication.yml for Neo4j connectionNeo4jClient or Neo4jTemplateNeo4jVectorStore for vector search in Spring appsneo4j-driver-java-skillneo4j-cypher-skillneo4j-migration-skillneo4j-gds-skill| SDN | Spring Boot | Spring Framework | Java | Neo4j |
|--------|-------------|-----------------|------|-------|
| 8.0.x | 3.3.x / 3.4.x | 6.2.x | 17+ | 5.15+ |
| 8.1.x | 3.4.x+ | 7.0.x | 17+ | 5.15+ |
| 7.5.x | 3.2.x | 6.1.x | 17+ | 4.4+ |
Use spring-boot-starter-data-neo4j — it pulls SDN + driver. No explicit SDN version needed when using Spring Boot BOM.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
spring:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
spring:
neo4j:
uri: ${NEO4J_URI} # neo4j+s://xxxx.databases.neo4j.io
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
Credentials: store in .env; never hardcode. Verify .env is in .gitignore.
import org.springframework.data.neo4j.core.schema.*;
// Internal generated ID (default for most cases)
@Node("Person")
public class PersonEntity {
@Id @GeneratedValue private Long id; // element ID (Long)
private String name;
@Property("birth_year") private Integer birthYear; // custom property name
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private List<PersonEntity> friends = new ArrayList<>();
}
// UUID business key
@Node("Product")
public class ProductEntity {
@Id @GeneratedValue(generatorClass = GeneratedValue.UUIDStringGenerator.class)
private String id;
@Version private Long version; // optimistic locking; required with business key
}
// User-assigned key (caller sets value; no @GeneratedValue)
@Node("Country")
public class CountryEntity {
@Id private String isoCode;
private String name;
}
// Multiple static labels
@Node(primaryLabel = "Vehicle", labels = {"Car", "Auditable"})
public class CarEntity { ... }
// Runtime labels
@Node("Content")
public class ContentEntity {
@Id @GeneratedValue private Long id;
@DynamicLabels private Set<String> tags = new HashSet<>(); // labels added at runtime
}
Use @RelationshipProperties when the relationship itself carries data.
@RelationshipProperties
public class RolesRelationship {
@RelationshipId // internal relationship ID; required
private Long id;
private List<String> roles;
@TargetNode // marks the other end of the relationship
private PersonEntity person;
}
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String title;
@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
private List<RolesRelationship> actorsAndRoles = new ArrayList<>();
}
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long> {
Optional<PersonEntity> findByName(String name);
List<PersonEntity> findByBirthYearBetween(int from, int to);
List<PersonEntity> findByNameContainingIgnoreCase(String fragment);
long countByBirthYearGreaterThan(int year);
void deleteByName(String name);
}
// CORRECT: $param bound parameter
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f")
List<PersonEntity> findFriendsOf(String name);
// With pagination
@Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name",
countQuery = "MATCH (p:Person) RETURN count(p)")
Page<PersonEntity> findAllPaged(Pageable pageable);
// Return relationship-rich entity; map target via @Node return
@Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person {name: $name}) RETURN m, collect(r), collect(p)")
List<MovieEntity> findMoviesActedInBy(String name);
Security rule: NEVER string-concatenate user input into Cypher. Always use $paramName.
Page<PersonEntity> findByBirthYearGreaterThan(int year, Pageable pageable);
List<PersonEntity> findTop10ByOrderByNameAsc();
List<PersonEntity> findByName(String name, Sort sort);
Usage:
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<PersonEntity> result = repo.findByBirthYearGreaterThan(1980, page);
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List<PersonSummary> findByBirthYearLessThan(int year);
public record PersonDto(String name, Integer birthYear) {}
List<PersonDto> findByName(String name);
<T> List<T> findByName(String name, Class<T> type);
// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactivePersonRepository extends ReactiveNeo4jRepository<PersonEntity, Long> {
Mono<PersonEntity> findByName(String name);
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f")
Flux<PersonEntity> findFriendsOf(String name);
}
Do NOT mix imperative and reactive database access in the same application context.
Fragment pattern — use when @Query is not enough.
// 1. Fragment interface
public interface PersonRepositoryCustom {
List<PersonEntity> findByComplexCriteria(String criteria);
}
// 2. Impl — must end with "Impl"
public class PersonRepositoryCustomImpl implements PersonRepositoryCustom {
private final Neo4jClient neo4jClient;
PersonRepositoryCustomImpl(Neo4jClient c) { this.neo4jClient = c; }
@Override
public List<PersonEntity> findByComplexCriteria(String c) {
return new ArrayList<>(neo4jClient
.query("MATCH (p:Person) WHERE p.name CONTAINS $c RETURN p").bind(c).to("c")
.fetchAs(PersonEntity.class)
.mappedBy((t, r) -> { var e = new PersonEntity(); e.setName(r.get("p").asNode().get("name").asString()); return e; })
.all());
}
}
// 3. Compose
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long>, PersonRepositoryCustom {}
Use when @Query is insufficient or you need full control over Cypher execution.
// Bind params + fetch single scalar
neo4jClient.query("MATCH (p:Person {name: $name}) RETURN count(*) AS cnt")
.bind("Alice").to("name")
.fetchAs(Long.class)
.mappedBy((t, r) -> r.get("cnt").asLong())
.one();
// Bind + run write (no result)
neo4jClient.query("MERGE (p:Person {name: $name})")
.bind(personName).to("name")
.run();
// Custom object mapping
neo4jClient.query("MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name=$n RETURN p, collect(m) AS movies")
.bind("Lilly Wachowski").to("n")
.fetchAs(Director.class)
.mappedBy((typeSystem, record) -> new Director(
record.get("p").asNode().get("name").asString(),
record.get("movies").asList(v -> new Movie(v.get("title").asString()))
)).one();
Full API: references/neo4j-client.md
@Service
@Transactional // class-level: all methods transactional
public class PersonService {
@Transactional(readOnly = true) // read-only hint
public Optional<PersonEntity> findByName(String name) { ... }
@Transactional // explicit write
public PersonEntity save(PersonEntity p) { return repository.save(p); }
}
Neo4jTransactionManager auto-configured. Do NOT mix with JPA PlatformTransactionManager without explicit qualifier. Use @Transactional on concrete class, not interface.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>
spring:
ai:
vectorstore:
neo4j:
initialize-schema: true # creates vector index on first run
index-name: my-index
embedding-dimension: 1536 # must match your embedding model
distance-type: cosine # cosine (default) or euclidean
label: Document # node label for stored chunks
embedding-property: embedding # property for the vector
Requires Neo4j 5.15+. Reuses spring.neo4j.* connection config.
@Autowired VectorStore vectorStore;
// Store
vectorStore.add(List.of(new Document("text", Map.of("author", "alice"))));
// Similarity search
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5).similarityThreshold(0.75).build()
);
// With metadata filter
vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5)
.filterExpression("author == 'alice'").build()
);
| Error | Cause | Fix |
|---|---|---|
| MappingException: Could not find entity | Entity not scanned | Check @EnableNeo4jRepositories base package |
| Relationships null after load | Default depth may skip deep rels | Use @Query with RETURN m, collect(r), collect(p) |
| N+1 queries | Per-entity relationship fetch | Rewrite with single @Query; use projections |
| OptimisticLockingFailureException | Stale @Version on concurrent write | Retry in service layer |
| IllegalStateException: Cannot mix reactive/imperative | Both repo types in same context | Pick one stack |
| Projection null fields | Getter name mismatch | Match getter to property name; check @Property alias |
| @Query empty with rels | Missing collect(r), collect(p) | Return root node + rels + related nodes together |
| Cannot delete node, node has relationships | deleteById without detach | Use @Query with DETACH DELETE |
| Transaction not rolling back | @Transactional on interface | Apply on concrete service class |
SDN loads related entities eagerly up to a configured depth (default: 1 hop). For deeper graphs:
// Explicit @Query to control what gets loaded
@Query("""
MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person)
WHERE m.title = $title
RETURN m, collect(r), collect(p)
""")
Optional<MovieEntity> findByTitleWithCast(String title);
collect(r), collect(p) in RETURN is required for SDN to map @RelationshipProperties correctly.
@Node uses explicit label string, not default class name@Id @GeneratedValue (or @Id + @Version for business key with optimistic lock)@RelationshipProperties class has @RelationshipId and @TargetNode@Relationship direction is explicit (OUTGOING / INCOMING)@Query Cypher uses $paramName — no string concatenation@Query returns collect(r), collect(p) alongside root nodeapplication.yml (avoids default DB ambiguity)@Transactional on concrete service class (not interface).env in .gitignorespring.ai.vectorstore.neo4j.initialize-schema: true for first run (Spring AI)This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.
Build agentic applications with GitHub Copilot SDK. Use when embedding AI agents in apps, creating custom tools, implementing streaming responses, managing sessions, connecting to MCP servers, or creating custom agents. Triggers on Copilot SDK, GitHub SDK, agentic app, embed Copilot, programmable agent, MCP server, custom agent.
Coding Agent Session Search - unified CLI/TUI to index and search local coding agent history from Claude Code, Codex, Gemini, Cursor, Aider, ChatGPT, Pi-Agent, Factory, and more. Purpose-built for AI agent consumption with robot mode.
Destructive Command Guard - High-performance Rust hook for Claude Code that blocks dangerous commands before execution. SIMD-accelerated, modular pack system, whitelist-first architecture. Essential safety layer for agent workflows.
Makepad UI development skills for Rust apps: setup, patterns, shaders, packaging, and troubleshooting.
Secure environment variable management ensuring secrets are never exposed in Claude sessions, terminals, logs, or git commits
Prompt for generating an AGENTS.md file for a repository
Take neo4j-contrib/neo4j-spring-data-skill from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.