What you'll learn
  • what are lifecycle events
  • how lifecycle events work
  • how to subscribe to a lifecycle event

Lifecycle events using publish/subscribe pattern replace the hook plugins starting from version 5.18.0.

Overview
anchor

In our Headless CMS we provide lifecycle events available for you to hook into.

With the lifecycle events you can hook into a number of different operations.

For example, when using DynamoDB + Elasticsearch as storage layer, we use the onBeforeSystemInstall to insert the template for Elasticsearch index.

System
anchor

onBeforeSystemInstall
anchor

This event is triggered before the installation of the Headless CMS and insertion of initial “Ungrouped” group.

Event Arguments
anchor

PropertyDescription
tenantID of the current tenant

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeInstall.subscribe(async ({ tenant }) => {
    await notifyAnotherSystemThatHeadlessCmsIsGettingInstalled({ tenant });
  });
});

onAfterSystemInstall
anchor

This event is triggered after the installation of the Headless CMS.

Event Arguments
anchor

PropertyDescription
tenantID of the current tenant

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterInstall.subscribe(async ({ tenant }) => {
    await notifyAnotherSystemThatHeadlessCmsWasInstalled({ tenant });
  });
});

Please note that in between onBeforeInstall and onAfterInstall we create a default CmsGroup named “Ungrouped” and, because of that, there are events onBeforeGroupCreate and onAfterGroupCreate being run.

Groups
anchor

onBeforeGroupCreate
anchor

This event is triggered before new group is stored into the database.

Event Arguments
anchor

PropertyDescription
groupGroup object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeGroupCreate.subscribe(async ({ group }) => {
    /**
     * For example, you do not want the group name to contain the character "a".
     * You can check for that character here and throw an error. Or remove it.
     */
    if (group.name.match(/a/) === null) {
      return;
    }
    group.name = group.name.replace(/a/g, "");
  });
});

onAfterGroupCreate
anchor

This event is triggered after new group is stored into the database.

Event Arguments
anchor

PropertyDescription
groupGroup object which was stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterGroupCreate.subscribe(async ({ group }) => {
    /**
     * For example, notify another system that group was created.
     */
    await notifyAnotherSystemAboutGroupCreate({ group });
  });
});

onBeforeGroupUpdate
anchor

This event is triggered before group is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
originalGroup object which is received from the database
groupGroup object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeGroupUpdate.subscribe(async ({ original, group }) => {
    /**
     * For example, we are not allowing group slug to be changed.
     */
    if (original.slug === group.slug) {
      return;
    }
    throw new Error(`Changing of group slug value is not allowed.`);
  });
});

onAfterGroupUpdate
anchor

This event is triggered before group is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
originalGroup object which is received from the database
groupGroup object which was stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterGroupUpdate.subscribe(async ({ original, group }) => {
    /**
     * For example, notify another system that group was updated.
     */
    await notifyAnotherSystemAboutGroupUpdate({ original, group });
  });
});

onBeforeGroupDelete
anchor

This event is triggered before group is deleted from the database.

Event Arguments
anchor

PropertyDescription
groupGroup object which is going to be deleted

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeGroupDelete.subscribe(async ({ group }) => {
    /**
     * For example, we are not allowing group with slug "ungroupped" to be deleted.
     */
    if (group.slug !== "ungroupped") {
      return;
    }
    throw new Error(`Deleting group with slug "ungroupped" is not allowed.`);
  });
});

onAfterGroupDelete
anchor

This event is triggered after group is deleted from the database.

Event Arguments
anchor

PropertyDescription
groupGroup object which was deleted

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterGroupDelete.subscribe(async ({ group }) => {
    /**
     * For example, notify another system that group was deleted.
     */
    await notifyAnotherSystemAboutGroupDelete({ group });
  });
});

Model
anchor

onBeforeModelCreate
anchor

This event is triggered before new model is stored into the database.

Event Arguments
anchor

PropertyDescription
modelModel object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeModelCreate.subscribe(async ({ model }) => {
    /**
     * For example, you do not want the modelId to be any of the reserved IDs you might think of.
     */
    if (reservedIdList.include(model.modelId) === false) {
      return;
    }
    throw new Error(`Model ID "${model.modelId}" is reserved.`);
  });
});

onAfterModelCreate
anchor

This event is triggered before new model is stored into the database.

Event Arguments
anchor

PropertyDescription
modelModel object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterModelCreate.subscribe(async ({ model }) => {
    /**
     * For example, you want to notify another system about new model.
     */
    await notifyAnotherSystemAboutNewModel({ model });
  });
});

onBeforeModelCreateFrom
anchor

This event is triggered before newly cloned model is stored into the database.

Event Arguments
anchor

PropertyDescription
originalModel object from which we are creating a new model
modelModel object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeModelCreateFrom.subscribe(async ({ original, model }) => {
    /**
     * For example, you want to clone only to another locale.
     */
    if (original.locale !== model.locale) {
      return;
    }
    throw new Error(`Model "${model.modelId}" cannot be cloned into same locale.`);
  });
});

onAfterModelCreateFrom
anchor

This event is triggered after newly cloned model is stored into the database.

Event Arguments
anchor

PropertyDescription
originalModel object from which we are creating a new model
modelModel object which was stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterModelCreateFrom.subscribe(async ({ original, model }) => {
    /**
     * For example, notify another system about cloned model.
     */
    await notifyAnotherSystemAboutClonedModel({ original, model });
  });
});

onBeforeModelUpdate
anchor

This event is triggered before model is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
originalModel object from which we received from the database
modelModel object which is going to be stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeModelUpdate.subscribe(async ({ model }) => {
    /**
     * For example, you want to leave all fields unlocked on all the models (you should never do that, of course, but you can if you want to).
     */
    model.lockedFields = [];
  });
});

onAfterModelUpdate
anchor

This event is triggered after model is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
originalModel object from which we received from the database
modelModel object which was stored

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterModelUpdate.subscribe(async ({ original, model }) => {
    /**
     * For example, you want to notify another system about model update.
     */
    await notifyAnotherSystemAboutModelUpdate({ original, model });
  });
});

onBeforeModelDelete
anchor

This event is triggered before model is deleted from the database.

Event Arguments
anchor

PropertyDescription
modelModel object which is going to be deleted

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeModelDelete.subscribe(async ({ model }) => {
    /**
     * For example, you do not want to allow deletion of any model.
     */
    throw new Error("Models cannot be deleted!");
  });
});

onAfterModelDelete
anchor

This event is triggered after model is deleted from the database.

Event Arguments
anchor

PropertyDescription
modelModel object which was deleted

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterModelDelete.subscribe(async ({ model }) => {
    /**
     * For example, you want to notify another system about deleted model.
     */
    await notifyAnotherSystemAboutDeletedModel({ model });
  });
});

Entry
anchor

Note that storageEntry, originalStorageEntry, latestStorageEntry and publishedStorageEntry are objects derived from the entry object which we want to store into the database. Those new objects have StorageTransformPlugin run on them to prepare them to be stored. To find out why we use StorageTransformPlugin, read about it in this article.

onBeforeEntryCreate
anchor

This event is triggered before entry is stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
entryEntry object which is going to be stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryCreate.subscribe(async ({ model, entry }) => {
    /**
     * For example, your entry has a "slug" field, which must be unique. So we check if it is.
     */
    const manager = await context.cms.getModelManager(model);

    const existing = await manager.listLatest({
      where: {
        slug: entry.values.slug
      },
      limit: 1
    });
    if (existing.length === 0) {
      return;
    }
    throw new Error(`Entry with slug "${entry.values.slug}" already exists.`);
  });
});

onAfterEntryCreate
anchor

This event is triggered after entry is stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
entryEntry object which was stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryCreate.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about new entry.
     */
    await notifyAnotherSystemAboutNewEntry({ model, entry });
  });
});

onBeforeEntryCreateRevision
anchor

This event is triggered before a new entry is created from originating entry and is stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
originalOriginal entry which we received from the database
entryEntry object which is going to be stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryCreateRevision.subscribe(async ({ model, entry }) => {
    /**
     * For example, your entry has a "slug" field, which must be unique, but not within the entryId constraint.
     */
    const manager = await context.cms.getModelManager(model);

    const existing = await manager.listLatest({
      where: {
        entryId_not: entry.entryId,
        slug: entry.values.slug
      },
      limit: 1
    });
    if (existing.length === 0) {
      return;
    }
    throw new Error(`Entry with slug "${entry.values.slug}" already exists.`);
  });
});

onAfterEntryCreateRevision
anchor

This event is triggered after a new entry is created from originating entry and is stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
originalOriginal entry which we received from the database
storageEntryEntry object prepared to be stored into the database
entryEntry object which was stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryCreateRevision.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about new entry.
     */
    await notifyAnotherSystemAboutNewEntryRevision({ model, entry });
  });
});

onBeforeEntryUpdate
anchor

This event is triggered before entry is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
originalOriginal entry which we received from the database
entryEntry object which is going to be stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryUpdate.subscribe(async ({ model, entry }) => {
    /**
     * For example, your entry has a "slug" field, which must be unique, but not within the entryId constraint.
     */
    const manager = await context.cms.getModelManager(model);

    const existing = await manager.listLatest({
      where: {
        entryId_not: entry.entryId,
        slug: entry.values.slug
      },
      limit: 1
    });
    if (existing.length === 0) {
      return;
    }
    throw new Error(`Entry with slug "${entry.values.slug}" already exists.`);
  });
});

onAfterEntryUpdate
anchor

This event is triggered after entry is changed and stored into the database.

Event Arguments
anchor

PropertyDescription
inputRaw user input
originalOriginal entry which we received from the database
storageEntryEntry object prepared to be stored into the database
entryEntry object which was stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryUpdate.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about updated entry.
     */
    await notifyAnotherSystemAboutEntryUpdate({ model, entry });
  });
});

onBeforeEntryDelete
anchor

This event is triggered before all entry revisions are deleted from the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be deleted
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryDelete.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow entry with slug "index" to be deleted.
     */
    if (entry.values.slug !== "index") {
      return;
    }
    throw new Error(`Cannot delete entry with slug "index". Not allowed!`);
  });
});

onAfterEntryDelete
anchor

This event is triggered after all entry revisions are deleted from the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be deleted
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryDelete.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about deleted entry.
     */
    await notifyAnotherSystemAboutDeletedEntry({ model, entry });
  });
});

onBeforeEntryDeleteRevision
anchor

This event is triggered before entry revision is deleted from the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be deleted
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryDeleteRevision.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow entry with version 1 to be deleted.
     */
    if (entry.version > 1) {
      return;
    }
    throw new Error(`Cannot delete entry version 1. Not allowed!`);
  });
});

onAfterEntryDeleteRevision
anchor

This event is triggered after entry revision is deleted from the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be deleted
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryDeleteRevision.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about deleted entry.
     */
    await notifyAnotherSystemAboutDeletedEntryRevision({ model, entry });
  });
});

onBeforeEntryPublish
anchor

This event is triggered before entry is published.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be published
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryPublish.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow entry with version 1 to be published.
     */
    if (entry.version > 1) {
      return;
    }
    throw new Error(`Cannot publish entry version 1. Not allowed!`);
  });
});

onAfterEntryPublish
anchor

This event is triggered after entry is published.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be published
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryPublish.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about published entry.
     */
    await notifyAnotherSystemAboutPublishedEntry({ model, entry });
  });
});

onBeforeEntryUnpublish
anchor

This event is triggered before entry is unpublished.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be unpublished
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryUnpublish.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow entry with version 1 to be unpublished.
     */
    if (entry.version > 1) {
      return;
    }
    throw new Error(`Cannot unpublish entry version 1. Not allowed!`);
  });
});

onAfterEntryUnpublish
anchor

This event is triggered after entry is unpublished.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be unpublished
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryUnpublish.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about unpublished entry.
     */
    await notifyAnotherSystemAboutUnpublishedEntry({ model, entry });
  });
});

onBeforeEntryRequestChanges
anchor

This event is triggered before request changes entry object is stored to the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be modified and stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryRequestChanges.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow to request changes on entry with version 1.
     */
    if (entry.version > 1) {
      return;
    }
    throw new Error(`Cannot request changes on entry version 1. Not allowed!`);
  });
});

onAfterEntryRequestChanges
anchor

This event is triggered after request changes entry object is stored to the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be modified and stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryRequestChanges.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about requested changes on entry.
     */
    await notifyAnotherSystemAboutRequestedChangesOnEntry({ model, entry });
  });
});

onBeforeEntryRequestReview
anchor

This event is triggered before request review entry object is stored to the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be modified and stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onBeforeEntryRequestReview.subscribe(async ({ model, entry }) => {
    /**
     * For example, you do not allow to request review on entry with version 1.
     */
    if (entry.version > 1) {
      return;
    }
    throw new Error(`Cannot request review on entry version 1. Not allowed!`);
  });
});

onAfterEntryRequestReview
anchor

This event is triggered after request review entry object is stored to the database.

Event Arguments
anchor

PropertyDescription
entryEntry object which is going to be modified and stored
modelModel this entry belongs to

How to Subscribe to This Event?
anchor

new ContextPlugin<CmsContext>(async context => {
  /**
   * There is no context.cms on options request, so we skip adding lifecycle event subscription.
   */
  if (!context.cms) {
    return;
  }
  context.cms.onAfterEntryRequestReview.subscribe(async ({ model, entry }) => {
    /**
     * For example, notify another system about requested review on entry.
     */
    await notifyAnotherSystemAboutRequestedReviewOnEntry({ model, entry });
  });
});

Please, be aware that you can change what ever you want on the object before it is stored into the database, so be careful with changing the data.

Registering Lifecycle Event Subscriptions
anchor

System Lifecycle Events
anchor

For the subscriptions (your code) to be run, you must register it in the createHandler in the api/code/graphql/src/index.ts file.

api/code/graphql/src/index.ts
const handler = createHandler({
  plugins: [
    // ... rest of plugins
    new ContextPlugin<PbContext>(async context => {
      /**
       * There is no context.cms on options request, so we skip adding lifecycle event subscription.
       */
      if (!context.cms) {
        return;
      }
      context.cms.onBeforeInstall.subscribe(async ({ system }) => {
        // do your magic here
      });
    })
  ]
});

Group, Model and Entries Lifecycle Events
anchor

For the subscriptions (your code) to be run, you must register it in the createHandler in the api/code/headlessCMS/src/index.ts file.

api/code/headlessCMS/src/index.ts
const handler = createHandler({
  plugins: [
    // ... rest of plugins
    new ContextPlugin<PbContext>(async context => {
      /**
       * There is no context.cms on options request, so we skip adding lifecycle event subscription.
       */
      if (!context.cms) {
        return;
      }
      context.cms.onBeforeEntryCreate.subscribe(async ({ entry }) => {
        // do your magic here
      });
    })
  ]
});

Please, be aware that the order of subscribing matters, so if you want some event subscription to be executed before some other one, add it first.

The check condition for context.cms will not be required starting with version 5.22.0.