diff --git a/docs/developer-docs/5.40.x/page-builder/references/lifecycle-events.mdx b/docs/developer-docs/5.40.x/page-builder/references/lifecycle-events.mdx
new file mode 100644
index 000000000..c0124faf0
--- /dev/null
+++ b/docs/developer-docs/5.40.x/page-builder/references/lifecycle-events.mdx
@@ -0,0 +1,977 @@
+---
+id: aafeab60
+title: Lifecycle Events
+description: Learn about Page Builder lifecycle events, how they work and how to subscribe to a lifecycle event.
+---
+
+import { Alert } from "@/components/Alert";
+
+
+
+- what are lifecycle events
+- how lifecycle events work
+- how to subscribe to a lifecycle event
+
+
+
+## Overview
+
+In our Page Builder we provide lifecycle events available for you to hook into.
+Lifecycle events are triggered before (`onBefore` keyword) and after (`onAfter` keyword) the data is stored into the database.
+
+With `onBefore` events you can change the data that is being stored into the database, so be careful with that.
+
+With the lifecycle events you can hook into a number of different operations, for example:
+
+- change the page data which is going to be stored
+- notify another system that new page was stored
+
+## System
+
+### onBeforeInstall
+
+This event is triggered before the installation of the Page Builder and insertion of initial `Welcome to Webiny` and `Not Found` pages.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------ |
+| tenant | ID of the current tenant |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeInstall.subscribe(async ({ tenant }) => {
+ await notifyAnotherSystemThatPageBuilderIsGettingInstalled({ tenant });
+ });
+});
+```
+
+### onAfterInstall
+
+This event is triggered after the installation of the Page Builder and insertion of initial `Welcome to Webiny` and `Not Found` pages.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------ |
+| tenant | ID of the current tenant |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterInstall.subscribe(async ({ tenant }) => {
+ await notifyAnotherSystemThatPageBuilderWasInstalled({ tenant });
+ });
+});
+```
+
+## Settings
+
+### onBeforeSettingsUpdate
+
+This event is triggered before settings data is going to be stored.
+
+#### Event arguments
+
+| Property | Description |
+| --------------- | ---------------------------------------------------- |
+| original | Settings object which was received from the database |
+| settings | Settings object which was changed by user input |
+| meta | Metadata |
+| meta.diff.pages | Array which contains which pages were changed |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeSettingsUpdate.subscribe(async ({ meta }) => {
+ await triggerPreparePrerenderingOfChangedPages({ pages: meta.diff.pages });
+ });
+});
+```
+
+### onAfterSettingsUpdate
+
+This event is triggered after settings data was stored.
+
+#### Event arguments
+
+| Property | Description |
+| --------------- | ---------------------------------------------------- |
+| original | Settings object which was received from the database |
+| settings | Settings object which was changed by user input |
+| meta | Metadata |
+| meta.diff.pages | Array which contains calculated page changes |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterSettingsUpdate.subscribe(async ({ original, settings, meta }) => {
+ await storeSettingsToAnotherSystem({ original, settings });
+ await triggerPrerenderingOfChangedPages({ pages: meta.diff.pages });
+ });
+});
+```
+
+## Categories
+
+### onBeforeCategoryCreate
+
+This event is triggered before new category is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------------------------- |
+| category | Category object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeCategoryCreate.subscribe(async ({ category }) => {
+ /**
+ * For example, you do not want the category name to contain the character "a".
+ * You can check for that character here and throw an error. Or remove it.
+ */
+ if (category.name.match(/a/) === null) {
+ return;
+ }
+ category.name = category.name.replace(/a/g, "");
+ });
+});
+```
+
+### onAfterCategoryCreate
+
+This event is triggered after new category is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | -------------------------------- |
+| category | Category object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterCategoryCreate.subscribe(async ({ category }) => {
+ await storeCategoryToAnotherSystem({ category });
+ });
+});
+```
+
+### onBeforeCategoryUpdate
+
+This event is triggered before existing category is updated and stored.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------------------------------- |
+| original | Category object which was received from the database |
+| category | Category object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeCategoryUpdate.subscribe(async ({ original, category }) => {
+ /**
+ * For example, you do not want to allow category slug changes.
+ */
+ if (original.slug === category.slug) {
+ return;
+ }
+ throw new Error(`You are not allowed to change the category slug.`);
+ });
+});
+```
+
+### onAfterCategoryUpdate
+
+This event is triggered after existing category is updated and stored.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------------------------------- |
+| original | Category object which was received from the database |
+| category | Category object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterCategoryUpdate.subscribe(async ({ original, category }) => {
+ await storeCategoryToAnotherSystem({ original, category });
+ });
+});
+```
+
+### onBeforeCategoryDelete
+
+This event is triggered before category is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | -------------------------------------------- |
+| category | Category object which is going to be deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeCategoryDelete.subscribe(async ({ original, category }) => {
+ /**
+ * For example, we do not want to allow any category with a name which contains character "b" to be deleted.
+ */
+ if (category.name.match(/b/) === null) {
+ return;
+ }
+ throw new Error(`You are not allowed to delete a category with charcter "b" in its name.`);
+ });
+});
+```
+
+### onAfterCategoryDelete
+
+This event is triggered after category is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | --------------------------------- |
+| category | Category object which was deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterCategoryDelete.subscribe(async ({ original, category }) => {
+ await deleteCategoryFromAnotherSystem({ original, category });
+ });
+});
+```
+
+## Menus
+
+### onBeforeMenuCreate
+
+This event is triggered before new menu is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | --------------------------------------- |
+| input | Input received from user |
+| menu | Menu object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeMenuCreate.subscribe(async ({ menu }) => {
+ /**
+ * For example, do not allow empty menu description.
+ */
+ if (menu.description) {
+ return;
+ }
+ throw new Error(`Empty menu description is not allowed.`);
+ });
+});
+```
+
+### onAfterMenuCreate
+
+This event is triggered after new menu is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------- |
+| input | Input received from user |
+| menu | Menu object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterMenuCreate.subscribe(async ({ menu }) => {
+ await notifyAnotherSystemAboutNewMenu({ menu });
+ });
+});
+```
+
+### onBeforeMenuUpdate
+
+This event is triggered before existing menu is changed and stored.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------------------------------ |
+| input | Input received from user |
+| original | Menu object which was received from the database |
+| menu | Menu object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeMenuUpdate.subscribe(async ({ menu }) => {
+ /**
+ * For example, do not allow empty menu description.
+ */
+ if (menu.description) {
+ return;
+ }
+ throw new Error(`Empty menu description is not allowed.`);
+ });
+});
+```
+
+### onAfterMenuUpdate
+
+This event is triggered after existing menu is changed and stored.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------------------------------ |
+| input | Input received from user |
+| original | Menu object which was received from the database |
+| menu | Menu object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterMenuUpdate.subscribe(async ({ original, menu }) => {
+ await notifyAnotherSystemAboutMenuUpdate({ original, menu });
+ });
+});
+```
+
+### onBeforeMenuDelete
+
+This event is triggered before existing menu is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------------------- |
+| menu | Menu object which is going to be deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforeMenuDelete.subscribe(async ({ menu }) => {
+ /**
+ * For example, do not allow menu with non-empty description to be deleted.
+ */
+ if (!menu.description) {
+ return;
+ }
+ throw new Error(`Menu with non-empty description cannot be deleted.`);
+ });
+});
+```
+
+### onAfterMenuDelete
+
+This event is triggered after existing menu is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ----------------------------- |
+| menu | Menu object which was deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterMenuDelete.subscribe(async ({ menu }) => {
+ await notifyAnotherSystemAboutMenuDelete({ menu });
+ });
+});
+```
+
+## Page Elements
+
+### onBeforePageElementCreate
+
+This event is triggered before new page element is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | ----------------------------------------------- |
+| pageElement | Page element object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageElementCreate.subscribe(async ({ pageElement }) => {
+ /**
+ * For example, change the default generated ID to uuid v4
+ */
+ pageElement.id = uuidv4();
+ });
+});
+```
+
+### onAfterPageElementCreate
+
+This event is triggered after new page element is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | ----------------------------------- |
+| pageElement | Page element object which is stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageElementCreate.subscribe(async ({ pageElement }) => {
+ await notifyAnotherSystemAboutNewPageElement({ pageElement });
+ });
+});
+```
+
+### onBeforePageElementUpdate
+
+This event is triggered before existing page element is changed and stored.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | -------------------------------------------------------- |
+| original | Page element object which was received from the database |
+| pageElement | Page element object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageElementUpdate.subscribe(async ({ original, pageElement }) => {
+ /**
+ * For example, do not allow id to be changed
+ */
+ if (original.id === pageElement.id) {
+ return;
+ }
+ throw new Error(`You cannot change the ID of the page element.`);
+ });
+});
+```
+
+### onAfterPageElementUpdate
+
+This event is triggered after existing page element is changed and stored.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | -------------------------------------------------------- |
+| original | Page element object which was received from the database |
+| pageElement | Page element object which is stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageElementUpdate.subscribe(async ({ original, pageElement }) => {
+ await notifyAnotherSystemAboutPageElementUpdate({ original, pageElement });
+ });
+});
+```
+
+### onBeforePageElementDelete
+
+This event is triggered before page element is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | ------------------------------------------------ |
+| pageElement | Page element object which is going to be deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageElementDelete.subscribe(async ({ pageElement }) => {
+ /**
+ * For example, do not allow ANY page element to be deleted.
+ */
+ throw new Error(`You cannot delete page element.`);
+ });
+});
+```
+
+### onAfterPageElementDelete
+
+This event is triggered after page element is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| ----------- | ------------------------------------- |
+| pageElement | Page element object which was deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageElementDelete.subscribe(async ({ pageElement }) => {
+ await notifyAnotherSystemAboutPageElementDelete({ pageElement });
+ });
+});
+```
+
+## Pages
+
+### onBeforePageCreate
+
+This event is triggered before a new page is stored into the database. This event is not triggered when creating a page from another page.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | --------------------------------------- |
+| page | Page object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageCreate.subscribe(async ({ page }) => {
+ /**
+ * For example, set default title.
+ */
+ page.title = `Page created on ${new Date().toISOString()}`;
+ });
+});
+```
+
+### onAfterPageCreate
+
+This event is triggered after new page is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------- |
+| page | Page object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageCreate.subscribe(async ({ page }) => {
+ /**
+ * For example, notify another system that a new page was created.
+ */
+ await notifyAnotherSystemAboutNewPage({ page });
+ });
+});
+```
+
+### onBeforePageCreateFrom
+
+This event is triggered before a new page, which is created from another page, is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | -------------------------------------------- |
+| original | Page object which is the base for a new page |
+| page | Page object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageCreateFrom.subscribe(async ({ original, page }) => {
+ /**
+ * For example, change the title of the page so user knows it's a clone.
+ */
+ page.title = `Page is a clone of ${original.title}`;
+ });
+});
+```
+
+### onAfterPageCreateFrom
+
+This event is triggered after a new page, which is created from another page, is stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | -------------------------------------------- |
+| original | Page object which is the base for a new page |
+| page | Page object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageCreateFrom.subscribe(async ({ original, page }) => {
+ /**
+ * For example, notify another system that a new page was created from the original one.
+ */
+ await notifyAnotherSystemAboutClonedPage({ original, page });
+ });
+});
+```
+
+### onBeforePageUpdate
+
+This event is triggered before a page is changed and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------------------------------ |
+| original | Page object which was received from the database |
+| page | Page object which is going to be stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageUpdate.subscribe(async ({ original, page }) => {
+ /**
+ * For example, check if the page path is unique and if it's not, add some random string at the end of the path.
+ */
+ const unique = await yourCustomIsUniqueFunction(page);
+ if (unique) {
+ return;
+ }
+ page.path = `${page.path}-${yourRandomStringGenerator()}`;
+ });
+});
+```
+
+### onAfterPageUpdate
+
+This event is triggered after a page changed and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ------------------------------------------------ |
+| original | Page object which was received from the database |
+| page | Page object which was stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageUpdate.subscribe(async ({ original, page }) => {
+ /**
+ * For example, notify another system that a page was changed and stored.
+ */
+ await notifyAnotherSystemAboutPageUpdate({ original, page });
+ });
+});
+```
+
+### onBeforePageDelete
+
+This event is triggered before a page is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ---------------------------------------- |
+| page | Page object which is going to be deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageDelete.subscribe(async ({ page }) => {
+ /**
+ * For example, we do not allow a page which has "index" set as path to be deleted
+ */
+ if (page.path !== "index") {
+ return;
+ }
+ throw new Error(`Page with path "index" cannot be deleted.`);
+ });
+});
+```
+
+### onAfterPageDelete
+
+This event is triggered after a page is deleted from the database.
+
+#### Event arguments
+
+| Property | Description |
+| -------- | ----------------------------- |
+| page | Page object which was deleted |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageDelete.subscribe(async ({ page }) => {
+ /**
+ * For example, notify another system that a page was deleted.
+ */
+ await notifyAnotherSystemAboutPageDelete({ page });
+ });
+});
+```
+
+### onBeforePagePublish
+
+This event is triggered before a page is changed and stored into the database as the published one.
+
+#### Event arguments
+
+| Property | Description |
+| ------------- | ----------------------------------------------------------------------------------------------------------- |
+| publishedPage | Page object that is set as the published one in the database - published revision of page we are publishing |
+| latestPage | Page object of the last revision of the page we are publishing |
+| page | Page object which is going to be published |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePagePublish.subscribe(async ({ latestPage, page }) => {
+ /**
+ * For example, we do not allow a page which is not the latest one to be published.
+ */
+ if (latestPage.version > page.version) {
+ throw new Error(`Page you are trying to publish is not the latest revision of the page.`);
+ }
+ });
+});
+```
+
+### onAfterPagePublish
+
+This event is triggered after a page is changed and stored into the database as the published one.
+
+#### Event arguments
+
+| Property | Description |
+| ------------- | ----------------------------------------------------------------------------------------------------------- |
+| publishedPage | Page object that is set as the published one in the database - published revision of page we are publishing |
+| latestPage | Page object of the last revision of the page we are publishing |
+| page | Page object which was published |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPagePublish.subscribe(async ({ publishedPage, latestPage, page }) => {
+ /**
+ * For example, notify another system of a published page.
+ */
+ await notifyAnotherSystemAboutPublishedPage({ publishedPage, page });
+ });
+});
+```
+
+### onBeforePageUnpublish
+
+This event is triggered before a page is changed and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ---------------------------------------------------------------- |
+| latestPage | Page object of the last revision of the page we are unpublishing |
+| page | Page object which is going to be unpublished |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageUnpublish.subscribe(async ({ latestPage, page }) => {
+ /**
+ * For example, we do not allow a page which is the latest one to be unpublished.
+ */
+ if (latestPage.version === page.version) {
+ throw new Error(
+ `Page you are trying to unpublish is the latest revision of the page and it is not allowed to unpublish it.`
+ );
+ }
+ });
+});
+```
+
+### onAfterPageUnpublish
+
+This event is triggered after a page is changed and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ---------------------------------------------------------------- |
+| latestPage | Page object of the last revision of the page we are unpublishing |
+| page | Page object which was unpublished |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageUnpublish.subscribe(async ({ page }) => {
+ /**
+ * For example, notify another system that page was unpublished.
+ */
+ await notifyAnotherSystemAboutUnpublishedPage({ page });
+ });
+});
+```
+
+### onBeforePageRequestChanges
+
+This event is triggered before a page is marked as Requested Changes status and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ------------------------------------------------------------------------- |
+| latestPage | Page object of the last revision of the page we are requesting changes on |
+| page | Page object which is going to be set into Requested Changes status |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageRequestChanges.subscribe(async ({ page }) => {
+ /**
+ * For example, we will check another system if request changes can be done on this page.
+ */
+ const allowed = await canRequestChangesBeDone({ page });
+ if (allowed) {
+ return;
+ }
+ throw new Error(`You cannot request changes on this page.`);
+ });
+});
+```
+
+### onAfterPageRequestChanges
+
+This event is triggered after a page is marked as Requested Changes status and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ------------------------------------------------------------------------- |
+| latestPage | Page object of the last revision of the page we are requesting changes on |
+| page | Page object which was set into Requested Changes status and stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageRequestChanges.subscribe(async ({ page }) => {
+ /**
+ * For example, notify another system that a request changes was set on this page.
+ */
+ await notifyAnotherSystemAboutRequestChanges({ page });
+ });
+});
+```
+
+### onBeforePageRequestReview
+
+This event is triggered before a page is marked as Requested Review status and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ------------------------------------------------------------------------ |
+| latestPage | Page object of the last revision of the page we are requesting review on |
+| page | Page object which is going to be set into Requested Review status |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageRequestReview.subscribe(async ({ page }) => {
+ /**
+ * For example, we will check another system if request review can be done on this page.
+ */
+ const allowed = await canRequestReviewBeDone({ page });
+ if (allowed) {
+ return;
+ }
+ throw new Error(`You cannot request review on this page.`);
+ });
+});
+```
+
+### onAfterPageRequestReview
+
+This event is triggered after a page is marked as Requested Review status and stored into the database.
+
+#### Event arguments
+
+| Property | Description |
+| ---------- | ------------------------------------------------------------------------ |
+| latestPage | Page object of the last revision of the page we are requesting review on |
+| page | Page object which was set into Requested Review status and stored |
+
+#### How to subscribe to this event?
+
+```typescript
+new ContextPlugin(async context => {
+ context.pageBuilder.onAfterPageRequestReview.subscribe(async ({ page }) => {
+ /**
+ * For example, notify another system that a request review was set on this page.
+ */
+ await notifyAnotherSystemAboutRequestReview({ page });
+ });
+});
+```
+
+
+
+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
+
+For the subscriptions (your code) to be run, you must register it in the `createHandler` in the `api/code/graphql/src/index.ts` file.
+
+```typescript api/code/graphql/src/index.ts
+const handler = createHandler({
+ plugins: [
+ // ... rest of plugins
+ new ContextPlugin(async context => {
+ context.pageBuilder.onBeforePageCreate.subscribe(async ({ page }) => {
+ // 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.
+
+
diff --git a/docs/release-notes/5.40.0/changelog.mdx b/docs/release-notes/5.40.0/changelog.mdx
index 11f11f236..b800beb1f 100644
--- a/docs/release-notes/5.40.0/changelog.mdx
+++ b/docs/release-notes/5.40.0/changelog.mdx
@@ -10,9 +10,13 @@ import { GithubRelease } from "@/components/GithubRelease";
## Breaking Changes❗
-### Removed Deprecated Code ([#4051](https://github.com/webiny/webiny-js/pull/4051))
+### Removed Deprecated APIs ([#4051](https://github.com/webiny/webiny-js/pull/4051))
-✍️
+With this release, we've removed a couple of backend JavaScript APIs that were marked as deprecated in previous releases.
+
+Essentially, we've removed a couple of older methods used for subscribing to lifecycle events and also for performing Headless CMS-related security permissions checks.
+
+For more information and how to migrate your code, please refer to the separate [Deprecated APIs](/docs/release-notes/5.40.0/deprecated-apis) article.
## Other Improvements
diff --git a/docs/release-notes/5.40.0/deprecated-apis.mdx b/docs/release-notes/5.40.0/deprecated-apis.mdx
new file mode 100644
index 000000000..39a0b45fd
--- /dev/null
+++ b/docs/release-notes/5.40.0/deprecated-apis.mdx
@@ -0,0 +1,179 @@
+---
+id: cd0e6f97
+title: Deprecated APIs
+description: Learn about the deprecated APIs and their replacements in this guide.
+---
+
+import { Alert } from "@/components/Alert";
+
+## Overview
+
+As mentioned in the [changelog](/docs/release-notes/5.40.0/changelog#removed-deprecated-ap-is-4051), with this release, we've removed a couple of backend JavaScript APIs that were marked as deprecated in previous releases.
+
+In this guide, we'll go through the list of deprecated APIs and their replacements.
+
+## Headless CMS
+
+We've removed a set of security permissions checking-related methods that were accessible via the `context.cms.permissions` object. To provide more context, here's an example of how the object could've been used in a simple `CmsGraphQLSchemaPlugin` plugin:
+
+```ts
+import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms";
+import { ErrorResponse, Response } from "@webiny/handler-graphql";
+
+new CmsGraphQLSchemaPlugin({
+ typeDefs: /* GraphQL */ `
+ extend type Query {
+ myListVideos: VideosListResponse
+ }
+ `,
+ resolvers: {
+ Query: {
+ myListVideos: async (_, __, context) => {
+ // Ensure user can read entries.
+ const hasAccess = await context.cms.permissions.entries.ensure({
+ rwd: "r"
+ });
+
+ if (!hasAccess) {
+ throw new ErrorResponse({
+ message: "Unauthorized: Insufficient access rights."
+ });
+ }
+
+ // ...
+ }
+ }
+ }
+});
+```
+
+In the example above, the `context.cms.permissions.entries.ensure` method is used to check if the current user has the required permissions to read entries.
+
+With this release, the `context.cms.permissions` object has been removed. Instead, you should use the `context.cms.accessControl` object to perform the same checks.
+
+
+
+ With this [5.39.2](/docs/release-notes/5.39.2/changelog#new-authorization-related-utilities-3865) release, we’ve introduced a new property on the CMS context object, called `accessControl`. Essentially, this property is a reference to the [AccessControl](https://github.com/webiny/webiny-js/blob/dev/packages/api-headless-cms/src/crud/AccessControl/AccessControl.ts) class instance, which is used to perform authorization checks on content model groups, models, and entries.
+
+
+
+If we were to rewrite the previous example using the new `context.cms.accessControl` object, it would look like this:
+
+```diff-ts
+import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms";
+import { ErrorResponse, Response } from "@webiny/handler-graphql";
+
+new CmsGraphQLSchemaPlugin({
+ typeDefs: /* GraphQL */ `
+ extend type Query {
+ myListVideos: VideosListResponse
+ }
+ `,
+ resolvers: {
+ Query: {
+ myListVideos: async (_, __, context) => {
+ // Ensure user can read entries.
++ const model = await context.cms.getModel("myVideo");
++ const hasAccess = await context.cms.accessControl.canAccessEntry({
++ model,
++ rwd: "r"
++ });
+
+ if (!hasAccess) {
+ throw new ErrorResponse({
+ message: "Unauthorized: Insufficient access rights."
+ });
+ }
+
+ // ...
+ }
+ }
+ }
+});
+```
+
+## Page Builder
+
+Multiple legacy methods used for subscribing to Page Builder application's lifecycle events have been removed.
+
+
+
+ Learn more about lifecycle events in the [Lifecycle Events](/docs/page-builder/references/lifecycle-events) article.
+
+
+
+### Pages
+
+| Deprecated API | Description | Replacement API |
+| ------------------------ | ------------------------------------------------------- | ------------------------ |
+| `onBeforePageCreate` | Triggered before a page is created | `onPageBeforeCreate` |
+| `onAfterPageCreate` | Triggered after a page is created | `onPageAfterCreate` |
+| `onBeforePageCreateFrom` | Triggered before a page is created from a page revision | `onPageBeforeCreateFrom` |
+| `onAfterPageCreateFrom` | Triggered after a page is created from a page revision | `onPageAfterCreateFrom` |
+| `onBeforePageUpdate` | Triggered before a page is updated | `onPageBeforeUpdate` |
+| `onAfterPageUpdate` | Triggered after a page is updated | `onPageAfterUpdate` |
+| `onBeforePageDelete` | Triggered before a page is deleted | `onPageBeforeDelete` |
+| `onAfterPageDelete` | Triggered after a page is deleted | `onPageAfterDelete` |
+| `onBeforePagePublish` | Triggered before a page is published | `onPageBeforePublish` |
+| `onAfterPagePublish` | Triggered after a page is published | `onPageAfterPublish` |
+| `onBeforePageUnpublish` | Triggered before a page is unpublished | `onPageBeforeUnpublish` |
+| `onAfterPageUnpublish` | Triggered after a page is unpublished | `onPageAfterUnpublish` |
+
+### Menus
+
+| Deprecated API | Description | Replacement API |
+| -------------------- | ---------------------------------- | -------------------- |
+| `onBeforeMenuCreate` | Triggered before a menu is created | `onMenuBeforeCreate` |
+| `onAfterMenuCreate` | Triggered after a menu is created | `onMenuAfterCreate` |
+| `onBeforeMenuUpdate` | Triggered before a menu is updated | `onMenuBeforeUpdate` |
+| `onAfterMenuUpdate` | Triggered after a menu is updated | `onMenuAfterUpdate` |
+| `onBeforeMenuDelete` | Triggered before a menu is deleted | `onMenuBeforeDelete` |
+| `onAfterMenuDelete` | Triggered after a menu is deleted | `onMenuAfterDelete` |
+
+### Page Categories
+
+| Deprecated API | Description | Replacement API |
+| ------------------------ | -------------------------------------- | ------------------------ |
+| `onBeforeCategoryCreate` | Triggered before a category is created | `onCategoryBeforeCreate` |
+| `onAfterCategoryCreate` | Triggered after a category is created | `onCategoryAfterCreate` |
+| `onBeforeCategoryUpdate` | Triggered before a category is updated | `onCategoryBeforeUpdate` |
+| `onAfterCategoryUpdate` | Triggered after a category is updated | `onCategoryAfterUpdate` |
+| `onBeforeCategoryDelete` | Triggered before a category is deleted | `onCategoryBeforeDelete` |
+| `onAfterCategoryDelete` | Triggered after a category is deleted | `onCategoryAfterDelete` |
+
+### Page Builder Settings
+
+| Deprecated API | Description | Replacement API |
+| ------------------------ | -------------------------------------------------- | ------------------------ |
+| `onBeforeSettingsUpdate` | Triggered before Page Builder settings are updated | `onSettingsBeforeUpdate` |
+| `onAfterSettingsUpdate` | Triggered after Page Builder settings are updated | `onSettingsAfterUpdate` |
+
+### Page Builder Installation
+
+| Deprecated API | Description | Replacement API |
+| ----------------- | ------------------------------------------ | ----------------------- |
+| `onBeforeInstall` | Triggered before Page Builder is installed | `onSystemBeforeInstall` |
+| `onAfterInstall` | Triggered after Page Builder is installed | `onSystemAfterInstall` |
+
+## I18N
+
+Multiple legacy methods used for subscribing to I18N application's lifecycle events have been removed.
+
+### Locales
+
+| Deprecated API | Description | Replacement API |
+| ------------------------ | -------------------------------------- | ------------------------ |
+| `onBeforeLocaleCreate` | Triggered before a locale is created | `onLocaleBeforeCreate` |
+| `onAfterLocaleCreate` | Triggered after a locale is created | `onLocaleAfterCreate` |
+| `onBeforeLocaleUpdate` | Triggered before a locale is updated | `onLocaleBeforeUpdate` |
+| `onAfterLocaleUpdate` | Triggered after a locale is updated | `onLocaleAfterUpdate` |
+| `onBeforeLocaleDelete` | Triggered before a locale is deleted | `onLocaleBeforeDelete` |
+| `onAfterLocaleDelete` | Triggered after a locale is deleted | `onLocaleAfterDelete` |
+
+
+### Installation
+
+| Deprecated API | Description | Replacement API |
+| ----------------- | ------------------------------------------ | ----------------------- |
+| `onBeforeInstall` | Triggered before Page Builder is installed | `onSystemBeforeInstall` |
+| `onAfterInstall` | Triggered after Page Builder is installed | `onSystemAfterInstall` |