Triggers

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:

apoc.conf
apoc.trigger.enabled=true
apoc.trigger.refresh=60000
Table 1. Description
Option Key Value Description

apoc.trigger.enabled

true/false, default false

Enable/Disable the feature

apoc.trigger.refresh

number, default 60000

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

Qualified Name Type

apoc.trigger.add

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.

Procedure

apoc.trigger.remove

remove previously added trigger, returns trigger information

Procedure

apoc.trigger.removeAll

removes all previously added trigger, returns trigger information

Procedure

apoc.trigger.list

list all installed triggers

Procedure

apoc.trigger.pause

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

Procedure

apoc.trigger.resume

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

Procedure

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

transactionId

returns the id of the transaction

commitTime

returns the date of the transaction in milliseconds

createdNodes

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

createdRelationships

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

deletedNodes

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

deletedRelationships

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

removedLabels

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

removedNodeProperties

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

removedRelationshipProperties

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

assignedLabels

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

assignedNodeProperties

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

assignedRelationshipProperties

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

metaData

a map containing the metadata of that transaction. Transaction meta data can be set on client side e.g. via https://neo4j.com/docs/api/java-driver/current/org/neo4j/driver/TransactionConfig.html#metadata--

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.

Dataset

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 (l)-[:DAUGHTER_OF]->(d)
CREATE (t)-[:SON_OF]->(d)
CREATE (t)-[:BROTHER]->(j)
CREATE (a)-[:WIFE_OF]->(d)
CREATE (d)-[:SON_OF]->(m)
CREATE (j)-[:SON_OF]->(d)
apoc.trigger.add.setAllConnectedNodes.dataset

With the above dataset, if a trigger is added and the following query is executed: MATCH (n:Person) WHERE n.name 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, n.country = 'Italy'

Executing

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 n.name 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:

apoc.trigger.pause
Resume paused trigger

To resume a paused trigger, use the following procedure:

apoc.trigger.resume
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 apoc.any.properties 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 node+=apoc.any.properties(deletedNode)" ,
{phase:'before'})

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 r+=apoc.any.properties(deletedRel)" ,
{phase:'before'})

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 n.id = toLower(n.name)', {});
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

Phase

Description

before

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

rollback

The trigger will be activated right after the rollback

after

The trigger will be activated right after the commit

afterAsync

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