Notifications
This document explains how the package notifies a site's team about content changes, by Slack message, by email, or both. It covers enabling the feature, the twelve events that can be sent, how delivery behaves when a channel fails, how the Slack URL is protected from the MCP server, and how a host application can listen to the same events directly. It is written for the developer who enables the feature and for the site administrator who chooses which events are sent.
Notifications are the human-facing counterpart to Webhooks. The two features are separate because they serve different recipients with different thresholds:
- Webhooks tell a decoupled front end to rebuild. A rebuild should run on every change, because a front end that misses a change serves outdated content.
- Notifications tell a person. A message should be sent only for the changes a person has chosen to be interrupted by, so each event has its own toggle.
Both features use the same event keys, such as content.published and content.moved (src/Support/SiteEvents.php).
Enabling notifications
Notifications are sent only when two conditions are met: the feature is enabled in the deployment's environment, and at least one destination is configured in Site settings.
One-time setup
Step 1. In .env at the site's root, enable the feature. The default is false, so upgrading the package never starts sending messages on its own.
CMS_NOTIFICATIONS_ENABLED=true
Step 2. In a terminal at the site's root, run php artisan config:clear so the new value takes effect.
Step 3. In the admin panel, on the Site settings screen (/cms/site-settings on a panel of the CMS's own), scroll to the Notifications section and fill in one or both destinations:
- Slack webhook URL: create it in Slack under Apps β Incoming Webhooks β Add to Workspace. The URL determines the channel, so one URL posts to one channel.
- Email recipients: any number of addresses. Press Enter after each address.
Either destination alone is enough. With neither filled in, nothing is sent and no delivery is attempted, whatever the event toggles say. An entry in Email recipients that is not a valid email address is skipped, so that one mistyped address does not fail the whole send.
Step 4. In the same section, under Send on, tick the events to be notified about. The events are listed under Events below.
Step 5. In the same section, click Send a test message in the section header. The test is sent immediately rather than queued, and it uses the values currently in the form, so a URL can be tested before the form is saved. A success notification titled "Test message sent" or an error notification titled "Could not send the test message", with the error text, appears on screen.
Step 6. At the bottom of the screen, click Save.
Behavior while the feature is disabled
While CMS_NOTIFICATIONS_ENABLED is false, the Notifications section shows a warning that notifications are switched off for the installation. The fields still save, and Send a test message still delivers, because the test posts directly to the destinations. The listener that turns a published page into a message (NotifyPeople) is registered only when cms.notifications.enabled is true. A successful test therefore proves only that the destination works. When the feature is disabled, the success notification says so and stays on screen until it is dismissed.
Send a test message requires the cms.settings.update ability when permissions are enabled.
Events
| Event (label under Send on) | Key | Sent when | Ticked by default |
|---|---|---|---|
| Content published | content.published | A version goes live | Yes |
| Content unpublished | content.unpublished | A page is withdrawn from the site | Yes |
| Content scheduled | content.scheduled | A page is given a date to go live or come down | No |
| Draft saved | content.saved | An existing page is saved without publishing | No |
| Content deleted | content.deleted | A page is moved to the bin | Yes |
| Content restored | content.restored | A page is brought back from the bin | No |
| Page moved | content.moved | A page changes address | No |
| Media uploaded | media.uploaded | A new file is added to the media library | No |
| Media replaced | media.replaced | A file is swapped for a new one, keeping its place on every page that uses it | No |
| Redirect created | redirect.created | Any redirect is created, whether typed by an editor or written automatically | No |
| Webhook endpoints changed | webhooks.changed | An endpoint is added, edited or removed in the Webhooks section | No |
| Form submitted | form.submitted | A form is submitted. The message names the form and never includes the answers | No |
Three events are ticked by default: a publish, a withdrawal and a deletion are the changes a team most often needs to know about. The other nine are off, because a channel that reports every save stops being read.
A site whose Send on list has never been saved gets the three defaults. Once the list has been saved, it is used exactly as saved: unticking every event sends nothing, and the defaults do not return.
Behavior of individual events
- Draft saved is sent on every save of an existing page, which happens often while an editor is working. Two saves are not reported: a page's first version, which is a creation rather than an edit, and a save whose version has become the published version by the time the queued job runs, which is the save half of a save-and-publish. Without that second rule, a save-and-publish would be reported twice.
- Redirect created overlaps with Page moved. Moving a page writes a redirect automatically, so with both events ticked, one rename produces two messages.
- Form submitted is in addition to each form's own recipients. Each form already emails its own recipient list with every answer and a reply-to address. This event is a separate, short message for a team channel, and a person on both lists receives both. The answers are never included in this message, so text typed into a support form is not posted to a channel everyone in it can read.
- Media uploaded is sent only for files added to the CMS media library, not for files a host application adds to its own Spatie media collections. Replacing a file sends Media replaced, not Media uploaded.
Delivery
Sending never blocks or fails the change that caused it:
NotifyPeopleimplementsShouldQueueAfterCommit, so it runs on the queue after the database transaction that wrote the change has committed.- Slack and email are attempted independently. If Slack is unreachable, the email is still sent, and neither failure affects the save.
- A failed attempt is logged as a warning, "CMS notification via slack failed" or "CMS notification via mail failed", with the error message.
- The Slack request times out after
cms.notifications.timeoutseconds (default 5, set withCMS_NOTIFICATIONS_TIMEOUT). Emails are queued as separate mail jobs.
With a real queue driver, a queue worker must be running for notifications to be sent. With the sync driver, they are sent during the request.
The Slack URL and the MCP server
The list of refused groups is built in code, not read from config alone. SettingsPolicy::denied() (src/Mcp/Support/SettingsPolicy.php) returns the groups in cms.mcp.settings.denied, which ships as ['analytics'], with webhooks and notifications always added. The reason is that Laravel's mergeConfigFrom is a shallow merge: it supplies a top-level key the application's published config is missing, but it does not descend into a key the application has already published. An installation that published config/cms.php before either group existed would never receive a config addition, and would continue to return both groups in full to any MCP client that asked. That is how the webhooks group was once served with its signing secrets included. An application can add further groups to cms.mcp.settings.denied; it cannot remove webhooks or notifications from the refused list.
Configuration
'notifications' => [
'enabled' => (bool) env('CMS_NOTIFICATIONS_ENABLED', false),
'timeout' => (int) env('CMS_NOTIFICATIONS_TIMEOUT', 5),
],
| Key | Environment variable | Default | Effect |
|---|---|---|---|
| notifications.enabled | CMS_NOTIFICATIONS_ENABLED | false | Registers the NotifyPeople subscriber. When false, no event sends a message |
| notifications.timeout | CMS_NOTIFICATIONS_TIMEOUT | 5 | Timeout, in seconds, of the HTTP request to the Slack webhook |
Listening to the events directly
Every notification is driven by an ordinary Laravel event, so a host application can listen to the same events without using the notification feature:
Mcg\FilamentCms\Events\ContentPublished ContentUnpublished
Mcg\FilamentCms\Events\ContentScheduled ContentSaved
Mcg\FilamentCms\Events\ContentDeleted ContentRestored
Mcg\FilamentCms\Events\ContentPathChanged MediaUploaded
Mcg\FilamentCms\Events\MediaReplaced RedirectCreated
Mcg\FilamentCms\Events\WebhooksChanged FormSubmitted
These twelve classes are the complete set of domain events. Five of them were added for the notification feature: ContentScheduled, MediaUploaded, RedirectCreated, WebhooksChanged and FormSubmitted. The other seven existed before it. See Integrating another package for listening to them from a host application.
Mcg\FilamentCms\Events holds two further classes, ContentUpdatedForViewers and SiteUpdatedForViewers, which are not domain events. They implement ShouldBroadcastNow and tell a browser that is already showing a page to refresh. No notification is sent for either. Neither is suitable for triggering other work:
ContentUpdatedForViewersis dispatched on both save and publish, so a listener on it runs twice for a save-and-publish.- It is dispatched inside a
try/catch, so that a broadcasting failure cannot break the write. A listener on it therefore does not run at all when broadcasting throws.
MediaUploaded is dispatched differently from the other events. It is dispatched from Spatie's MediaHasBeenAddedEvent (by src/Listeners/DispatchMediaUploaded.php) rather than from the media model's created event. Spatie's library writes the database row before it copies the file, and force-deletes the row if the copy fails. When MediaUploaded is dispatched, the file is already on the disk.