Triggers

Triggers allow the registration of Cypher queries that are called when data in Neo4j is changed (created, updated, deleted). Triggers can be run before or after a commit.

The apoc.trigger.* procedures are intended to be executed in the system database, therefore they have to be executed by opening a system database session.

There are several ways of doing this:

  • When using Cypher-shell or Neo4j Browser, prefix Cypher queries with :use system

  • When using Fabric, prefix Cypher queries with USE system

  • When using the drivers, open a session directly against the system database

Moreover, the apoc.trigger procedures accept as first parameter the name of the database in which the triggers should be installed, updated, or removed.

Installing, updating, or removing a trigger is an eventually consistent operation. Therefore, they are not immediately added/updated/removed, but have a refresh rate handled by the APOC configuration apoc.trigger.refresh=<MILLISECONDS>, with default 60000 (milliseconds).

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

apoc.trigger.drop(databaseName STRING, name STRING) - eventually removes the given trigger.

Procedure

apoc.trigger.dropAll

apoc.trigger.dropAll(databaseName STRING) - eventually removes all triggers from the given database.

Procedure

apoc.trigger.install

apoc.trigger.install(databaseName STRING, name STRING, statement STRING, selector MAP<STRING, ANY>, config MAP<STRING, ANY>) - eventually adds a trigger for a given database which is invoked when a successful transaction occurs.

Procedure

apoc.trigger.list

apoc.trigger.list() - lists all currently installed triggers for the session database.

Procedure

apoc.trigger.show

apoc.trigger.show(databaseName STRING) - lists all eventually installed triggers for a database.

Procedure

apoc.trigger.start

apoc.trigger.start(databaseName STRING, name STRING) - eventually restarts the given paused trigger.

Procedure

apoc.trigger.stop

apoc.trigger.stop(databaseName STRING, name STRING) - eventually stops the given 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/org/neo4j/driver/TransactionConfig.html#metadata()

Phase parameter

The third parameter of the apoc.trigger.install() is a map {phase: PHASE}, where PHASE is a string that can have one of the following values:

Table 2. Trigger Phase Table

PHASE

Description

before

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

rollback

The trigger will be activated right after the rollback.

after

The trigger will be activated after the commit.

afterAsync

The trigger will be activated 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 the 'after' and 'before' phases can sometimes block transactions, so generally the afterAsync phase is preferred.

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.install('neo4j', '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 (in default database 'neo4j')

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.install('neo4j', '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'});

Note that the apoc.trigger.install, as well as the apoc.trigger.drop, apoc.trigger.dropAll, apoc.trigger.stop and apoc.trigger.start, have to be executed in the system database.

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, after a time defined by the configuration apoc.trigger.refresh:

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.install('neo4j','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

Note that the apoc.trigger.stop and the apoc.trigger.start procedures are eventually consistent. It is therefore necessary to wait a set amount of time for the changes to propagate. The waiting time is defined by the configuration apoc.trigger.refresh.

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

apoc.trigger.stop
Resume paused trigger

To resume a paused trigger, use the following procedure:

apoc.trigger.start
Optional parameters

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

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

Remove triggers

To remove the trigger with name 'test' in 'neo4j' database, run the following query:

CALL apoc.trigger.drop('neo4j', 'test')

To remove all triggers in 'neo4j' db, run the following query:

CALL apoc.trigger.dropAll('neo4j')

List of triggers

It is possible to return the full list of triggers in a database. For example, if the trigger in the following query is created:

CALL apoc.trigger.install('neo4j', 'count-removals',
    'MATCH (c:Counter) SET c.count = c.count + size([f IN $deletedNodes WHERE id(f) > 0])',
    {})

It is then possible to run (also in this case, after a time defined by the configuration apoc.trigger.refresh):

CALL apoc.trigger.show('neo4j')
Table 3. Results
name query selector params installed paused

"count-removals"

MATCH (c:Counter) SET c.count = c.count + size([f IN $deletedNodes WHERE id(f)  0])

{}

{}

TRUE

FALSE

Please note that, since the trigger operations are eventually consistent (based on the apoc.trigger.refresh configuration), the apoc.trigger.show may return some triggers not yet added/updated/removed. To get the list of all of currently installed triggers, use the apoc.trigger.list against the session database ("neo4j" in the above case).