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.

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 Release

apoc.trigger.add

Procedure

APOC Core

apoc.trigger.remove

Procedure

APOC Core

apoc.trigger.removeAll

Procedure

APOC Core

apoc.trigger.list

Procedure

APOC Core

apoc.trigger.pause

Procedure

APOC Core

apoc.trigger.resume

Procedure

APOC Core

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 2. 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}]

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

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, updates 2 properties 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) AND 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 add the condition AND NOT 'lasts' IN propList to prevent an infinite loop as this SET will trigger this query again.

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()}});
Handle deleted entities

If we to create a 'before' or 'after' trigger query, with $deletedRelationships or $deletedNodes, and then we want to retrieve entities information like labels and/or properties, we cannot use the 'classic' cypher functions labels() and properties(), but we can leverage on virtual nodes and relationships, via the functions apoc.trigger.toNode(node, $removedLabels, $removedNodeProperties) and apoc.trigger.toRelationship(rel, $removedRelationshipProperties).

So that, we can retrieve information about nodes and relations, using the apoc.any.properties, and the apoc.node.labels functions.

For example, if we want to create a new node with the same properties (plus the id) and with an additional label retrieved for each deleted node, we can execute:

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'})

Or also, if we want to create a node Report with the same properties (plus the id and rel-type as additional properties) for each deleted relationship, we can execute:

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', we don’t need to execute apoc.trigger.toNode and apoc.trigger.toRelationship, because using this one, the rebuild of entities is executed automatically under the hood.

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 3. 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.