Triggers

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

Enable apoc.trigger.enabled=true in $NEO4J_HOME/conf/apoc.conf first.

CALL apoc.trigger.add(name, statement, selector) yield name, statement, installed

add a trigger statement under a name, in the statement you can use $createdNodes, $deletedNodes etc., the selector is {phase:'before/after/rollback/afterAsync'} returns previous and new trigger information, please check Trigger Phase Table for more details

CALL apoc.trigger.remove(name) yield name, statement, installed

remove previously added trigger, returns trigger information

CALL apoc.trigger.removeAll() yield name, statement, installed

removes all previously added triggers , returns trigger information

CALL apoc.trigger.list() yield name, statement, installed

update and 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 your statement, i.e. $createdNodes.

The parameters available are:

Statement Description

transactionId

returns the id of the transaction

commitTime

return 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--

You can use these helper functions to extract nodes or relationships by label/relationship-type or updated property key.

Table 1. Helper Functions

apoc.trigger.nodesByLabel($assignedLabels/$assignedNodeProperties,'Label')

function to filter entries by label, to be used within a trigger statement with $assignedLabels and $removedLabels

apoc.trigger.propertiesByKey($assignedNodeProperties,'key')

function to filter propertyEntries by property-key, to be used within a trigger statement with $assignedNode/RelationshipProperties and $removedNode/RelationshipProperties. Returns [{old,[new],key,node,relationship}]

Triggers Examples

Set properties connected to a node

We could add a trigger that when is added a specific property on a node, that property is added to all the 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 we add a trigger and we execute for example, MATCH (n:Person) WHERE n.name IN ['Daniel', 'Mary'] SET n.age=55, n.surname='Quinn', the $assignedNodeProperties which can be 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"
      }]
}

As we can see, the result is a map of list, 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 (of course, in this case new values will be always null).

Same thing regarding assignedRelationshipProperties and removedRelationshipProperties, with the only difference that instead of node: NODE(n) key, we’ll have relationship: RELATIONSHIP(n).

For example, if we want to create a trigger that at every set, update 2 property "time" and "lasts" with the current date and the property updated, we can do:

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)
where not "lasts" in propList               // to prevent loops
SET n.time = date(),  n.lasts = propList', {phase:'afterAsync'});

In the example above, we put match (n) where id(n) = id(node) to demonstrate that the we pull the node by id into parameters. Anyway, we can get rid off this one and change last row with SET node.time = date(), node.lasts = propList. Note that we have to put the clause where not "lasts" in propList to prevent infinite cascade SET.

Then, we can execute:

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

Executing

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

we can see the property time with the today’s date, and lasts=['country','age'].

So when we add the surname property on a node, it’s added to all the nodes connected (in this case one level deep)

MATCH (d:Person {name:'Daniel'})
SET d.surname = 'William'

Now we add the trigger using apoc.trigger.propertiesByKey on the surname property

CALL apoc.trigger.add('setAllConnectedNodes','UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties,"surname") as prop
WITH prop.node as n
MATCH(n)-[]-(a)
SET a.surname = n.surname', {phase:'after'});

So when we add the surname property on a node, it’s added to all the nodes connected (in this case one level deep)

MATCH (d:Person {name:'Daniel'})
SET d.surname = 'William'
apoc.trigger.add.setAllConnectedNodes

The surname property is add/change on all related nodes

Update labels

Dataset

CREATE (k:Actor {name:'Keanu Reeves'})
CREATE (l:Actor {name:'Laurence Fishburne'})
CREATE (c:Actor {name:'Carrie-Anne Moss'})
CREATE (m:Movie {title:'Matrix'})
CREATE (k)-[:ACT_IN]->(m)
CREATE (l)-[:ACT_IN]->(m)
CREATE (c)-[:ACT_IN]->(m)
apoc.trigger.add.setLabels

We add a trigger using apoc.trigger.nodesByLabel that when the label Actor of a node is removed, update all labels Actor with Person

CALL apoc.trigger.add('updateLabels',"UNWIND apoc.trigger.nodesByLabel($removedLabels,'Actor') AS node
MATCH (n:Actor)
REMOVE n:Actor SET n:Person SET node:Person", {phase:'before'})
MATCH(k:Actor {name:'Keanu Reeves'})
REMOVE k:Actor
apoc.trigger.add.setLabelsResult
Create relationship on a new node

We can add a trigger that connect every new node with label Actor and as name property a specific value

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

Generally, is recommended to use afterAsync phase, to prevent some annoying transaction locks. For example, given this trigger:

CALL apoc.trigger.add('lockTriggerTest1','UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties,"name") as prop
WITH prop.node as n
CREATE (z:AnotherNode {myId: id(n)})
CREATE (n)-[:GENERATED]->(z)',
{phase:'after'});

if we execute:

MATCH (n:Person {name: 'John'}) set n.name = 'Jack'

the query will remain pending indefinitely. To solve this, we can use {phase:'afterAsync'}

Pause trigger

We have the possibility to pause a trigger without remove it, if we will need it in the future

apoc.trigger.pause
Resume paused trigger

When you need again of a trigger paused

apoc.trigger.resume
Enforcing property type

For this example, we would like that all the reference node properties are of type STRING

CALL apoc.trigger.add("forceStringType",
"UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties, 'reference') AS prop
CALL apoc.util.validate(apoc.meta.type(prop) <> 'STRING', 'expected string property type, got %s', [apoc.meta.type(prop)]) RETURN null", {phase:'before'})
CREATE (a:Node) SET a.reference = 1

Neo.ClientError.Transaction.TransactionHookFailed
Optional params

We can pass as a 4th parameter, a {params: {parameterMaps}} to insert additional parameters.

CALL apoc.trigger.add('timeParams','UNWIND $createdNodes AS n SET n.time = $time', {}, {params: {time: timestamp()}});
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"])', {})
CALL apoc.trigger.add('lowercase-by-label','UNWIND apoc.trigger.nodesByLabel($assignedLabels,"Person") AS n SET n.id = toLower(n.name)', {})
Table 2. Helper Functions

Phase

Description

before

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

rollback

The trigger will be activate right after the rollback

after

The trigger will be activate right after the commit

afterAsync

The trigger will be activate 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, afterAsync phase is preferred

Export metadata

To import triggers in another database (for example after a ./neo4j-admin backup and /neo4j-admin restore), please see the apoc.systemdb.export.metadata procedure.