In a trigger you register Cypher statements that are called when data in Neo4j is changed (created, updated, deleted). Triggers can be run before or after a commit.

By default triggers are disabled. We can enable them by setting the following property in apoc.conf:

Table 1. Description
Option Key Value Description


true/false, default false

Enable/Disable the feature


number, default 60000

Interval in ms after which a replication check is triggered across all cluster nodes

Qualified Name Type


add a trigger kernelTransaction under a name, in the kernelTransaction you can use {createdNodes}, {deletedNodes} etc., the selector is {phase:'before/after/rollback'} returns previous and new trigger information. Takes in an optional configuration.



remove previously added trigger, returns trigger information



removes all previously added trigger, returns trigger information



list all installed triggers



CALL apoc.trigger.pause(name) | it pauses the trigger



CALL apoc.trigger.resume(name) | it resumes the paused trigger


The transaction data from Neo4j is turned into appropriate data structures to be consumed as parameters to a statement, i.e. $createdNodes.

The parameters available are:

Statement Description


returns the id of the transaction


returns the date of the transaction in milliseconds


when a node is created our trigger fires (list of nodes)


when a relationship is created our trigger fires (list of relationships)


when a node is deleted our trigger fires (list of nodes)


when a relationship is deleted our trigger fires (list of relationships)


when a label is removed our trigger fires (map of label to list of nodes)


when a properties of node is removed our trigger fires (map of key to list of map of key,old,node)


when a properties of relationship is removed our trigger fires (map of key to list of map of key,old,relationship)


when a labes is assigned our trigger fires (map of label to list of nodes)


when node property is assigned our trigger fires (map of key to list of map of key,old,new,node)


when relationship property is assigned our trigger fires (map of key to list of map of key,old,new,relationship)


a map containing the metadata of that transaction. Transaction meta data can be set on client side e.g. via

The helper functions can be used to extract nodes or relationships by label/relationship-type or updated property key.

Table 2. Helper Functions

apoc.trigger.toNode(node, $removedLabels, $removedNodeProperties)

function to rebuild a node as a virtual, to be used in triggers with a not 'afterAsync' phase

apoc.trigger.toRelationship(rel, $removedRelationshipProperties)

function to rebuild a relationship as a virtual, to be used in triggers with a not 'afterAsync' phase

Unlike previous versions of Neo4j, it will not be possible using Neo4j 5 to modify an entity created in phase: “after”. For example, the following query will return an Exception with message can’t acquire ExclusiveLock…​:

CALL apoc.trigger.add('name','UNWIND $createdNodes AS n SET n.txId = $transactionId',{phase:'after'});
CREATE (f:Baz);

It is instead necessary to use another phase or perform only reading operations.

Triggers Examples

Set properties connected to a node

It is possible to add a trigger which, when added to a specific property on a node, adds the same property to all nodes connected to this node.


CREATE (d:Person {name:'Daniel', surname: 'Craig'})
CREATE (l:Person {name:'Mary', age: 47})
CREATE (t:Person {name:'Tom'})
CREATE (j:Person {name:'John'})
CREATE (m:Person {name:'Michael'})
CREATE (a:Person {name:'Anne'})
CREATE (t)-[:SON_OF]->(d)
CREATE (t)-[:BROTHER]->(j)
CREATE (a)-[:WIFE_OF]->(d)
CREATE (d)-[:SON_OF]->(m)
CREATE (j)-[:SON_OF]->(d)

With the above dataset, if a trigger is added and the following query is executed: MATCH (n:Person) WHERE IN ['Daniel', 'Mary'] SET n.age=55, n.surname='Quinn', the $assignedNodeProperties used in the trigger statement will be as follows (where NODE(1) is (:Person {name: 'Daniel'}), and NODE(2) is (:Person {name: 'Mary'})):

   age: [{
         node : NODE(1),
         new: 55,
         old: null,
         key: "age"
         node: NODE(2),
         new: 55,
         old: 47,
         key: "age"

   surname: [{
         node: NODE(1),
         new: "Quinn",
         old: "Craig",
         key: "surname"
         node: NODE(2),
         new: "Quinn",
         old: null,
         key: "surname"

The result is a map where the keys are the assigned properties, and the values are a list of entities involved. Every element of a list have the node itself, the new value of the changed properties, the old value (or null if the property didn’t exist) and the key with the property name.

The $removedNodeProperties parameter has the same structure and logic (in this case, new values will be always null).

The same is true for assignedRelationshipProperties and removedRelationshipProperties, with the only difference being that the node: NODE(n) key is replaced with the relationship: RELATIONSHIP(n) key.

As an example, the following statement creates a trigger which for every SET, updates the two properties time and lasts with the current date:

CALL apoc.trigger.add('setLastUpdate',
  "UNWIND keys($assignedNodeProperties) AS k
  UNWIND $assignedNodeProperties[k] AS map
  WITH map.node AS node, collect(map.key) AS propList
  MATCH (n)
  WHERE id(n) = id(node) AND NOT 'lasts' in propList // to prevent loops
  SET n.time = date(),  n.lasts = propList",
  {phase: 'afterAsync'});

In the example above, MATCH (n) WHERE id(n) = id(node) is used to demonstrate that the node is found by id first, before setting its parameters. However, it is more efficient to remove this command and instead change the penultimate row to: SET node.time = date(), node.lasts = propList. Note that the condition AND NOT 'lasts' IN propList must be added to prevent an infinite loop as the SET command will trigger this query again.

It is then possible to execute the following query:

MATCH (n:Person {name: 'Daniel'}) set n.age = 123, = 'Italy'


MATCH (n:Person {name: 'Daniel'}) return n

It is possible to set the property time with today’s date, and lasts=['country','age'].

In cases where the surname property is added to a node, it’s added to all the nodes connected to it as well (in this case one level deep).

MATCH (d:Person {name:'Daniel'})
SET d.surname = 'William'
Create relationship on a new node

To add a trigger that connects every new node with the label Actor and assign a specific value to the name property, run the following query:

CALL apoc.trigger.add('create-rel-new-node',"UNWIND $createdNodes AS n
MATCH (m:Movie {title:'Matrix'})
WHERE n:Actor AND IN ['Keanu Reeves','Laurence Fishburne','Carrie-Anne Moss']
CREATE (n)-[:ACT_IN]->(m)", {phase:'before'})
CREATE (k:Actor {name:'Keanu Reeves'})
CREATE (l:Actor {name:'Laurence Fishburne'})
CREATE (c:Actor {name:'Carrie-Anne Moss'})
CREATE (a:Actor {name:'Tom Hanks'})
CREATE (m:Movie {title:'Matrix'})
apoc.trigger.add.create rel new node
Prevent transaction blocking

To prevent certain transaction locks, it is generally recommended to use the afterAsync phase. This will stop the query from pending indefinitely.

Pause trigger

To pause a trigger without removing it for future purposes, use the following procedure:

Resume paused trigger

To resume a paused trigger, use the following procedure:

Optional parameters

Add \{params: {parameterMaps}} to insert additional parameters.

CALL apoc.trigger.add('timeParams','UNWIND $createdNodes AS n SET n.time = $time', {}, {params: {time: timestamp()}});
Handle deleted entities

If a ‘before’ or ‘after’ trigger query has been created, with $deletedRelationships or $deletedNodes, and entities information such as labels and/or properties need to be retrieved, it is not possible to use the cypher functions labels() and properties(). However, it is possible to leverage virtual nodes and relationships via the functions apoc.trigger.toNode(node, $removedLabels, $removedNodeProperties) and apoc.trigger.toRelationship(rel, $removedRelationshipProperties). If so, it is possible to retrieve information about nodes and relationships using the and the apoc.node.labels functions.

For example, to create a new node with the same properties (plus the id) and with an additional label retrieved for each node, the following query can be executed:

CALL apoc.trigger.add('myTrigger',
"UNWIND $deletedNodes as deletedNode
WITH apoc.trigger.toNode(deletedNode, $removedLabels, $removedNodeProperties) AS deletedNode
CREATE (r:Report {id: id(deletedNode)}) WITH r, deletedNode
CALL apoc.create.addLabels(r, apoc.node.labels(deletedNode)) yield node with node, deletedNode
set" ,

To create a node called Report with the same properties (plus the id and rel-type as additional properties) for each deleted relationship, the following query can be executed:

CALL apoc.trigger.add('myTrigger',
"UNWIND $deletedRelationships as deletedRel
WITH apoc.trigger.toRelationship(deletedRel, $removedRelationshipProperties) AS deletedRel
CREATE (r:Report {id: id(deletedRel), type: apoc.rel.type(deletedRel)})
WITH r, deletedRelset" ,

By using phase 'afterAsync', there is no need to execute the functions apoc.trigger.toNode and apoc.trigger.toRelationship. This is because the rebuild of entities is executed automatically under the hood in this phase.

Other examples
CALL apoc.trigger.add('timestamp','UNWIND $createdNodes AS n SET n.ts = timestamp()', {});
CALL apoc.trigger.add('lowercase','UNWIND $createdNodes AS n SET = toLower(', {});
CALL apoc.trigger.add('txInfo','UNWIND $createdNodes AS n SET n.txId = $transactionId, n.txTime = $commitTime', {phase:'after'});
CALL apoc.trigger.add('count-removed-rels','MATCH (c:Counter) SET c.count = c.count + size([r IN $deletedRelationships WHERE type(r) = "X"])', {})
Table 3. Helper Functions




The trigger will be activated right before the commit. If no phase is specified, it is the default.


The trigger will be activated right after the rollback


The trigger will be activated right after the commit


The trigger will be activated right after the commit and inside a new transaction and thread that will not impact the original one Heavy operations should be processed in this phase without blocking the original transaction Please note that 'after' and 'before' phases can sometimes block transactions, so generally, the afterAsync phase is preferred