APOC Procedures: Production Patterns for Neo4j's Swiss Army Knife
Beyond Core Cypher
Cypher is elegant for graph queries, but production Neo4j work inevitably hits its boundaries. You need to convert a date string to a temporal type. Copy a subgraph between databases. Trigger side effects on writes. Generate UUIDs for new nodes. Parse JSON from an API response.
This is where APOC (Awesome Procedures on Cypher) enters — Neo4j's official standard library of 500+ procedures and functions that extend Cypher into a full-fledged data processing environment.
Despite being practically mandatory for production use, APOC is rarely given systematic treatment. This article covers the procedure categories that deliver the most leverage in real deployments: data conversion, graph refactoring, triggers, full-text search, temporal operations, and ETL. Each section includes working Cypher and, where relevant, the performance trade-offs to consider.
Installing and Verifying APOC
APOC ships as a Neo4j plugin. In Docker, mount the plugins directory and set the unrestricted procedures list:
volumes:
- ./plugins:/plugins
environment:
NEO4J_dbms_security_procedures_unrestricted: apoc.*
NEO4J_server_apoc_import_file_enabled: "true" # for CSV/JSON import
Once the jar (matching your Neo4j version) is in place, verify the installation:
RETURN apoc.version();
For production, never set dbms.security.procedures.unrestricted to apoc.* without reviewing what each procedure does. Some APOC procedures (file operations, OS commands, subgraph deletes) are powerful enough to be dangerous. Use a whitelist like apoc.trigger.*,apoc.convert.* where only specific packages are needed.
Data Conversion: The Most-Used Category
The single most common production use of APOC is type conversion — because Neo4j's property types are limited (string, number, boolean, list, map), and real-world data arrives as JSON, ISO date strings, or comma-separated values.
JSON to Graph
APOC can parse JSON strings into Cypher maps and lists, making it straightforward to ingest API responses:
WITH '{"repo": "neo4j", "stars": 12500, "topics": ["graph", "database"]}' AS json
CALL apoc.convert.fromJson(json) YIELD value
RETURN value.repo AS repository, value.stars AS stars
When the JSON is an array, use fromJsonList:
WITH '[{"name": "APOC"}, {"name": "GDS"}, {"name": "Bloom"}]' AS json
UNWIND apoc.convert.fromJsonList(json) AS product
RETURN product.name
This pattern is the foundation of any JSON-to-graph ETL pipeline. Combined with UNWIND and MERGE, you can populate an entire graph from a single JSON payload without writing a line of application code.
Type Coercion
APOC provides explicit type coercion where Cypher leaves you guessing:
// String to integer
RETURN apoc.convert.parseInt("42") AS number;
// String to float
RETURN apoc.convert.parseFloat("3.14") AS pi;
// Anything to boolean
RETURN apoc.convert.toBoolean("true") AS bool;
Conversion Decision Matrix
| Source Type | APOC Function | Use Case | Performance |
|---|---|---|---|
| JSON string → map | apoc.convert.fromJson() | API responses, webhooks | O(n) — negligible |
| JSON string → list | apoc.convert.fromJsonList() | Batch ingestion | O(n) — negligible |
| String → int | apoc.convert.parseInt() | CSV import, form data | Constant |
| String → boolean | apoc.convert.toBoolean() | Config flags, feature toggles | Constant |
| Any → Cypher types | apoc.convert.toTree() | Nested object expansion | O(n) — watch depth |
| List → string | apoc.text.join() | Export, reporting | O(n) |
Graph Refactoring: Evolving Your Model
One of APOC's superpowers is automated graph refactoring — renaming labels, splitting properties, and merging duplicate nodes without writing complex Cypher.
Merging Duplicate Nodes
Entity resolution produces candidate matches that must be merged. APOC's merge procedures handle this atomically:
// Merge two Person nodes, combining their properties
MATCH (a:Person {id: "alice-1"}), (b:Person {id: "alice-2"})
CALL apoc.refactor.mergeNodes([a, b], {
properties: "combine",
mergeRels: true
}) YIELD node
RETURN node
The properties parameter controls conflict resolution: "combine" keeps both values as a list, "discard" keeps the first node's value, "override" takes the second node's value, and "keep" preserves the first node's value unchanged.
Renaming Labels and Relationships
When your data model evolves, APOC refactors the entire graph without manual migration scripts:
// Rename a label
CALL apoc.refactor.rename.label("Employee", "Contractor");
// Rename a relationship type
CALL apoc.refactor.rename.type("WORKS_AT", "CONTRACTED_BY");
These operations are transactional — they run in a single transaction and can be rolled back if something goes wrong. For large graphs, run them during a maintenance window with the database in single-user mode.
Subgraph Copying
Need to move a subset of your graph between databases or environments? APOC's subgraph export:
// Export a subgraph as Cypher statements
MATCH path = (a:Person {id: "alice"})-[*1..2]-(neighbour)
CALL apoc.export.cypher.data(path, null, {
stream: true,
format: "plain"
}) YIELD cypherStatements
RETURN cypherStatements
The output is a sequence of CREATE statements that can be replayed against another database — useful for seeding test environments or isolating problematic subgraphs for analysis.
Triggers: Event-Driven Reactions
APOC triggers let you execute Cypher logic automatically when data changes — Neo4j's equivalent of database triggers in relational systems.
The canonical use case is automatic timestamping:
CALL apoc.trigger.add(
'setTimestamps',
'UNWIND $createdNodes AS n
SET n.createdAt = timestamp(),
n.updatedAt = timestamp()',
{phase: 'before'}
);
Now every CREATE assigns createdAt and updatedAt automatically. For updates:
CALL apoc.trigger.add(
'updateTimestamps',
'UNWIND $updatedNodes AS n
SET n.updatedAt = timestamp()',
{phase: 'before'}
);
Performance warning: Triggers fire synchronously within the writing transaction. A slow trigger directly increases write latency. Profile trigger execution time with:
// Check trigger performance
CALL apoc.trigger.list() YIELD name, installed, paused
RETURN *
If installed is true but a trigger is slowing writes, pause it without removing the definition:
CALL apoc.trigger.pause('updateTimestamps');
Trigger Capabilities at a Glance
| Feature | APOC Procedure | Notes |
|---|---|---|
| Automatic timestamps | apoc.trigger.add with $createdNodes/$updatedNodes | Most common trigger use |
| Audit logging | apoc.trigger.add with $assignedNodeProperties | Track who changed what |
| Cascading updates | apoc.trigger.add on certain label changes | Recompute denormalised counts |
| Constraint enforcement | apoc.trigger.add in before phase | Reject invalid writes |
| Read-only nodes | apoc.trigger.add on $deletedNodes | Prevent accidental deletion |
Temporal Operations
Working with dates and durations in Cypher alone is surprisingly limited. APOC provides a complete temporal toolkit:
// Parse ISO-8601 into a Cypher DateTime
RETURN apoc.date.parse("2026-07-19T14:30:00Z", "s", "yyyy-MM-dd'T'HH:mm:ss'Z'") AS epoch;
// Format a timestamp for display
RETURN apoc.date.format(timestamp(), "yyyy-MM-dd HH:mm") AS formatted;
// Add business days (skip weekends)
RETURN apoc.date.add(timestamp(), "d", 5) AS fiveWeekdaysFromNow;
For time-series data modelled as linked events, the temporal converter helps transform raw timestamps into Cypher's native DateTime type, enabling efficient range queries:
// Convert stored epoch to DateTime for range filtering
MATCH (e:Event)
WHERE e.timestamp > 1721300000
WITH e, apoc.date.convert(e.timestamp, "ms", "datetime") AS dt
WHERE dt.month = 7 AND dt.year = 2026
RETURN e.name, dt
Full-Text Search
While Neo4j supports CONTAINS and STARTS WITH for string matching, these cannot use indexes efficiently for substring searches. APOC bridges this gap by exposing Neo4j's Lucene-backed full-text indexes:
// Add a full-text index on Person names
CALL apoc.index.addAllNodes('personIndex', {
Person: ["name", "biography"]
});
// Query with Lucene syntax
CALL apoc.index.search('personIndex',
'name: alice~ OR biography: engineer~',
10)
YIELD node, score
RETURN node.name, score
ORDER BY score DESC;
The tilde ~ enables fuzzy matching (Levenshtein distance), useful for catching spelling variations. The same Lucene query syntax supports wildcards, phrase queries, and boolean operators.
ETL: Loading External Data
APOC can read CSV, JSON, and XML directly from the filesystem or via HTTP, making it a self-contained ETL engine:
// Load CSV from URL and create the graph
LOAD CSV WITH HEADERS FROM
'https://data.example.com/products.csv' AS row
CALL apoc.merge.node(
['Product', row.Category],
{id: row.ProductID},
{name: row.ProductName, price: toFloat(row.Price)}
) YIELD node
RETURN count(node);
The apoc.merge.node procedure is particularly valuable: it combines MERGE semantics (find or create) with dynamic label assignment — the node automatically gets the Product label and a label matching its category.
For JSON APIs, the pattern is:
WITH 'https://api.github.com/repos/neo4j/neo4j' AS url
CALL apoc.load.json(url) YIELD value
MERGE (r:Repository {id: value.id})
SET r.name = value.name,
r.stars = value.stargazers_count,
r.description = value.description
Security and Performance Considerations
APOC's breadth means some procedures carry risk. Follow these rules in production:
| Risk | Procedure Category | Mitigation |
|---|---|---|
| File system access | apoc.load.*, apoc.export.* | Restrict to specific directories via apoc.import.file.allowlist |
| OS command execution | apoc.os.* | Disable entirely — only enable if you have a concrete use case |
| Large subgraph deletes | apoc.refactor.delete.* | Always run with LIMIT in a test transaction first |
| Trigger overhead | apoc.trigger.* | Profile with apoc.trigger.list(); pause unused triggers |
| Memory from large JSON | apoc.load.json | Set apoc.import.file.use_nio to true for streaming |
A sensible baseline configuration:
apoc.import.file.enabled=true
apoc.import.file.allowlist=/data/imports/*
apoc.export.file.enabled=true
apoc.trigger.enabled=true
# Disable dangerous categories
apoc.os.enabled=false
Putting It Together: A Real-World Pipeline
Here is a complete workflow that combines multiple APOC packages — the kind of operation that would require hundreds of lines of application code without APOC:
// Step 1: Load JSON orders from an API
CALL apoc.load.json("https://api.example.com/orders?date=2026-07-19")
YIELD value
// Step 2: Unwind and create the graph
UNWIND value.orders AS order
MERGE (c:Customer {email: order.customer.email})
SET c.name = order.customer.name
MERGE (p:Product {sku: order.product.sku})
SET p.name = order.product.name,
p.price = apoc.convert.parseFloat(order.product.price)
// Step 3: Create the order with temporal properties
CREATE (o:Order {
id: apoc.create.uuid(),
total: apoc.convert.parseFloat(order.total),
orderedAt: apoc.date.parse(order.date, "s", "yyyy-MM-dd'T'HH:mm:ss'X'"),
status: "pending"
})
CREATE (c)-[:PLACED]->(o)-[:INCLUDES]->(p)
// Step 4: Trigger auto-assigns updatedAt
// Step 5: Full-text index enables search immediately
Without APOC, this pipeline requires: a JSON HTTP client, a UUID generator, a date parser, and type coercion logic — all of which APOC provides as Cypher procedures running inside the database.
Conclusion
APOC transforms Neo4j from a graph query engine into a general-purpose data platform. The procedures covered here — data conversion, graph refactoring, triggers, full-text search, temporal operations, and ETL — account for roughly 80% of what production systems reach for APOC to do.
The key insight is that APOC lets you keep logic inside the database where the data lives, rather than shuttling data through application code for simple transformations. This reduces latency, simplifies deployment, and keeps your stack lean.
Start with apoc.convert and apoc.trigger — they pay for themselves immediately. Add apoc.refactor when your data model evolves, and apoc.load when you need to bring external data into the graph. For a deeper treatment of graph data modeling that prepares your schema for APOC-friendly operations, see Graph Data Modeling Patterns for Neo4j. If you are evaluating which Neo4j driver to use with APOC in your application layer, the Neo4j Database Adapters Compared guide covers the driver landscape.