Get notified when WordPress plugins need updating

Published Updated 3 min read

WordPress shows a badge in the admin toolbar when plugins need updating, but only to someone logged in and looking. Nothing emails you to say three plugins on this site are waiting. If you run one site and check the dashboard often, the badge is enough. If you look after client sites, or several of your own, relying on remembering to log in leaves gaps.

For managing many sites properly, MainWP gives you a central dashboard with update status across every connected site and bulk updates, and ManageWP is the hosted alternative if you would rather not run your own server.

This plugin fills a narrower gap: a digest of pending updates for one site, emailed whether or not you think to log in. No central server and no dashboard to maintain. For a freelancer with two or three client sites, that is often enough.

WordPress tells you after, never before

Core and plugin auto-updates have existed since WordPress 5.5, and when they run WordPress emails you about what was applied. What it never sends is the warning beforehand: updates are waiting and nothing has been applied yet.

That gap matters on exactly the sites where auto-updates are off. A staging workflow, a client contract that requires approval, or a plugin combination that has broken before are all reasons to update by hand, and once auto-updates are off, logging in is the only way to know anything is pending.

A digest only when something is waiting

Activation registers a cron event on the interval you choose, daily or weekly. When it fires, it asks the WordPress update API for fresh data on plugins, themes and core. If nothing is pending, nothing is sent.

If something is, a plain-text email lists each item with its current and available version, with the site name and update count in the subject so you can tell at a glance which site is writing and how much needs doing. The email links to wp-admin/update-core.php, where you apply them yourself.

Settings > Update Notifier holds the recipient (the admin email by default), the frequency, and checkboxes for plugins, themes and core. A Send test email now button confirms delivery before the first scheduled run.

One site, nothing applied, and cron timing

It knows about one site. Ten client sites means ten installs and ten separate digests, which works for a few and gets impractical beyond that. It reports what is pending and applies nothing; automated updates with rollback are a different class of tool.

WordPress cron runs when someone loads a page, so on a quiet site or a development install the digest can arrive late. WP Crontrol will show whether the event is firing, and on hosting that restricts PHP cron, a real server cron hitting wp-cron.php on a schedule makes delivery reliable.

MainWP and ManageWP for more than a few sites

Once you manage more than a few sites and want one view of all of them, MainWP is worth setting up. It is self-hosted on a separate WordPress install, free for the core dashboard, and connects to client sites through a companion plugin; its update view lists every site with pending updates and applies them in batch.

ManageWP is the same idea as a hosted service. There is no server to maintain, the free tier covers basic update monitoring for a handful of sites, and paid tiers add backups, performance monitoring and client reports.

Both do far more than this plugin, and if manual update rounds across many sites already eat real time, either pays back its setup quickly. This is for the stage before that.

What you get

Included

  • Weekly or daily email digest of pending plugin, theme, and core updates
  • Sends only when updates are pending, not a blank email on a fixed schedule
  • Settings for recipient email, frequency, and which update types to include
  • Test email button to verify delivery before waiting for the scheduled run

Not included

  • Multiple sites: the plugin runs independently on each site with no central aggregation
  • Applying updates: reports what is pending but does not run anything
  • Update changelogs or release notes: lists version numbers only
  • Filtering by update age: emails on the first pending update with no grace period setting
  • Rollback or staging: no mechanism to test updates before applying them

ManageWP does the things on the right. This build covers the part most sites use.

Questions

Does it conflict with auto-updates?

No. Updates that auto-update applies drop off the pending list, and the digest simply does not mention them. Both can run together.

Why not install WP Update Notifier from the directory?

If it does what you need, install it. It has been around since 2012 and does the job, though its settings are minimal and it has not been updated recently. Building your own makes sense if you want to extend it: a webhook instead of an email, a grace period before alerting, or folding it into a larger admin digest.

Can it email more than one address?

Not as built; wp_mail() is given a single recipient. Change the $to variable to a comma-separated string or an array to notify several.

Can it wait until a plugin has been out of date for a week?

Not as built. That needs the date each pending update was first seen: store an option per plugin when it first appears, and skip any first seen less than seven days ago.

Build it in Steem

The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.

Build this pluginAbout 55 credits. The free plan includes enough for one.
Read the prompt (550 words)

Build a WordPress plugin called WP Update Notifier. Plugin Name: WP Update Notifier. Version: 1.0.0. Description: Email a digest of pending plugin, theme, and core updates on a configurable schedule. File structure: one PHP file. At the top level of the file (outside the class), register activation and deactivation hooks: register_activation_hook(__FILE__, ['WP_Update_Notifier', 'activate']); register_deactivation_hook(__FILE__, ['WP_Update_Notifier', 'deactivate']); add_action('plugins_loaded', function() { (new WP_Update_Notifier())->init(); }); Class WP_Update_Notifier: public static activate(): if (!wp_next_scheduled('wp_un_digest')) { $opts = get_option('wp_update_notifier_options', []); $freq = !empty($opts['frequency']) ? $opts['frequency'] : 'weekly'; wp_schedule_event(time(), $freq, 'wp_un_digest'); } public static deactivate(): $ts = wp_next_scheduled('wp_un_digest'); if ($ts) wp_unschedule_event($ts, 'wp_un_digest'); public init(): add_filter('cron_schedules', [$this, 'add_weekly_schedule']); add_action('wp_un_digest', [$this, 'send_digest']); add_action('admin_menu', [$this, 'add_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_init', [$this, 'handle_test']); add_action('admin_init', [$this, 'maybe_reschedule']); add_weekly_schedule($schedules): if (!isset($schedules['weekly'])) $schedules['weekly'] = ['interval' => 604800, 'display' => 'Once Weekly']; return $schedules; maybe_reschedule(): $opts = get_option('wp_update_notifier_options', []); $freq = !empty($opts['frequency']) ? $opts['frequency'] : 'weekly'; $ts = wp_next_scheduled('wp_un_digest'); if (!$ts) { wp_schedule_event(time(), $freq, 'wp_un_digest'); } else { $event = wp_get_scheduled_event('wp_un_digest'); if ($event && $event->schedule !== $freq) { wp_unschedule_event($ts, 'wp_un_digest'); wp_schedule_event(time(), $freq, 'wp_un_digest'); } } add_menu(): add_options_page('Update Notifier', 'Update Notifier', 'manage_options', 'wp-update-notifier', [$this, 'render_page']); register_settings(): register_setting('wp_update_notifier', 'wp_update_notifier_options', ['sanitize_callback' => [$this, 'sanitize_options']]). sanitize_options($input): return ['email' => sanitize_email($input['email'] ?? ''), 'frequency' => in_array($input['frequency'] ?? '', ['daily', 'weekly']) ? $input['frequency'] : 'weekly', 'include' => array_values(array_intersect((array)($input['include'] ?? []), ['plugins', 'themes', 'core']))]; render_page(): $defaults = ['email' => get_option('admin_email'), 'frequency' => 'weekly', 'include' => ['plugins', 'themes', 'core']]; $opts = wp_parse_args(get_option('wp_update_notifier_options', []), $defaults); Output a div.wrap with h1 'Update Notifier'. If $_GET['sent'] is set show: '<div class="notice notice-success"><p>Test email sent.</p></div>'. Form 1: method post, settings_fields('wp_update_notifier'), then a table.form-table with rows: Email (input[type=email name=wp_update_notifier_options[email] value=esc_attr($opts['email']) class=regular-text]), Frequency (select[name=wp_update_notifier_options[frequency]] with options value=weekly 'Weekly' and value=daily 'Daily', selected= matching $opts['frequency']), Notify about (three checkboxes each with name=wp_update_notifier_options[include][] and values plugins/themes/core, checked if value is in $opts['include'], labelled 'Plugins' / 'Themes' / 'WordPress core'). submit_button('Save Settings'). Form 2: method post, wp_nonce_field('wp_un_test'), hidden input name=wp_un_send_test value=1, submit_button('Send test email now', 'secondary'). handle_test(): if (empty($_POST['wp_un_send_test'])) return; check_admin_referer('wp_un_test'); $this->send_digest(); wp_redirect(add_query_arg(['page' => 'wp-update-notifier', 'sent' => 1], admin_url('options-general.php'))); exit; send_digest(): Force a fresh check: wp_update_plugins(); wp_update_themes(); wp_version_check(); $opts = get_option('wp_update_notifier_options', []); $include = !empty($opts['include']) ? (array)$opts['include'] : ['plugins', 'themes', 'core']; $to = sanitize_email(!empty($opts['email']) ? $opts['email'] : get_option('admin_email')); $lines = []; Plugins: if (in_array('plugins', $include)) { $data = get_site_transient('update_plugins'); foreach ((array)($data->response ?? []) as $file => $info) { $pd = get_plugin_data(WP_PLUGIN_DIR . '/' . $file, false, false); $lines[] = 'Plugin: ' . ($pd['Name'] ?: $file) . ' ' . ($pd['Version'] ?: '?') . ' -> ' . ($info->new_version ?? '?'); } } Themes: if (in_array('themes', $include)) { $data = get_site_transient('update_themes'); foreach ((array)($data->response ?? []) as $slug => $info) { $t = wp_get_theme($slug); $lines[] = 'Theme: ' . $t->get('Name') . ' ' . $t->get('Version') . ' -> ' . ($info['new_version'] ?? '?'); } } Core: if (in_array('core', $include)) { $updates = get_core_updates(); if (!empty($updates) && isset($updates[0]->response) && $updates[0]->response === 'upgrade') { global $wp_version; $lines[] = 'WordPress: ' . $wp_version . ' -> ' . ($updates[0]->version ?? '?'); } } if (empty($lines)) return; $site = get_bloginfo('name'); $count = count($lines); $subject = '[' . $site . '] ' . $count . ' update' . ($count === 1 ? '' : 's') . ' pending'; $body = 'Updates pending on ' . get_bloginfo('url') . ":\n\n" . implode("\n", $lines) . "\n\nManage updates: " . admin_url('update-core.php'); wp_mail($to, $subject, $body); No custom database table, no JavaScript, no front-end output.