Skip to content



This hook is called after a db write on some core objects.


pre and post hooks are useful for developers building more complex applications and need to perform operations before CiviCRM takes action. This is very applicable when you need to maintain foreign key constraints etc (when deleting an object, the child objects have to be deleted first).


These hooks use database transactions. Don't execute code that updates the same data in the database in this hook. Eg. if triggering on a Membership entity, don't try and update that membership entity within the hook. Use hook_civicrm_postCommit instead.


Some of the more esoteric entities may not fire this hook when they're saved. If you happen to find such an entity, please make a PR to core to add this hook by refactoring to use writeRecord() and deleteRecord(), which include the hook.


hook_civicrm_post(string $op, string $objectName, int $objectId, &$objectRef)


  • $op - operation being performed with CiviCRM object. Can have the following values:

    • 'view' : The CiviCRM object is going to be displayed
    • 'create' : The CiviCRM object is created (or contacts are being added to a group)
    • 'edit' : The CiviCRM object is edited
    • 'delete' : The CiviCRM object is being deleted (or contacts are being removed from a group)
    • 'update': The contact is being moved to trash or restored (Contact objects only)
    • 'merge': The contact has been merged into another contact
  • $objectName - can have the following values:

    • 'Activity'
    • 'ActionLog'
    • 'Address'
    • 'Batch'
    • 'Campaign' (from 4.6)
    • 'Case'
    • 'CaseContact' (from 5.14.0)
    • 'CaseType'
    • 'Contribution'
    • 'ContributionRecur'
    • 'ContributionSoft' (from 5.23.0)
    • 'CustomField'
    • 'CustomGroup'
    • 'CRM_Mailing_DAO_Spool'
    • 'Domain' (from 5.18.0)
    • 'Email'
    • 'Event'
    • 'EntityTag'
    • 'EntityBatch'
    • 'Grant'
    • 'Group'
    • 'GroupNesting'
    • 'GroupContact'
    • 'Household'
    • 'Individual'
    • 'IM'
    • 'LineItem'
    • 'Mailing'
    • 'MailingAB'
    • 'Membership'
    • 'MembershipBlock'
    • 'MembershipPayment'
    • 'OpenID'
    • 'Participant'
    • 'ParticipantPayment'
    • 'Phone'
    • 'Pledge'
    • 'PledgeBlock'
    • 'PledgePayment'
    • 'Profile' (while this is not really an object, people have expressed an interest to perform an action when a profile is created/edited)
    • 'RecurringEntity'
    • 'Relationship'
    • 'SmsProvider'
    • 'StatusPreference'
    • 'Survey' (from 5.1.x)
    • 'Tag'
    • 'UFGroup' (from 5.34)
    • 'UFMatch' (when an object is linked to a CMS user record, at the request of GordonH. A UFMatch object is passed for both the pre and post hooks)
    • 'PriceField'
    • 'PriceFieldValue'
    • 'PriceSet'
  • $objectId - the unique identifier for the object. tagID in case of EntityTag

  • $objectRef - the reference to the object if available. Note that this might be a CRM_Core_DAO type object, but in other situations it could be an Array. For case of EntityTag it is an array of (entityTable, entityIDs)


  • None


Here is a simple example, assuming your extension is called myextension, that will send you an email whenever an individual contact is either added, updated or deleted (note that this is probably a terrible idea, it’s just for demonstration):


   * This hook is called after a db write on entities.
   * @param string $op
   *   The type of operation being performed.
   * @param string $objectName
   *   The name of the object.
   * @param int $objectId
   *   The unique identifier for the object.
   * @param object $objectRef
   *   The reference to the object.
function myextension_civicrm_post(string $op, string $objectName, int $objectId, &$objectRef) {

  if ($objectName === 'Individual') {
    if ($op === 'create') {
      $subject = "myextension_civicrm_post - ADDED NEW contact";
    elseif ($op === 'edit') {
      $subject = "myextension_civicrm_post - EDITED contact";
    elseif ($op == 'delete') {
      $subject = "myextension_civicrm_post - DELETED contact";

  if (isset($subject)) {
    $to   = '';
    $from = '';
    $msg  = "CiviCRM exampleSendEmailOnIndividual called with:\n"
          . "$op $objectName $objectId on $objectRef->display_name";

    mail($to, $subject, $msg, "From: ".$from);


Discussion of the post and postCommit hooks

It may be worth considering the order in which hooks will get called in your use case. Take the example of a humble api3 Contact.create call that creates a contact with an email address, then immediately deletes it.

If that api call is run outside of a transaction, then the hooks are called:

  • post create email
  • post create individual
  • postCommit create email
  • postCommit create individual
  • post delete individual
  • postCommit delete individual

This is presumably because Contact.create uses a transaction internally.

If that api call is run inside an explicit transaction then the following order is observed:

  • post create email
  • post create individual
  • post delete individual
  • postCommit create email
  • postCommit create individual
  • postCommit delete individual

And (perhaps obviously), if the transaction is rolled back, none of the postCommit hooks fire.

In Summary

  • The post hook is often called too early; not all the data has been saved to the database (custom fields are a common cause for concern here), so postCommit is very helpful.

  • But! You don't necessarily know when the transaction will end. Did you know Contact.create uses one? Did you implement one yourself? Is your code being called by something else that started a transaction? So you really need to test that your code works as expected thoroughly. Which is hard given...

  • If your extension’s phpunit tests use the TransactionalInterface (which is nice for efficiency and robustness of tests), then your postCommit hooks will never fire, so you’re probably testing the wrong data.

Deprecated pattern: using CRM_Core_Transaction::addCallback

The postCommit hook was added in CiviCRM 5.25. Code written before that may have used a pattern as follows to achieve the same thing:


function example_civicrm_post(string $op, string $objectName, int $objectId, &$objectRef) {
  if ($objectName === 'Membership' && $op === 'create') {
    if (CRM_Core_Transaction::isActive()) {
      CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, 'updateMembershipCustomField', [$objectRef->id]);
    else {

This pattern is not necessary now: the same result can be achieved simply with the following using hook_civicrm_postCommit instead:


function example_civicrm_postCommit(string $op, string $objectName, int $objectId, &$objectRef) {
  if ($objectName === 'Membership' && $op === 'create') {