User-defined procedures and functions
Defining procedures and functions enables you to extend Neo4j by writing customized code, which can be invoked directly from Cypher. Procedures and functions can take arguments, perform operations on the database, and return results.
-
User-defined procedures are the most powerful form of customization, allowing you to perform complex operations and return multiple results.
-
User-defined functions are simpler forms of procedures that return a single value and are read-only. Although they are less powerful in capability, they are often easier to use and more efficient than procedures for many common tasks.
-
User-defined aggregation functions are functions that aggregate data and return a single result.
For a comparison between user-defined procedures, functions, and aggregation functions, see Comparison of procedures and functions.
|
User-defined procedures requiring execution on the system database need to include the annotation |
Create a procedure or function
Make sure you have read and followed the preparatory setup instructions in Setting up a plugin project.
|
The example discussed below is available as a repository on GitHub. To get started quickly you can fork the repository and work with the code as you follow along in the guide below. |
First, decide what the procedure, function, or aggregation function should do, then write a test that proves that it does it right. Finally, write a procedure, function, or aggregation function that passes the test.
Create integration tests
You can use the test dependencies Neo4j Harness and JUnit to write integration tests for your procedure, function, or aggregation function. The tests should start a Neo4j server, load the procedure, and execute queries against it.
The following is an example using Neo4j Harness and JUnit 5 for testing a procedure that returns relationship types found in the graph:
package example;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.neo4j.driver.Value;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import static org.assertj.core.api.Assertions.assertThat;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class GetRelationshipTypesTests {
private Driver driver;
private Neo4j embeddedDatabaseServer;
@BeforeAll
void initializeNeo4j() {
this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withProcedure(GetRelationshipTypes.class)
.build();
this.driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI());
}
@AfterAll
void closeDriver(){
this.driver.close();
this.embeddedDatabaseServer.close();
}
@AfterEach
void cleanDb(){
try(Session session = driver.session()) {
session.run("MATCH (n) DETACH DELETE n");
}
}
/**
* We should be getting the correct values when there is only one type in each direction
*/
@Test
public void shouldReturnTheTypesWhenThereIsOneEachWay() {
final String expectedIncoming = "INCOMING";
final String expectedOutgoing = "OUTGOING";
// In a try-block, to make sure we close the session after the test
try(Session session = driver.session()) {
//Create our data in the database.
session.run(String.format("CREATE (:Person)-[:%s]->(:Movie {id:1})-[:%s]->(:Person)", expectedIncoming, expectedOutgoing));
//Execute our procedure against it.
Record record = session.run("MATCH (u:Movie {id:1}) CALL example.getRelationshipTypes(u) YIELD outgoing, incoming RETURN outgoing, incoming").single();
//Get the incoming / outgoing relationships from the result
assertThat(record.get("incoming").asList(Value::asString)).containsOnly(expectedIncoming);
assertThat(record.get("outgoing").asList(Value::asString)).containsOnly(expectedOutgoing);
}
}
}
The following is an example that uses Neo4j Harness for testing a user-defined function that joins a list of strings:
package example;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Session;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import static org.assertj.core.api.Assertions.assertThat;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JoinTest {
private Neo4j embeddedDatabaseServer;
@BeforeAll
void initializeNeo4j() {
this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withFunction(Join.class)
.build();
}
@AfterAll
void closeNeo4j() {
this.embeddedDatabaseServer.close();
}
@Test
void joinsStrings() {
// This is in a try-block, to make sure we close the driver after the test
try(Driver driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI());
Session session = driver.session()) {
// When
String result = session.run( "RETURN example.join(['Hello', 'World']) AS result").single().get("result").asString();
// Then
assertThat( result).isEqualTo(( "Hello,World" ));
}
}
}
The following is an example that uses Neo4j Harness and JUnit 5 for testing a user-defined aggregation function that finds the longest string:
package example;
import org.junit.Rule;
import org.junit.Test;
import org.neo4j.driver.v1.*;
import org.neo4j.harness.junit.Neo4jRule;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
public class LongestStringTest
{
// This rule starts a Neo4j instance
@Rule
public Neo4jRule neo4j = new Neo4jRule()
// This is the function to test
.withAggregationFunction( LongestString.class );
@Test
public void shouldAllowIndexingAndFindingANode() throws Throwable
{
// This is in a try-block, to make sure you close the driver after the test
try( Driver driver = GraphDatabase.driver( neo4j.boltURI() , Config.build().withEncryptionLevel( Config.EncryptionLevel.NONE ).toConfig() ) )
{
// Given
Session session = driver.session();
// When
String result = session.run( "UNWIND ["abc", "abcd", "ab"] AS string RETURN example.longestString(string) AS result").single().get("result").asString();
// Then
assertThat( result, equalTo( "abcd" ) );
}
}
}
Define your procedure, function, or aggregation function
With the test in place, write a procedure, function, or aggregation function that fulfills the expectations of the test. The full example is available in the Neo4j Procedure Template repository.
See Values and types for details on values and types.
Particular things to note:
-
All procedures are annotated
@Procedure. -
The procedure annotation can take three optional arguments:
name,mode, andeager.-
nameis used to specify a different name for the procedure than the default generated, which isclass.path.nameOfMethod. Ifmodeis specified,namemust be specified as well. -
nameis not allowed in a reserved namespace, and having anamewithout a namespace is deprecated behavior. -
If a procedure is registered with the same name as a built-in procedure in a deprecated namespace, the built-in procedure is shadowed.
-
modeis used to declare the types of interactions that the procedure performs. A procedure fails if it attempts to execute database operations that violate its mode. The defaultmodeisREAD. The following modes are available:-
READ— This procedure only performs read operations against the graph. -
WRITE— This procedure performs read and write operations against the graph. -
SCHEMA— This procedure performs operations against the schema, i.e. create and drop indexes and constraints. A procedure with this mode can read graph data, but not write. -
DBMS— This procedure performs system operations such as user management and query management. A procedure with this mode is not able to read or write graph data.
-
-
eageris a boolean setting defaulting tofalse. If it is set totrue, the Cypher planner plans an extraeageroperation before and after calling the procedure. This is useful in cases where the procedure makes changes to the database in a way that could interact with the operations preceding or following the procedure. For example:MATCH (n) WHERE n.key = 'value' WITH n CALL example.deleteNeighbours(n, 'FOLLOWS')This query can delete some of the nodes that are matched by the Cypher query, and the
n.keylookup will fail. Marking this procedure aseagerprevents this from causing an error in Cypher code. However, it is still possible for the procedure to interfere with itself by trying to read entities it has previously deleted. It is the responsibility of the procedure author to handle that case.
-
-
The context of the procedure, which is the same as each resource that the procedure wants to use, is annotated
@Context.
-
All functions are annotated with
@UserFunction. -
The function name must be namespaced and is not allowed in reserved namespaces.
-
If a function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed.
For more details, see the Neo4j Javadocs for org.neo4j.procedure.UserFunction.
-
All functions are annotated with
@UserAggregationFunction. -
The annotated function must return an instance of an aggregator class.
-
An aggregator class contains one method annotated with
@UserAggregationUpdateand one method annotated with@UserAggregationResult. The method annotated with@UserAggregationUpdateis called multiple times and enables the class to aggregate data. When the aggregation is done, the method annotated with@UserAggregationResultis called once and the result of the aggregation is returned. -
The aggregation function name must be namespaced and is not allowed in reserved namespaces.
-
If a user-defined aggregation function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed.
For more details, see the Neo4j Javadocs for org.neo4j.procedure.UserAggregationFunction.
|
The correct way to signal an error from within an aggregation function is to throw |
Write your user-defined procedure, function, and aggregation function
The following examples show how to write a user-defined procedure, function, and aggregation function.
The following is an example of a user-defined procedure that returns the relationship types going in and out of a given node:
package example;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.logging.Log;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
public class GetRelationshipTypes {
// This gives us a log instance that outputs messages to the
// standard log, normally found under `data/log/console.log`
@Context
public Log log;
/**
* This procedure takes a Node and gets the relationships going in and out of it
*
* @param node The node to get the relationships for
* @return A RelationshipTypes instance with the relations (incoming and outgoing) for a given node.
*/
@Procedure(name = "example.getRelationshipTypes")
@Description("Get the different relationships going in and out of a node.")
public Stream<RelationshipTypes> getRelationshipTypes(@Name("node") Node node) {
List<String> outgoing = new ArrayList<>();
node.getRelationships(Direction.OUTGOING).iterator()
.forEachRemaining(rel -> AddDistinct(outgoing, rel));
List<String> incoming = new ArrayList<>();
node.getRelationships(Direction.INCOMING).iterator()
.forEachRemaining(rel -> AddDistinct(incoming, rel));
return Stream.of(new RelationshipTypes(incoming, outgoing));
}
The following is an example of a user-defined function that joins a list of strings with a given delimiter:
package example;
import java.util.List;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.UserFunction;
/**
* This is an example how you can create a simple user-defined function for Neo4j.
*/
public class Join {
@UserFunction
@Description("example.join(['s1','s2',...], delimiter) - join the given strings with the given delimiter.")
public String join(
@Name("strings") List<String> strings,
@Name(value = "delimiter", defaultValue = ",") String delimiter) {
if (strings == null || delimiter == null) {
return null;
}
return String.join(delimiter, strings);
}
}
The following is an example of a user-defined aggregation function that finds the longest string:
package example;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.UserAggregationFunction;
import org.neo4j.procedure.UserAggregationResult;
import org.neo4j.procedure.UserAggregationUpdate;
public class LongestString
{
@UserAggregationFunction
@Description( "org.neo4j.function.example.longestString(string) - aggregates the longest string found" )
public LongStringAggregator longestString()
{
return new LongStringAggregator();
}
public static class LongStringAggregator
{
private int longest;
private String longestString;
@UserAggregationUpdate
public void findLongest(
@Name( "string" ) String string )
{
if ( string != null && string.length() > longest)
{
longest = string.length();
longestString = string;
}
}
@UserAggregationResult
public String result()
{
return longestString;
}
}
}
Injectable resources
When writing procedures, functions, or aggregation functions, some resources can be injected into the procedure from the database.
To inject these, use the @Context annotation.
The classes that can be injected are:
-
Log -
TerminationGuard -
GraphDatabaseService -
Transaction
All of the above classes are considered safe and future-proof and do not compromise the security of the database.
Several unsupported (restricted) classes can also be injected and can be changed with little or no notice.
Procedure, functions, and aggregation functions written to use these restricted APIs are not loaded by default, and you need to use the dbms.security.procedures.unrestricted to load them.
Read more about this config setting in Operations Manual → Securing extensions.
Call user-defined procedures or functions
You can call user-defined procedures and functions from Cypher queries in the same way as built-in procedures and functions.
To call a user-defined procedure, use a Cypher CALL clause.
The procedure name must be fully qualified, so a procedure named findDenseNodes defined in the package org.neo4j.examples could be called using:
CALL org.neo4j.examples.findDenseNodes(1000)
CALL may be the only clause within a Cypher statement or may be combined with other clauses.
Arguments can be supplied directly within the query or taken from the associated parameter set.
For full details, see the documentation in Cypher Manual → CALL procedure.
User-defined functions are called in the same way as any other Cypher function.
The function name must be fully qualified, so a function named join defined in the package org.neo4j.examples could be called using:
MATCH (p: Person) WHERE p.age = 36
RETURN org.neo4j.examples.join(collect(p.names))
User-defined aggregation functions are called in the same way as any other Cypher aggregation function.
The function name must be fully qualified, so a function named longestString defined in the package org.neo4j.examples could be called using:
MATCH (p: Person) WHERE p.age = 36
RETURN org.neo4j.examples.longestString(p.name)
Reload procedures and functionsAdmin OnlyCypher 25Enterprise OnlyIntroduced in 2026.09
You can use the built-in procedure dbms.reloadProcedures() to reload the user supplied procedure or function JAR files from the plugins directory into a running Neo4j, without restarting it.
This procedure is not supported on Windows.
For more information about plugins, see Operations Manual → Configure plugins.
Syntax
The syntax for the dbms.reloadProcedures() procedure is as follows:
Syntax |
|
||
Description |
Reload procedures from disk. |
||
Input arguments |
Name |
Type |
Description |
namespaces |
STRING |
Optional. A glob pattern selecting which namespaces to reload. Defaults to '*', meaning everything. Example: 'com.example.*'. |
|
Returned arguments |
Name |
Type |
Description |
name |
STRING |
The fully qualified name of each entry point that was reloaded. |
|
type |
STRING |
The type of each entry point that was reloaded. Possible values are: PROCEDURE, FUNCTION or AGGREGATION FUNCTION. |
|
Mode |
DBMS |
||
A successful call returns one row for every entry point it brought back into service. An empty result means nothing matched the pattern.
|
The namespace argument genuinely limits the operation.
It is not advisory.
A plugin whose namespace does not match the pattern is left untouched, even if its JAR file changed on disk.
Three namespaces |
Example
|
Keep in mind that in a cluster deployment, you need to deploy the JAR files and run the The reload process is isolated so that the operation only affects new transactions. Already running transactions continue to execute with a snapshot of the available procedures or functions at their respective initialization. |
You can use the following example to test the dbms.reloadProcedures() procedure to reload a user-defined procedure or function without restarting the server, as well as withdraw a procedure or function.
The example assumes you have a Neo4j server running with the example-v1.jar plugin in the plugins directory, which contains a procedure com.example.someProcedure().
-
Confirm that reload procedure is available by running the following query:
SHOW PROCEDURES YIELD name, description, admin WHERE name = 'dbms.reloadProcedures' RETURN name, description, admin;+------------------------------------------------------------------+ | name | description | admin | +------------------------------------------------------------------+ | "dbms.reloadProcedures" | "Reload procedures from disk." | TRUE | +------------------------------------------------------------------+ 1 row ready to start consuming query after 111 ms, results consumed after another 8 ms
-
Replace the
example-v1.jarwithexample-v2.jarwhile the server keeps running. The new JAR file contains thecom.example.someProcedureas well as some new procedures and functions:rm plugins/example-v1.jar cp example-v2.jar plugins/ -
Reload that namespace:
CALL dbms.reloadProcedures('com.example.*');name type "com.example.someProcedure" "PROCEDURE" "com.example.newProcedure" "PROCEDURE" "com.example.newFunction" "FUNCTION" "com.example.newAggregationFunction" "AGGREGATION FUNCTION"
The procedure returns all the entry points in that namespace, including the new ones.
Similarly, you can withdraw a procedure or function by removing its JAR file and reloading the namespace.
Confirm that no restart happened
You can check that the server did not restart by checking the server start time and uptime. The server start time can be read from Cypher and should be unchanged across a reload.
CALL dbms.queryJmx('java.lang:type=Runtime') YIELD attributes
RETURN datetime({epochMillis: attributes.StartTime.value}) AS serverStarted,
duration({milliseconds: attributes.Uptime.value}) AS uptime;
Troubleshooting
A reload builds an intermediate registry and only swaps it into service if the whole load succeeds. If any JAR in the directory is invalid, the reload fails and the previously loaded plugins keep serving, unchanged. When a corrupt file is in the plugins directory, the reload will return 52N24 and the plugin that was already in service will continue to answer correctly. Removing the invalid file and reloading again restores normal operation with no restart.
Logging
Every reload writes a matched pair of entries to debug.log, giving operators an audit trail of what was reloaded and when. For example:
INFO c.n.d.p.ReloadProcedure "Reloading procedure namespace `com.example.*`."
INFO c.n.d.p.ReloadProcedure "Reloaded 2 procedures, 1 functions, and 1 aggregation functions."
Query cache invalidation
Cached Cypher plans that reference a changed signature are discarded automatically, so a client does not continue to run a stale plan against the old plugin. For example, if a procedure signature changes, the following log entry is written to debug.log:
INFO o.n.c.i.c.CypherQueryCaches "Discarded stale query from the query cache after 5 seconds.
Reason: Procedure or function signature have been modified. Query id: 79."
Reserved and deprecated procedure namespaces
|
Note that deprecated procedure and function namespaces will be moved to reserved in the next major Cypher version. For more information about Neo4j and Cypher versioning, see Operations manual → Introduction. |
The following table shows the reserved and deprecated procedure namespaces:
| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The following table shows the reserved and deprecated function namespaces:
| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|