APOC Procedures: Production Patterns for Neo4j's Swiss Army Knife | Graphs | graphwiz.ai
APOC Procedures: Production Patterns for Neo4j's Swiss Army Knife
neo4japoccyphergraph-databaseperformanceetl
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:
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:
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
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.