Run your own code when an import completes
SSI fires two WordPress actions when imports finish, so you can bolt on whatever you like: Slack messages, webhooks to a downstream system, cache warming, kicking off a second dependent import. This article covers both hooks, what they pass you, and a worked example you can paste in.
If all you want is an email, you don’t need code at all: the Schedule tab → Email alerts panel has built-in per-import completion emails and a daily morning summary (added in 2.72). The hooks are for everything beyond that.
The two hooks
ssi_import_run_complete: every completed run
Fires exactly once for every run that completes, no matter how it was started: the Preview & Run button in the browser, wp ssi <id> on the CLI, or a schedule. If you want "tell me whenever anything finishes", this is the hook.
do_action( 'ssi_import_run_complete', $import_id, $history_id, $summary );
| Parameter | Type | What it is |
|---|---|---|
$import_id |
int | The import’s id, the number you see in wp ssi list and pass to wp ssi <id>. |
$history_id |
int | The id of this run’s row in the wp_ssi_import_history table. Use it to look up the full stage-by-stage record of the run. |
$summary |
array | The run’s counters. The keys you’ll actually use: rows_processed, posts_inserted, posts_updated, plus per-stage detail counters. Always check with isset(): a narrowed run (for example the Update prices/stock only mode) skips stages, so it only reports the counters for stages that ran. |
Note it fires on completion: a run that dies mid-way (fatal, out of memory, server restart) never reaches it. For "did my import NOT run?" monitoring, use the built-in daily summary email, which explicitly reports when nothing ran.
ssi_scheduled_import_complete: scheduled runs only
Fires when a scheduled run completes, right after SSI has updated the schedule’s bookkeeping (last run, next run). Manual browser runs and CLI runs do not fire this one. Use it when your code cares about the schedule itself: cadence, next run time, whether it’s still enabled.
do_action( 'ssi_scheduled_import_complete', $import_id, $schedule );
| Parameter | Type | What it is |
|---|---|---|
$import_id |
int | The import’s id, same as above. |
$schedule |
array | The schedule row. Keys: id, import_id, cadence (once / hourly / daily / weekly), enabled, run_at, run_days, timezone, next_run, last_run, last_result, status, history_id (the history row of the run that just finished). |
A scheduled run fires both hooks: ssi_import_run_complete first (when the run’s history row is finalised), then ssi_scheduled_import_complete (when the schedule bookkeeping is done).
Where to put the code
A must-use plugin (mu-plugin) is the right home: it loads on every request including WP-Cron and CLI, it survives theme switches, and it can’t be deactivated by accident. Create the file wp-content/mu-plugins/ssi-completion-hooks.php (make the mu-plugins folder if it doesn’t exist) and it’s live immediately, as mu-plugins don’t need activating.
A theme’s functions.php also works, but remember scheduled runs execute under WP-Cron with your active theme: if you switch themes, your hook silently disappears. That’s the footgun the mu-plugin avoids.
Worked example: notify a webhook when the nightly product feed lands
Say import 7 is your nightly supplier feed, scheduled daily at 03:00, and you want your stock dashboard pinged when it’s done, with the counts, and a warning if the run looks suspiciously small.
<?php
/**
* Plugin Name: SSI completion notifications
* Description: Pings the stock dashboard when the nightly feed (import 7) completes.
*/
add_action( 'ssi_scheduled_import_complete', function ( $import_id, $schedule ) {
// Only care about the nightly feed.
if ( 7 !== (int) $import_id ) {
return;
}
// Pull the counters for THIS run via the history row the schedule hands us.
global $wpdb;
$history = $wpdb->get_row( $wpdb->prepare(
"SELECT summary_data, duration FROM {$wpdb->prefix}ssi_import_history WHERE id = %d",
(int) $schedule['history_id']
) );
$summary = $history ? json_decode( $history->summary_data, true ) : array();
$rows = isset( $summary['rows_processed'] ) ? (int) $summary['rows_processed'] : 0;
$created = isset( $summary['posts_inserted'] ) ? (int) $summary['posts_inserted'] : 0;
$updated = isset( $summary['posts_updated'] ) ? (int) $summary['posts_updated'] : 0;
wp_remote_post( 'https://dashboard.example.com/hooks/feed-complete', array(
'timeout' => 10,
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array(
'import_id' => $import_id,
'rows' => $rows,
'created' => $created,
'updated' => $updated,
'duration' => $history ? round( (float) $history->duration, 1 ) : null,
'next_run' => $schedule['next_run'],
// A normal feed is ~40k rows; flag a tiny one so someone looks at it.
'suspicious' => $rows < 1000,
) ),
) );
}, 10, 2 );
Three things worth copying from this example:
- The
10, 2at the end matters.add_actiondefaults to passing your callback ONE argument; without the, 2you’d never receive$scheduleand$schedule['history_id']would fatal. Same deal with, 3forssi_import_run_complete. $schedule['history_id']is the bridge to the run’s detail. The schedule row itself only carries bookkeeping; join towp_ssi_import_historyfor counters, and towp_ssi_import_history_stagesif you want per-stage timings.- Sanity-check the counters. A supplier feed that normally moves 40k rows and suddenly moves 12 is a truncated CSV, not a quiet night. Completion is not the same as correctness: cheap threshold checks like
$rows < 1000catch real incidents.
Variant: the same thing for ANY run, not just scheduled
Hooking ssi_import_run_complete is simpler because the counters are handed to you directly, with no history lookup needed:
add_action( 'ssi_import_run_complete', function ( $import_id, $history_id, $summary ) {
if ( 7 !== (int) $import_id ) {
return;
}
$rows = isset( $summary['rows_processed'] ) ? (int) $summary['rows_processed'] : 0;
error_log( sprintf( 'SSI import %d finished (history %d): %d rows.', $import_id, $history_id, $rows ) );
}, 10, 3 );
This fires for manual test runs from the Config screen too, which is handy while you’re developing the listener, since you don’t have to wait for the schedule to tick. Run the import from the browser (or wp ssi 7), then check wp-content/debug.log.
Testing your listener
- Drop the mu-plugin in place.
- Point it at a small test import id and run that import from the Config screen.
- For the scheduled hook specifically: set the import’s schedule to
oncea couple of minutes out, then either wait for WP-Cron or force the due-schedule scan withwp cron event run ssi_scheduled_scan_due(or just load any front-end page to trigger wp-cron). - Check your endpoint or
debug.log. If nothing arrived, the usual suspect is the missing argument count (, 2/, 3).

Comments