neo4j-driver-go-skill
Covers the Neo4j Go Driver v6 — driver lifecycle, ExecuteQuery, managed and
Works with
---
name: neo4j-driver-go-skill
description: Covers the Neo4j Go Driver v6 — driver lifecycle, ExecuteQuery, managed and
license: MIT
---
## When to Use
- Writing Go code that connects to Neo4j
- Setting up `neo4j.NewDriver()`, `ExecuteQuery()`, or session/transaction patterns
- Debugging connection errors, result iteration, type assertions, causal consistency
## When NOT to Use
- **Writing/optimizing Cypher** → `neo4j-cypher-skill`
- **v5→v6 migration steps** → `neo4j-migration-skill`
---
## Installation
```bash
go get github.com/neo4j/neo4j-go-driver/v6
```
Import: `github.com/neo4j/neo4j-go-driver/v6/neo4j`
**v5→v6 rename** (deprecated aliases still compile, remove before v7):
| v5 | v6 |
|----|----|
| `neo4j.NewDriverWithContext(...)` | `neo4j.NewDriver(...)` |
| `neo4j.DriverWithContext` | `neo4j.Driver` |
---
## Environment Variables
```go
import "os"
uri := getEnv("NEO4J_URI", "neo4j://localhost:7687")
user := getEnv("NEO4J_USERNAME", "neo4j")
password := getEnv("NEO4J_PASSWORD", "")
database := getEnv("NEO4J_DATABASE", "neo4j")
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" { return v }
return fallback
}
```
Use [godotenv](https://github.com/joho/godotenv) to load `.env` in dev: `godotenv.Load()`. `.env` in `.gitignore`.
---
## Driver Lifecycle
One `Driver` per application. Goroutine-safe, connection-pooled, expensive to create.
```go
func NewNeo4jDriver(uri, user, password string) (neo4j.Driver, error) {
driver, err := neo4j.NewDriver(
uri, // "neo4j+s://xxx.databases.neo4j.io" for Aura
neo4j.BasicAuth(user, password, ""),
)
if err != nil {
return nil, fmt.Errorf("create driver: %w", err)
}
ctx := context.Background()
if err := driver.VerifyConnectivity(ctx); err != nil {
driver.Close(ctx)
return nil, fmt.Errorf("verify connectivity: %w", err)
}
return driver, nil
}
// In main / app teardown:
defer driver.Close(ctx)
```
❌ Never create driver per-request. Create once at startup; share across goroutines.
URI schemes: `neo4j+s://` (Aura/TLS+routing), `neo4j://` (plain+routing), `bolt+s://` (TLS+single), `bolt://` (plain+single).
---
## Choosing the Right API
| API | Use when | Auto-retry | Lazy results |
|-----|----------|:----------:|:------------:|
| `neo4j.ExecuteQuery()` | Most queries — simple default | ✅ | ❌ eager |
| `session.ExecuteRead/Write()` | Large result sets / streaming | ✅ | ✅ |
| `session.BeginTransaction()` | Spans multiple functions / ext coordination | ❌ | ✅ |
| `session.Run()` | `CALL IN TRANSACTIONS` / auto-commit only | ❌ | ✅ |
`CALL { … } IN TRANSACTIONS` and `USING PERIODIC COMMIT` manage their own transactions — use `session.Run()`. They fail inside managed transactions.
---
## ExecuteQuery (Recommended Default)
Manages sessions, transactions, retries, and bookmarks automatically.
```go
result, err := neo4j.ExecuteQuery(ctx, driver,
`MATCH (p:Person {name: $name})-[:KNOWS]->(friend)
RETURN friend.name AS name`,
map[string]any{"name": "Alice"},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"), // always specify
neo4j.ExecuteQueryWithReadersRouting(), // for read queries
)
if err != nil {
return fmt.Errorf("query people: %w", err)
}
for _, record := range result.Records {
name, _ := record.Get("name")
fmt.Println(name)
}
fmt.Println(result.Summary.Counters().NodesCreated())
```
Key options:
```go
neo4j.ExecuteQueryWithDatabase("mydb") // required for performance
neo4j.ExecuteQueryWithReadersRouting() // route reads to replicas
neo4j.ExecuteQueryWithImpersonatedUser("jane") // impersonate
neo4j.ExecuteQueryWithoutBookmarkManager() // opt out of causal consistency
```
❌ Never concatenate user input into query strings. Always use `map[string]any` parameters.
Struct parameters [v6.2.0+, Object Mapping preview] — a struct (or pointer) in `params` is sent as a Cypher map; same rules in `Session.Run`, `ManagedTransaction.Run`, `ExplicitTransaction.Run`:
```go
type Person struct {
Name string `neo4j:"name"`
Age int `neo4j:"age,omitempty"` // zero value dropped
Internal string `neo4j:"-"` // never sent
}
_, err := neo4j.ExecuteQuery(ctx, driver,
"MERGE (p:Person {name: $person.name}) SET p += $person",
map[string]any{"person": Person{Name: "Alice", Age: 30}},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"),
)
```
---
## Managed Transactions (Session-Based)
Use for lazy streaming (large result sets) or callback-level control.
```go
session := driver.NewSession(ctx, neo4j.SessionConfig{
DatabaseName: "neo4j", // always specify
AccessMode: neo4j.AccessModeRead,
})
defer session.Close(ctx)
result, err := session.ExecuteRead(ctx,
func(tx neo4j.ManagedTransaction) (any, error) {
res, err := tx.Run(ctx,
`MATCH (p:Person) RETURN p.name AS name LIMIT $limit`,
map[string]any{"limit": 100},
)
if err != nil {
return nil, err
}
var names []string
for res.Next(ctx) { // lazy — don't Collect() on large sets
name, _ := res.Record().Get("name")
names = append(names, name.(string))
}
return names, res.Err()
},
)
```
❌ No side effects in callback — retried on transient failures.
`ExecuteRead` → replicas. `ExecuteWrite` → cluster leader.
---
## Explicit Transactions
Use when transaction work spans multiple functions or requires external coordination.
```go
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
tx, err := session.BeginTransaction(ctx)
if err != nil {
return err
}
if err := doPartA(ctx, tx); err != nil {
tx.Rollback(ctx)
return err
}
if err := doPartB(ctx, tx); err != nil {
tx.Rollback(ctx)
return err
}
return tx.Commit(ctx)
```
❌ Not auto-retried. Caller handles retry. Prefer managed transactions unless you need explicit control.
---
## Error Handling
```go
result, err := neo4j.ExecuteQuery(...)
if err != nil {
var neo4jErr *neo4j.Neo4jError
if errors.As(err, &neo4jErr) {
slog.Error("database error", "code", neo4jErr.Code, "msg", neo4jErr.Msg)
}
var connErr *neo4j.ConnectivityError
if errors.As(err, &connErr) {
slog.Error("connectivity error", "err", connErr)
}
return fmt.Errorf("execute query: %w", err)
}
```
Helpers:
```go
neo4j.IsNeo4jError(err) // server-side Cypher/database error
neo4j.IsTransactionExecutionLimit(err) // managed tx retries exhausted
```
In managed tx callback: return error → driver retries if transient.
`ConnectivityError` at startup: check URI scheme, credentials, firewall.
---
## Data Types
| Cypher | Go |
|--------|----|
| `Integer` | `int64` |
| `Float` | `float64` |
| `String` | `string` |
| `Boolean` | `bool` |
| `List` | `[]any` |
| `Map` | `map[string]any` |
| `Node` | `neo4j.Node` |
| `Relationship` | `neo4j.Relationship` |
| `Path` | `neo4j.Path` |
| `Date` | `neo4j.Date` |
| `DateTime` | `neo4j.Time` |
| `Duration` | `neo4j.Duration` |
| `UUID` | `neo4j.UUID` (= `dbtype.UUID`, `[16]byte` RFC 9562) — v6.2.0+, Bolt 6.1; parse with `dbtype.ParseUUID(s)`, render with `.String()` |
| `null` | `nil` |
```go
// Typed extraction (v6+, preferred):
neo4j.GetRecordValue[string](record, "name")
// Manual extraction:
rawAge, ok := record.Get("age")
if !ok { return errors.New("missing 'age' field") }
age := rawAge.(int64) // Neo4j integers → int64
// Node access:
rawNode, _ := record.Get("p")
node := rawNode.(neo4j.Node)
name := node.Props["name"].(string)
labels := node.Labels // []string
```
❌ Always check `ok` from `record.Get()` before type-asserting — panics on missing key.
❌ After lazy `for res.Next(ctx)` loop, always check `res.Err()`.
---
## Key Patterns
### Context — always propagate
```go
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
// pass ctx to all driver calls
```
`context.Background()` has no deadline — slow queries block indefinitely.
### Batching Writes
```go
// Bad: one transaction per record
for _, item := range items {
neo4j.ExecuteQuery(ctx, driver, writeQuery, item, ...)
}
// Good: UNWIND batch in one transaction
neo4j.ExecuteQuery(ctx, driver,
`UNWIND $items AS item
MERGE (n:Node {id: item.id})
SET n += item`,
map[string]any{"items": items},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"),
)
```
### Generic Helpers (v6+)
Prefer type-safe helpers over manual assertions:
```go
// GetRecordValue[T] — extract + cast in one call
name, isNil, err := neo4j.GetRecordValue[string](record, "name")
// isNil=true when OPTIONAL MATCH returned null; err != nil when key absent or wrong type
// CollectTWithContext — map all records to a slice
people, err := neo4j.CollectTWithContext(ctx, result, func(record *neo4j.Record) (Person, error) {
name, _, err := neo4j.GetRecordValue[string](record, "name")
age, _, _ := neo4j.GetRecordValue[int64](record, "age")
return Person{Name: name, Age: int(age)}, err
})
// SingleTWithContext — expect exactly one record (error if 0 or 2+)
person, err := neo4j.SingleTWithContext(ctx, result, func(record *neo4j.Record) (Person, error) {
name, _, _ := neo4j.GetRecordValue[string](record, "name")
return Person{Name: name}, nil
})
// GetProperty — typed property from Node or Relationship
node, _, _ := neo4j.GetRecordValue[neo4j.Node](record, "p")
nameVal, err := neo4j.GetProperty[string](node, "name")
```
### Spatial Types
```go
// 2D Cartesian (SRID 7203), 3D Cartesian (SRID 9157)
pt2d := neo4j.Point2D{X: 1.23, Y: 4.56, SpatialRefId: 7203}
pt3d := neo4j.Point3D{X: 1.23, Y: 4.56, Z: 7.89, SpatialRefId: 9157}
// 2D WGS-84 (SRID 4326), 3D WGS-84 (SRID 4979)
london := neo4j.Point2D{X: -0.118092, Y: 51.509865, SpatialRefId: 4326}
shard := neo4j.Point3D{X: -0.0865, Y: 51.5045, Z: 310, SpatialRefId: 4979}
// Pass as parameter
result, err := neo4j.ExecuteQuery(ctx, driver,
"CREATE (p:Place {location: $loc})",
map[string]any{"loc": london},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"),
)
// Read from result — assert to Point2D or Point3D
raw, _ := record.Get("location")
if p2d, ok := raw.(neo4j.Point2D); ok {
fmt.Printf("lon=%f lat=%f srid=%d\n", p2d.X, p2d.Y, p2d.SpatialRefId)
}
// Distance (same SRID only)
result, _ = neo4j.ExecuteQuery(ctx, driver,
"RETURN point.distance($p1, $p2) AS distance",
map[string]any{"p1": pt2d, "p2": neo4j.Point2D{X: 10, Y: 10, SpatialRefId: 7203}},
neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"),
)
dist, _ := result.Records[0].Get("distance")
fmt.Println(dist.(float64))
```
### Always Specify Database
```go
neo4j.ExecuteQueryWithDatabase("neo4j") // in ExecuteQuery
neo4j.SessionConfig{DatabaseName: "neo4j"} // in sessions
```
Omitting costs a network round-trip per call to resolve home database.
### Causal Consistency
`ExecuteQuery` manages bookmarks automatically — no action needed for sequential calls.
Cross-session (parallel workers): combine bookmarks explicitly — see [references/repository-pattern.md](references/repository-pattern.md).
---
## Common Errors
| Error / Symptom | Cause | Fix |
|-----------------|-------|-----|
| `ConnectivityError` at startup | URI wrong / TLS mismatch / firewall | Check scheme (`neo4j+s://` for Aura), credentials, port 7687 |
| `ConnectivityError` mid-run | Pool exhausted | Increase `MaxConnectionPoolSize`; check for leaked sessions |
| Panic on type assertion | `record.Get()` returned nil/wrong type | Use `neo4j.GetRecordValue[T]()` or check `ok` first |
| `res.Err()` non-nil after loop | Network error mid-stream | Handle error; re-run transaction |
| Callback retried unexpectedly | Side effect inside managed tx | Move side effects outside callback |
| Context deadline exceeded | No timeout on context | Use `context.WithTimeout` |
| 0 results, query looks correct | Wrong `DatabaseName` | Always set `DatabaseName` in config |
| `CALL IN TRANSACTIONS` fails | Run inside managed tx | Use `session.Run()` (auto-commit) |
---
## References
Load on demand:
- [references/advanced-config.md](references/advanced-config.md) — connection pool tuning, custom address resolver, notification config, Bolt logging, auth options, URI scheme table, deprecated result-summary accessors (`Profile()` → `QueryProfile()`)
- [references/repository-pattern.md](references/repository-pattern.md) — repository wrapper pattern, cross-session causal consistency with bookmarks
## WebFetch
| Need | URL |
|------|-----|
| Go driver manual | `https://neo4j.com/docs/go-manual/current/` |
| API reference | `https://pkg.go.dev/github.com/neo4j/neo4j-go-driver/v6/neo4j` |
---
## Checklist
- [ ] One driver created at startup; shared across goroutines; `defer driver.Close(ctx)`
- [ ] `driver.VerifyConnectivity(ctx)` called at startup
- [ ] `DatabaseName` set in all `SessionConfig` / `ExecuteQueryWithDatabase`
- [ ] `context.WithTimeout` used for production queries
- [ ] `map[string]any` parameters used — no string interpolation
- [ ] `ExecuteQueryWithReadersRouting()` on read-only `ExecuteQuery` calls
- [ ] `res.Err()` checked after lazy `for result.Next(ctx)` loop
- [ ] Type assertions guarded (use `GetRecordValue[T]` or check `ok`)
- [ ] No side effects inside managed transaction callbacks
- [ ] `session.Run()` used for `CALL IN TRANSACTIONS` / auto-commit queries
- [ ] Sessions closed with `defer session.Close(ctx)`More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

