Neo4j APOC Procedures: The Production Standard Library for Graph Applications
More Than a Plugin
APOC — Awesome Procedures on Cypher — is the standard library for Neo4j. With over 450 procedures and functions spanning data integration, graph refactoring, temporal arithmetic, geospatial computation, text processing, and operational monitoring, it is the single most important plugin in any production Neo4j deployment. Running Neo4j without APOC is like running PostgreSQL without its contrib modules: possible, but you will end up reimplementing wheel after wheel.
This article surveys the APOC procedure categories that deliver the most value in production, with concrete examples for each. By the end, you will know which APOC module to reach for in every common graph engineering scenario.
Installation and Configuration
APOC ships as a Neo4j plugin. In Docker, add it via the NEO4J_PLUGINS environment variable:
docker run \
--name neo4j-apoc \
--publish=7474:7474 --publish=7687:7687 \
-e NEO4J_AUTH=neo4j/password \
-e NEO4J_PLUGINS='["apoc"]' \
neo4j:5
For self-managed deployments, download the APOC JAR matching your Neo4j version from the APOC releases page and place it in the plugins/ directory. APOC Core (the procedures covered in this article) is fully open-source under the Apache 2.0 licence. APOC Extended, which includes additional procedures for AWS, MongoDB, and other external systems, is a separate download for Neo4j Enterprise customers.
Verify the installation:
RETURN apoc.version();
You should see the installed APOC version. If not, check that the JAR is in the right directory and that dbms.security.procedures.unrestricted=gds.*,apoc.* is set in neo4j.conf.
Categories of Procedures
APOC's procedure catalogue is vast, but the procedures fall into a manageable set of functional categories:
| Category | What It Does | Key Procedures | Production Use Case |
|---|---|---|---|
| Data integration | Load and export JSON, CSV, XML, JDBC | apoc.load.json, apoc.load.csv, apoc.export.csv | Ingest data from REST APIs and files |
| Graph refactoring | Merge, split, clone, and restructure nodes and relationships | apoc.refactor.mergeNodes, apoc.refactor.normalizeAsBoolean | Schema migration, data deduplication |
| Temporal | Date, time, duration arithmetic and formatting | apoc.date.parse, apoc.date.add, apoc.temporal.format | Time-series analysis, expiry logic |
| Geospatial | Distance calculation, path routing, coordinate conversion | apoc.spatial.geocode, apoc.spatial.distance | Location-aware queries, proximity search |
| Text | String cleaning, tokenisation, fuzzy matching | apoc.text.clean, apoc.text.fuzzySearch, apoc.text.urlencode | Data normalisation, search |
| Monitoring | Query logging, memory inspection, schema inspection | apoc.monitor.query, apoc.meta.schema, apoc.memory.maps | Performance troubleshooting, observability |
| Cypher execution | Dynamic and batch query execution | apoc.periodic.iterate, apoc.cypher.run, apoc.cypher.runMany | Bulk updates, ETL pipelines |
| Utilities | UUID generation, node/relationship inspection, labelling | apoc.create.uuid, apoc.create.setLabels, apoc.merge.relationship | Data enrichment, idempotent ingestion |
Let us walk through each category with production-relevant examples.
Data Integration
The most common use of APOC in production is loading external data into the graph. APOC supports JSON, CSV, XML, and JDBC sources.
Loading JSON from an API
WITH "https://api.github.com/repos/neo4j/neo4j/releases" AS url
CALL apoc.load.json(url) YIELD value
RETURN value.tag_name AS tag,
value.published_at AS published,
value.prerelease AS prerelease
ORDER BY published DESC
LIMIT 10;
This pattern — apoc.load.json with a URL — streams JSON arrays directly into Cypher rows. Each array element becomes one row, and nested documents are accessible via dot-notation path expressions.
For local files, use the file:// protocol:
CALL apoc.load.json("file:///import/sales_data.json") YIELD value
MERGE (p:Product {id: value.product_id})
ON CREATE SET p.name = value.product_name, p.price = value.price;
Loading CSV with Error Handling
CSV loading in APOC supports quoting, delimiters, and header auto-detection:
CALL apoc.load.csv("https://data.example.com/users.csv",
{header: true, sep: ",", nullValues: ["N/A", "NULL"], ignoreQuotations: false}
) YIELD map AS row
MERGE (u:User {email: row.email})
SET u.name = row.full_name, u.signup_date = date(row.signup_date);
The map output gives you a dictionary keyed by header name. APOC handles encoding detection and quoted fields automatically — the same options that take hours to configure in a bespoke ETL script are a single procedure call.
Bulk Export for Backup
The reverse direction — exporting subgraphs — is equally important for data science and archiving:
CALL apoc.export.csv.query(
"MATCH (a:Author)-[:AUTHORED]->(p:Paper)-[:ABOUT]->(t:Topic)
RETURN a.name AS author, p.title AS paper, t.name AS topic
ORDER BY a.name",
"author_papers.csv",
{}
);
Exported files land in the Neo4j import/ directory by default. APOC supports CSV, JSON, GraphML, and Cypher-script formats.
Graph Refactoring
Graph refactoring procedures are the unsung heroes of schema migration. They let you restructure the graph without writing complex multi-step Cypher.
Merging Duplicate Nodes
Entity resolution inevitably leaves duplicates. APOC's mergeNodes combines them in a single call:
MATCH (p:Person)
WHERE p.email IS NOT NULL
WITH p.email AS email, collect(p) AS duplicates
WHERE size(duplicates) > 1
CALL apoc.refactor.mergeNodes(duplicates, {
properties: "combine",
mergeRels: true
}) YIELD node
RETURN node.name, node.email;
The properties option controls conflict resolution: combine merges all property values into an array, discard keeps the first node's values, and override overwrites with the last. The mergeRels: true option consolidates relationships, reconnecting all related nodes to the surviving node.
Normalising Property Types
When data arrives with inconsistent types — "true" and false as strings instead of booleans — APOC normalises them:
MATCH (u:User)
WHERE u.is_active IN ["true", "false"]
CALL apoc.refactor.normalizeAsBoolean(u, "is_active", ["true"], ["false"])
YIELD result
SET u.is_active = result;
This is particularly valuable after ingesting from CSV or JSON sources that lack schema enforcement.
Cloning a Subgraph for Testing
Need a test fixture that mirrors a production subgraph?
MATCH (p:Project {name: "GraphWiz"})
CALL apoc.refactor.cloneSubgraph([p], {extraNodes: 5}) YIELD input, output, error
RETURN input, output, error;
This clones the matched node and up to five connected neighbours, preserving the relationship structure — useful for spinning up sandbox environments from production snapshots.
Temporal Procedures
Neo4j has native temporal types (Date, LocalDateTime, Duration) but APOC extends them with parsing, formatting, and arithmetic that Cypher alone does not cover.
Parsing Arbitrary Date Formats
Log files and external APIs send dates in every format imaginable. APOC parses them all:
// Parse RFC 2822 date strings
RETURN apoc.date.parse("Thu, 18 Jul 2026 14:30:00 +0000",
"s", "EEE, dd MMM yyyy HH:mm:ss Z") AS epoch_seconds;
// Parse ISO 8601 with timezone
RETURN apoc.date.parse("2026-07-18T14:30:00Z",
"s", "yyyy-MM-dd'T'HH:mm:ss'Z'") AS epoch_seconds;
// Human-readable relative dates
RETURN apoc.date.parse("2 days ago", "s", "RELATIVE") AS epoch_seconds;
The "s" parameter specifies the target unit (seconds). Replace with "ms" for milliseconds or specify a pattern string for custom output formats.
Date Arithmetic for Time-Series
Temporal procedures shine for time-series and expiry logic:
// Add 90 days to a date
MATCH (c:Contract)
RETURN c.id,
c.start_date,
apoc.date.add(c.start_date, "d", 90) AS renewal_window;
// Date difference in business days
MATCH (t:Task)
RETURN t.name,
t.started_at,
t.completed_at,
apoc.date.difference(t.started_at, t.completed_at, "d") AS calendar_days;
Field Extractor for Time Buckets
Grouping events by hour, day, or month is a common analytical pattern:
MATCH (e:Event)
WHERE e.timestamp > datetime() - duration({hours: 24})
RETURN apoc.date.field(e.timestamp, "hour") AS hour_bucket,
count(*) AS event_count
ORDER BY hour_bucket;
Geospatial Procedures
Neo4j has spatial types (Point) for coordinates, but APOC extends them with distance calculations, geocoding, and spatial path utilities.
Distance Between Points
WITH point({latitude: 52.52, longitude: 13.405}) AS berlin,
point({latitude: 48.135, longitude: 11.582}) AS munich
RETURN apoc.spatial.distance(berlin, munich) AS distance_metres;
This returns the great-circle distance in metres. For routing applications, combine with apoc.spatial.path to find the shortest sequence of connected spatial nodes.
Reverse Geocoding
CALL apoc.spatial.reverseGeocode(52.52, 13.405) YIELD address
RETURN address;
This returns a structured address from OpenStreetMap data — useful for enriching transaction or delivery nodes with human-readable locations.
Geocoding for Data Enrichment
When loading data that contains only city names or postcodes, APOC resolves coordinates:
CALL apoc.spatial.geocodeOnce("Berlin, Germany") YIELD latitude, longitude, description
RETURN latitude, longitude, description;
The geocoder supports multiple providers — OpenStreetMap (default), Google Maps, and Azure Maps — configurable via the apoc.conf file.
Text Procedures
Text-processing procedures handle the string-cleaning and normalisation tasks that are tedious in pure Cypher.
String Cleaning for Entity Resolution
RETURN apoc.text.clean(" Neo4j, Inc. ") AS clean_string;
-- Result: "neo4j inc"
apoc.text.clean lower-cases, strips punctuation, removes extra whitespace, and drops common stop-words — perfect as a pre-processing step before entity matching.
Fuzzy Search for Name Matching
WITH ["Neo Technology", "Neo4j Inc.", "Neo4j Sweden AB"] AS companies
UNWIND companies AS company
WITH company
WHERE apoc.text.fuzzyScore(company, "Neo4j Inc.") > 0.7
RETURN company, apoc.text.fuzzyScore(company, "Neo4j Inc.") AS score
ORDER BY score DESC;
fuzzyScore returns a normalised similarity score using the Levenshtein-derived Optimal String Alignment distance. Combined with the apoc.text.clean procedure, it captures the majority of real-world name variations without an external matching service.
Phonetic Matching for Names
MATCH (a:Author), (b:Author)
WHERE a <> b
AND apoc.text.phonetic(a.last_name) = apoc.text.phonetic(b.last_name)
RETURN a.name AS name1, b.name AS name2;
apoc.text.phonetic uses the Soundex algorithm, which matches names that sound alike but are spelled differently — "Smith" and "Smyth", "Johnson" and "Jonson". This is invaluable for deduplicating author or customer records sourced from manual entry.
Cypher Execution and Batch Processing
The apoc.periodic.iterate procedure is the most important procedure in APOC for production ETL. It breaks large operations into manageable batches with commit control.
Batch Processing Without Transaction Overflow
A naive MATCH ... SET ... on 10 million nodes will exhaust transaction memory. apoc.periodic.iterate splits the work:
CALL apoc.periodic.iterate(
"MATCH (n:Entity) WHERE n.embedding IS NULL RETURN n",
"SET n.embedding = genai.vector.encode(n.description, 'openai')",
{batchSize: 100, parallel: true, retries: 3}
) YIELD batches, total, timeTaken, committed, failed, errorMessages
RETURN batches, total, timeTaken, committed, failed;
The first Cypher statement is the iterate query — it feeds rows to the second action query in batches of batchSize rows. The parallel: true option runs action batches concurrently, and retries: 3 handles transient failures. This single procedure replaces what would otherwise require a custom background job with commit management.
Dynamic Query Execution
When you need to generate Cypher dynamically — for example, running the same operation across every node label — use apoc.cypher.run:
CALL apoc.meta.schema() YIELD label, properties
WITH label
WHERE properties IS NOT NULL
CALL apoc.cypher.run(
"MATCH (n:" + label + ") RETURN count(n) AS count",
{}
) YIELD value
RETURN label, value.count AS node_count;
The apoc.cypher.runMany variant accepts multiple semicolon-separated queries in a single string, executing them sequentially in the same transaction — useful for migration scripts.
Monitoring and Observability
APOC exposes Neo4j's internal state through procedures that would otherwise require JMX or direct metrics access.
Schema Inspection
CALL apoc.meta.schema() YIELD label, properties, relationships
RETURN label,
properties,
relationships
ORDER BY label;
This outputs every node label in the database with its properties, their types, and incoming/outgoing relationship types. It is the fastest way to document your graph schema — especially valuable during code reviews and onboarding.
Slow Query Detection
CALL apoc.monitor.query(true) YIELD query, elapsed, params
WHERE elapsed > 1000
RETURN query, elapsed, params
ORDER BY elapsed DESC
LIMIT 10;
The apoc.monitor.query procedure exposes the same data as Neo4j's query log but without needing to parse log files. Pass true to include the query text and parameters.
Memory and Lock Inspection
// Current heap memory usage
CALL apoc.memory.maps() YIELD map, section, usage, committed
RETURN map, section, usage
ORDER BY usage DESC;
// Locks held by transactions
CALL apoc.lock.list() YIELD thread, label, resource, mode
RETURN thread, label, resource, mode;
These are invaluable when diagnosing contention in write-heavy workloads or memory pressure during large batch operations.
Comparison: APOC vs Native Cypher Capabilities
| Operation | Without APOC | With APOC |
|---|---|---|
| Load JSON from URL | Custom HTTP client | apoc.load.json(url) |
| Merge duplicate entities | Complex multi-step Cypher with MERGE | apoc.refactor.mergeNodes() |
| Parse non-standard date strings | Manual regex extraction | apoc.date.parse() |
| Batch large updates | Transaction splitting in client code | apoc.periodic.iterate() |
| Distance between coordinates | Manual Haversine implementation | apoc.spatial.distance() |
| Fuzzy string matching | External library | apoc.text.fuzzyScore() |
| Schema introspection | CALL db.schema.visualization() (limited) | apoc.meta.schema() |
| UUID generation | External or manual | apoc.create.uuid() |
Best Practices
Restrict APOC procedures by security context. Not every procedure belongs in every context. In production, configure apoc.<category>.procedure.name whitelists:
# Only allow read-only APOC procedures for application users
apoc.jdbc.<name>.enabled=false
apoc.spatial.<name>.enabled=true
apoc.export.<name>.enabled=false
Use apoc.periodic.iterate for any write operation affecting more than 10,000 nodes. The batch-size parameter prevents transaction heap exhaustion. Start with batchSize: 1000 and tune downward if you encounter memory pressure.
Prefer apoc.load.json over loading from CSV for deeply nested data. JSON preserves the hierarchical structure that you would lose in CSV flattening. Use CSV only when the source data is strictly tabular.
Always add limit to apoc.meta.schema() in large databases. On a database with hundreds of labels, the schema output can exceed the result-buffer limit. Filter by label prefix or sample specific labels:
CALL apoc.meta.schema() YIELD label, properties
WHERE label STARTS WITH "Product"
RETURN label, properties;
Version-lock APOC to your Neo4j major version. APOC releases follow Neo4j's versioning. Using APOC 5.23 with Neo4j 5.26 is safe, but using APOC 4.x with Neo4j 5.x will fail. The APOC compatibility matrix is maintained on the APOC documentation site.
Further Reading
- Graph Data Modeling Patterns — designing the schemas that APOC procedures operate on
- Cypher Query Optimisation Techniques — writing queries that perform well with APOC batch processing
- Knowledge Graph Construction from Unstructured Data — the ETL pipelines that rely on APOC data loading
- Neo4j Database Adapters Compared — choosing the right driver for programmatic APOC integration
- Introduction to GraphRAG — building graph-augmented AI pipelines with APOC-powered ingestion
The official APOC Documentation is the definitive reference. Every procedure in this article has additional options and edge-case handling documented there. Bookmark the category index — it is the page you will reach for more than any other when building production Neo4j applications.