from the inbox to the system message
Magento already ships a complete notification system through Magento_AdminNotification, visible via the bell icon in the top right corner of the backend: inbox notifications with severity levels and an unread counter, plus system messages shown as a red or yellow banner. Custom modules can tap into this system deliberately to report certificate expirations, failed API synchronizations or low stock levels directly inside the admin panel instead of forcing developers to dig through log files. This article shows how to build a custom Admin Notifications setup based on InboxInterface, MessageInterface, cron jobs and ACL rules, including complete code examples for Magento 2.4.8.
Table of Contents
- 1. How the built-in notification system works
- 2. Severity levels and the inbox data structure
- 3. A custom repository for inbox notifications
- 4. A cron job as the trigger for notifications
- 5. Registering system messages via di.xml
- 6. System message vs. inbox notification
- 7. ACL control: who sees which notification
- 8. A ViewModel for custom notification UI
- 9. Notification channels compared
- 10. Summary
- 11. FAQ
1. How the built-in notification system works
The Magento_AdminNotification module provides the bell icon in the top right of the backend that every Magento administrator recognizes. Behind that bell sits the adminnotification_inbox table, managed through Magento\AdminNotification\Model\Inbox and its resource model. Every row in that table is an Admin Notification with a title, description, optional link, severity level and an is_read flag. The unread counter on the bell is a simple aggregation over is_read = 0, recalculated on every backend page load.
Magento uses this system heavily itself: security warnings about outdated Composer packages, hints about available patches and messages from the official Magento security feed all land as backend notifications in exactly this inbox. The update_notifications cron job that ships with Magento_AdminNotification polls an external feed at regular intervals and writes new entries into the table via InboxFactory. This exact mechanism is what makes custom modules interesting: instead of an external feed, a custom cron job checks a project-specific condition, for example whether an SSL certificate is about to expire or whether a nightly API synchronization succeeded, and writes a new row into the same inbox table whenever needed.
The second building block of the notification system is independent of the inbox: system messages. They appear as a red or yellow banner directly below the admin navigation and are provided through implementations of Magento\Framework\Notification\MessageInterface, registered in a virtual list (Magento\Framework\Notification\MessageList). Both mechanisms, the inbox and the system message, exist in parallel and independently of each other, but can be combined in a single module to provide both a durable, historized message and an urgent, immediately visible warning.
2. Severity levels and the inbox data structure
Every inbox notification carries a severity level, defined through three constants on Magento\AdminNotification\Model\Inbox: NOTICE_SEVERITY_MINOR (value 1), NOTICE_SEVERITY_MAJOR (value 2) and NOTICE_SEVERITY_CRITICAL (value 3). The severity level controls not only the color of the icon in the notification list but also which entry gets prioritized at the top when the bell is opened. A MINOR message suits informational hints such as completed maintenance work, MAJOR suits warnings like an API key about to expire, and CRITICAL suits states that demand immediate action, for example a failed payment provider synchronization.
The columns of the adminnotification_inbox table are deliberately generic: severity, date_added, title, description, url, is_read, is_remove and notification_type. For most custom Admin Notifications this structure is entirely sufficient, without requiring a custom table through db_schema.xml. Only once project-specific metadata needs to be permanently linked to the notification, such as a reference to a specific order or a supplier record, does a custom extension table with a foreign key relationship pay off. In that case, the custom db_schema.xml defines a new table that references the inbox ID as a foreign key rather than modifying Magento's core table.
The is_remove field distinguishes between dismissible and persistent notifications. When is_remove is set to 1, the administrator can permanently remove the message via the X icon. When it is 0, the backend notification stays in the inbox until it is explicitly marked as read through the Magento\AdminNotification\Controller\Adminhtml\Notification\MarkAsRead controller. For critical compliance hints that should not be accidentally dismissed, is_remove = 0 is the right choice, while informational hints are typically dismissible.
3. A custom repository for inbox notifications
Rather than populating the inbox directly through the generic InboxFactory inside business logic classes, a dedicated repository following the service contract pattern is recommended. It encapsulates the creation logic, makes it testable, and allows project-specific rules to be added, for example preventing duplicates within a given time window. The following example shows a repository that raises an Admin Notification for an upcoming certificate expiry warning, using constructor property promotion in line with PHP 8.4 conventions.
<?php
declare(strict_types=1);
namespace Mironsoft\NotificationHub\Model;
use Magento\AdminNotification\Model\InboxFactory;
use Magento\AdminNotification\Model\Inbox;
use Mironsoft\NotificationHub\Api\CertificateNotifierInterface;
/**
* Writes certificate expiry warnings into the Magento_AdminNotification inbox.
*/
class CertificateNotifier implements CertificateNotifierInterface
{
/**
* @param InboxFactory $inboxFactory Factory for the Inbox model used to persist notifications.
*/
public function __construct(
private readonly InboxFactory $inboxFactory
) {
}
/**
* Adds a MAJOR severity notification when a TLS certificate is about to expire.
*
* @param string $domain Domain name whose certificate is expiring.
* @param int $daysRemaining Number of days until certificate expiry.
* @return void
*/
public function notifyExpiringCertificate(string $domain, int $daysRemaining): void
{
/** @var Inbox $inbox */
$inbox = $this->inboxFactory->create();
$inbox->addNotice(
sprintf('SSL certificate expires in %d days', $daysRemaining),
sprintf(
'The TLS certificate for %s expires in %d days. Check renewal in the hosting panel.',
$domain,
$daysRemaining
),
sprintf('https://%s', $domain),
true
);
}
}
The addNotice() method on the Inbox model is a convenience wrapper that internally sets NOTICE_SEVERITY_MAJOR and creates and saves the row in a single step. Anyone who needs to control the severity explicitly, for example for a CRITICAL message on a failed payment gateway, sets severity directly via setSeverity(Inbox::NOTICE_SEVERITY_CRITICAL) before calling save(). Important for decoupling: the repository should sit behind its own interface in the Api namespace, so other modules can use the notification logic without a hard dependency on the concrete implementation. This is the same service contract idea that Magento consistently applies to the order repository or the customer repository.
4. A cron job as the trigger for notifications
Most meaningful Admin Notifications do not arise synchronously during a customer request but asynchronously through a cron job that periodically checks a condition. A certificate expiry, a low stock level, or a failed nightly API synchronization are classic candidates for a daily or hourly check. Registration happens through crontab.xml in the custom module, with a cron group that either targets the standard default group or a dedicated group if the check is resource intensive and should run in isolation.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="mironsoft_notificationhub_check_certificates"
instance="Mironsoft\NotificationHub\Cron\CheckCertificateExpiry"
method="execute">
<schedule>0 6 * * *</schedule>
</job>
<job name="mironsoft_notificationhub_check_stock"
instance="Mironsoft\NotificationHub\Cron\CheckLowStock"
method="execute">
<schedule>0 * * * *</schedule>
</job>
</group>
</config>
The cron job itself injects the repository or notifier interface shown above and calls it after checking the condition. What matters for a robust notification system is avoiding duplicates: a cron job that runs hourly should not write a new row into the inbox on every run as long as the state has not changed. In practice this is solved by checking whether a notification with the same notification_type value already exists within the last 24 hours before creating a new one. The resource model collection filter addFieldToFilter('notification_type', ['eq' => $type]) combined with a date filter on date_added prevents a flood of identical messages in the bell.
5. Registering system messages via di.xml
System messages differ fundamentally from inbox notifications: they are not stored in a table but re-evaluated on every backend request. An implementation of Magento\Framework\Notification\MessageInterface checks live in the isDisplayed() method whether a condition currently applies, for example whether the cache mode is set to developer mode or whether a required configuration value is missing. The getSeverity() method returns one of the constants MessageInterface::SEVERITY_MINOR, SEVERITY_MAJOR or SEVERITY_CRITICAL and determines whether the banner appears yellow or red.
<?php
declare(strict_types=1);
namespace Mironsoft\NotificationHub\Model\System\Message;
use Magento\Framework\Notification\MessageInterface;
use Magento\Framework\UrlInterface;
use Mironsoft\NotificationHub\Model\ApiSyncStatusCheckerInterface;
/**
* System message shown when the last supplier API synchronization failed.
*/
class ApiSyncFailure implements MessageInterface
{
/**
* @param ApiSyncStatusCheckerInterface $statusChecker Checks the persisted state of the last sync run.
* @param UrlInterface $urlBuilder Builds the admin URL for the sync monitor page.
*/
public function __construct(
private readonly ApiSyncStatusCheckerInterface $statusChecker,
private readonly UrlInterface $urlBuilder
) {
}
/**
* Returns a unique identifier for this system message.
*
* @return string
*/
public function getIdentity(): string
{
return 'mironsoft_notificationhub_api_sync_failure';
}
/**
* Determines whether the banner should currently be shown.
*
* @return bool
*/
public function isDisplayed(): bool
{
return $this->statusChecker->hasFailedSync();
}
/**
* Returns the banner text including a link to the monitor page.
*
* @return string
*/
public function getText(): string
{
$url = $this->urlBuilder->getUrl('mironsoft_notificationhub/sync/monitor');
return sprintf('The last API synchronization failed. <a href="%s">View details</a>', $url);
}
/**
* Returns the severity level controlling banner color.
*
* @return int
*/
public function getSeverity(): int
{
return MessageInterface::SEVERITY_MAJOR;
}
}
Registering this class does not happen directly, but through a virtual type in di.xml that hooks into the MessageList managed by Magento. This list iterates over all registered messages when the backend header renders and calls isDisplayed() on each one.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\Notification\MessageList">
<arguments>
<argument name="messages" xsi:type="array">
<item name="mironsoft_api_sync_failure" xsi:type="string">
Mironsoft\NotificationHub\Model\System\Message\ApiSyncFailure
</item>
</argument>
</arguments>
</type>
</config>
An important distinction from the inbox: system messages are re-evaluated on every request and hold no persistent state. That makes them ideal for conditions that can change quickly, such as whether an external service is currently reachable. For historical traceability, that is, who was informed of what state and when, the inbox is the right tool instead, because it produces a durable record with a timestamp.
6. System message vs. inbox notification: which mechanism when
The choice between a system message and an inbox notification largely depends on urgency and the desired visibility. A system message appears immediately and is visible to every administrator as a banner, regardless of whether the bell is ever opened. It suits conditions that require an immediate reaction and where being overlooked would be unacceptable, for example a missing required configuration value after a deployment. An inbox notification, on the other hand, is less intrusive: it merely increases the counter on the bell and only becomes visible once the bell is actively opened, making it better suited to information that should be acknowledged but does not justify an immediate interruption.
Another practical difference lies in historization. Since inbox entries live in a table, it is always possible to trace how many Admin Notifications of a given type occurred over a certain period, for example for internal reporting on system stability. System messages, by contrast, leave no trace once the underlying condition no longer applies, because they are never persisted. For audit requirements or compliance evidence, a combination is therefore often sensible: a system message for immediate visibility and, in parallel, an inbox entry for permanent documentation of the same event.
7. ACL control: who sees which notification
Both inbox notifications and system messages are rendered in the backend for every logged-in administrator by default, unless the display logic explicitly checks the permissions of the current user. For backend notifications that are only relevant to certain roles, for example stock alerts intended only for the purchasing role, the system message's isDisplayed() method additionally injects Magento\Framework\AuthorizationInterface and checks with isAllowed('Mironsoft_NotificationHub::stock_alerts') whether the currently logged-in admin user holds the corresponding ACL resource.
The ACL resource itself is declared, as with any backend menu entry, in acl.xml in the custom module and hooked in below Magento_Backend::admin. Fine-grained ACL filtering for inbox notifications is more involved, since the standard controllers of Magento_AdminNotification do not provide type-based permission checks out of the box. Anyone who needs that either extends the controller via a plugin and filters the collection before output, or consistently separates critical, role-specific messages into system messages, where the ACL check is straightforward to implement per class. In practice it has proven effective to use the inbox for widely distributed, cross-role information and system messages with an explicit ACL check for role-specific, urgent hints.
8. A ViewModel for custom notification UI
Anyone who wants to display custom Admin Notifications not only through the standard bell but also on a dedicated overview page in the backend, for example a dashboard widget showing the last ten critical messages, should consistently rely on a ViewModel rather than a block class carrying business logic. The ViewModel implements Magento\Framework\View\Element\Block\ArgumentInterface and is wired to the template via layout XML, which keeps the presentation cleanly separated from data retrieval.
<?php
declare(strict_types=1);
namespace Mironsoft\NotificationHub\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\AdminNotification\Model\ResourceModel\Inbox\CollectionFactory;
/**
* Provides recent critical inbox notifications to the dashboard widget template.
*/
class RecentCriticalNotifications implements ArgumentInterface
{
/**
* @param CollectionFactory $collectionFactory Factory for the Inbox collection.
*/
public function __construct(
private readonly CollectionFactory $collectionFactory
) {
}
/**
* Returns the ten most recent critical severity notifications.
*
* @return \Magento\AdminNotification\Model\Inbox[]
*/
public function getRecentCritical(): array
{
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('severity', ['eq' => 3])
->setOrder('date_added', 'DESC')
->setPageSize(10);
return $collection->getItems();
}
}
This separation pays off especially once the same data logic is needed in more than one place, for example additionally in a REST endpoint for an external monitoring dashboard. Since the ViewModel has no dependency on a block class, it can be reused in a service layer without changes. For ACL filtering inside the dashboard widget, the ViewModel additionally injects AuthorizationInterface, so only users with the matching permission see the corresponding Admin Notifications in the widget.
9. Notification channels compared
A custom notification system does not have to rely exclusively on Magento_AdminNotification. Depending on urgency and target audience, email notifications or Slack webhooks also come into play, triggered in parallel with the backend display. The following table compares the four most common channels for operational events in a Magento project.
| Channel | Visibility | Urgency | Persistence | Use case |
|---|---|---|---|---|
| System Message | Banner, immediately visible to everyone | High | None, evaluated live | Critical configuration errors, failed services |
| Inbox Notification | Bell icon, visible once opened | Medium | Permanently stored in a table | Expiry warnings, historized events |
| Email Alert | Inbox, independent of the backend | Medium to high | Persistent in the mail archive | Nightly error reports outside office hours |
| Slack Webhook | Team channel, in real time | High | Visible in the channel history | Immediate team notification on critical sync failures |
| Dashboard Widget | Backend homepage, actively viewed | Low to medium | Depends on the underlying source | Aggregated overview across multiple notification types |
In practice these channels complement each other. A critical Admin Notification about a failed payment sync should ideally appear simultaneously as a system message in the backend, be stored as an inbox entry for history, and additionally be reported to the responsible team via a Slack webhook. For purely informational hints, a single inbox entry is entirely sufficient, without burdening additional channels.
10. Summary
A custom notification system in Magento 2 does not need to reinvent the wheel. Magento_AdminNotification already provides, through the inbox table, the three severity levels and the controller scaffolding, everything needed for durable, historized Admin Notifications. Complemented by system messages for immediately visible banners and a cron job as the trigger, a complete notification system emerges that can be cleanly encapsulated in service contracts, repositories and ViewModels, instead of scattering business logic directly across cron classes or block files.
The choice between an inbox notification and a system message is not purely a matter of taste but depends directly on urgency and the desired level of historization. ACL rules ensure that role-specific backend notifications only reach the right administrators, while dismissible and persistent flags control whether a message stays permanently in the system or can be dismissed. Anyone who combines these building blocks consistently ends up with a notification system that fits seamlessly into the familiar Magento backend interface, instead of building a parallel UI that feels foreign to administrators.
Registering custom Admin Notifications, the essentials at a glance
Inbox Notification
InboxFactory and addNotice() write permanently into the adminnotification_inbox table. Ideal for historized messages.
System Message
MessageInterface plus di.xml registration in the MessageList for immediately visible banners without persistence.
Cron as a trigger
crontab.xml registers the periodic check, which avoids duplicates via notification_type and a time window.
ACL & ViewModel
AuthorizationInterface filters by role, ViewModels cleanly separate data retrieval from template rendering.
11. FAQ: Registering Custom Admin Notifications in the Backend
1What is the difference between Magento_AdminNotification and system messages?
2How do I programmatically raise a custom Admin Notification?
3What severity levels exist for inbox notifications?
4Do I need a custom db_schema.xml for a notification system?
5How do I register a custom system message?
6How do I trigger a notification via a cron job?
7How do I prevent duplicate notifications on every cron run?
8How do I control which administrators see a notification?
9What does dismissible mean for inbox notifications?
10Should I also use email or Slack in addition to the inbox?
Mironsoft
Magento 2 backend development and process automation
A custom notification system for your Magento backend?
We build individual Admin Notifications on top of Magento_AdminNotification, including cron triggers, system messages and clean ACL control for your roles and teams.
Custom Module Development
Custom notification modules with service contracts, repositories and clean di.xml configuration
Cron Monitoring
Automatically monitor certificate expirations, stock levels and API syncs and report them in the backend
Backend Integration
System messages, ACL rules and ViewModels for seamless integration into the admin panel