Stage system reference (developer)
The SSI import pipeline is built as a registry of named stages with priorities. The user-facing how it works article covers the conceptual stages; this article is the developer reference for the registration API, filter chain, and parallel-vs-sequential split.
Audience: add-on authors, integrators, and anyone building a custom template.
Table of Contents
Registering a stage
Inside a template class (extending SSI_ImportTemplate, SSI_PostTemplate, etc.), override registerTemplateStages():
class My_Custom_Template extends SSI_PostTemplate {
protected function registerTemplateStages() {
parent::registerTemplateStages(); // keep the inherited stages
$this->registerStage(
'my-custom-stage', // name (unique across all stages)
85, // priority (lower = earlier)
[$this, 'stage_my_custom_stage'], // callable
'Description shown in the admin UI'
);
}
public function stage_my_custom_stage() {
// your stage logic — runs in $this context
// $this->table_names['batch'] is the per-import batch table
// $this->config holds the parsed import config
}
}
registerStage() is a method on the abstract template class (import-template-abstract.php). Add-on plugins (without a custom template) can register via the ssi_register_stages action — pass the template instance to registerStage():
add_action('ssi_register_stages', function($template) {
$template->registerStage(
'my-addon-stage',
92,
function() use ($template) { /* ... */ },
'My add-on stage'
);
});
Use registerStage (the public alias since 2.40+) when registering from outside the template class — it’s identical to registerStage() internally but accessible from add-on code.
Stage priorities (priority bands)
Lower priority runs first. The convention:
| Band | Phase | Examples |
|---|---|---|
| 10 | Load | load-csv |
| 20 | Taxonomies | import-taxonomies |
| 30 | Match | match-existing |
| 40-49 | Update existing | update-posts, update-postmeta |
| 50-59 | Insert new | insert-posts, insert-postmeta |
| 60-69 | Relationships | upsert-relationships |
| 70-89 | Images | attach-existing-images, upload-remote-images, attach-gallery-images |
| 90-95 | Post-processing | fix-attributes, process-deletes |
| 999 | Cleanup (proposed) | post-import-cleanup |
You’re free to pick any integer for your stage. Stick to the bands where possible so it slots into the natural execution order. If your stage needs to run after upsert-relationships but before image stages, use a priority around 65.
The ssi_filter_stages_to_run filter
Every run filters the registered stage list through ssi_filter_stages_to_run before dispatching. The filter signature:
apply_filters('ssi_filter_stages_to_run', $stages_assoc, $config, $template);
$stages_assoc is an associative array keyed by stage name. Return a filtered associative array (typically by unset()‘ing keys you want to skip, or building a new array with only the keys you want).
Priority is the standard WP filter priority and controls who-wins-when-multiple-filters-compose.
Built-in filter registrations (priorities)
| Source | Priority | Effect |
|---|---|---|
| Saved import-mode preset | 5 | Restricts to the preset’s allowed stages |
CLI comma-separated list (wp ssi 5 a,b,c) |
10 | Restricts to listed stages |
--delete-all flag |
99 | Forces only process-deletes |
This three-tier precedence lets you build presets, override per-run via CLI, and have --delete-all always trump everything.
Adding your own filter
To add an extra preset:
add_filter('ssi_filter_stages_to_run', function($stages, $config, $template) {
if (!empty($template->additional_options['import_mode'])
&& $template->additional_options['import_mode'] === 'my_custom_mode') {
return array_intersect_key($stages, array_flip([
'load-csv', 'match-existing', 'my-custom-stage',
]));
}
return $stages;
}, 4, 3); // priority 4 — runs BEFORE the core preset filter at 5
To compose on top of an existing preset:
add_filter('ssi_filter_stages_to_run', function($stages, $config, $template) {
// Remove image stages unconditionally
foreach (['attach-existing-images', 'upload-remote-images', 'attach-gallery-images'] as $img_stage) {
unset($stages[$img_stage]);
}
return $stages;
}, 11, 3); // priority 11 — runs AFTER the CLI filter at 10
Parallel vs sequential stages
Most stages run sequentially. Only these are marked parallel:
$parallel_stages = [
'load-csv' => 'csv',
'attach-existing-images' => 'batch',
'upload-remote-images' => 'batch',
'fix-attributes' => 'batch',
];
The value ('csv' or 'batch') tells the orchestrator how to segment the work:
'csv'— split the CSV file by byte offset and assign each segment to a worker.'batch'— split the batch table bypost_idrange.
To make your custom stage parallel, register it as parallel via the orchestrator metadata. This is done by overriding getStagesWithParallelInfo() in your template, or by hooking the post-registration loop in super-speedy-imports.php (the latter requires reading the orchestrator source). Most add-ons stay sequential.
Don’t parallelise a stage that writes to global state without coordination. The classic violation is wp_insert_term — concurrent workers race on the wp_terms / wp_term_taxonomy UNIQUE keys. As of 2.55.8 SSI recovers from that race (it retries via term_exists() instead of aborting on the duplicate-key error), but it still serialises import-taxonomies by policy — both because the race is cheaper to avoid than to recover from, and because parallelism measured only ~7% faster at 100k.
Add-on authors, take note:
ssi_perf_effective_workers($stage_name, $default)infunctions/perf-options.phphard-returns 1 forimport-taxonomiesbefore consulting any stage registration or saved worker config. So even if you mark a taxonomy-writing stage parallel, or copy the parallel-stage pattern forimport-taxonomiesitself, that short-circuit forces it back to a single worker — and the Performance Options Workers dropdown for it is silently ignored.
Hooks that fire during a run
do_action('ssi_register_stages', $template); // when stages get registered
do_action('ssi_template_init', $template); // right after stage registration
do_action('ssi_before_import_stages', $template); // just before the stage loop starts
// (stage callbacks fire here, one at a time)
do_action('ssi_after_import_stages', $template); // just after the stage loop ends
Use ssi_register_stages to add or remove stages. Use ssi_template_init to set up state that all your custom stages need. Use the before/after hooks to wrap the whole pipeline.
Logging from inside a stage
Every template has a $this->logger (instance of ImportLogger). The key methods:
$this->logger->logStageStart($stage_name); // already called by the orchestrator
$this->logger->logStageMetric($stage, $key, $value); // numeric stat shown in the summary
$this->logger->logStageDetail($stage, $msg); // free-form note attached to the stage row
$this->logger->updateSummaryMetrics($array); // top-level summary stat
$this->logger->logStageEnd($stage_name); // already called by the orchestrator
logStageMetric('upsert-relationships', 'product_cat_created', 850000) appears in the CLI summary table AND persists to wp_ssi_import_history_stages.metrics so you can query it post-run.
Re-running a single stage
The orchestrator supports wp ssi <id> <stage-name> for any registered stage. Your custom stage needs to be IDEMPOTENT for this to work — re-running it should produce the same end state whether it’s the first run or the 50th.
Pattern: do all destructive work (DELETE) before the constructive work (INSERT). For relationship-style stages:
-- Delete relationships this stage might have created
DELETE tr FROM wp_term_relationships tr
INNER JOIN wp_super_speedy_imports_batch_<id> it ON tr.object_id = it.post_id
INNER JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
WHERE tt.taxonomy = 'my-taxonomy';
-- Recreate them from scratch
INSERT INTO wp_term_relationships (object_id, term_taxonomy_id)
SELECT DISTINCT it.post_id, tt.term_taxonomy_id
FROM wp_super_speedy_imports_batch_<id> it
INNER JOIN wp_terms t ON ...
INNER JOIN wp_term_taxonomy tt ON ...;
A failing re-run after an aborted earlier run leaves the table in the expected end state, not a half-state.
Per-import staging tables
Every stage has access to these via $this->table_names:
| Key | Table | Purpose |
|---|---|---|
batch |
wp_super_speedy_imports_batch_<id> |
One row per CSV row, all mapped fields plus computed columns |
terms |
wp_super_speedy_imports_terms_<id> |
Flat-taxonomy terms found in the CSV |
hierarchies |
wp_super_speedy_imports_hierarchies_<id> |
Hierarchical-taxonomy paths found in the CSV |
gallery_images |
wp_super_speedy_imports_gallery_images_<id> |
Gallery URLs (WC only) |
deletes |
wp_super_speedy_imports_deletes_<id> |
IDs flagged for deletion in process-deletes |
duplicate_unique_ids |
wp_ssi_duplicate_unique_ids_<id> |
Rows whose UID got -N suffix added |
validation_errors |
wp_ssi_validation_errors_<id> |
Per-row errors (blank UIDs, etc.) |
These survive between runs on purpose. load-csv drops and recreates them at the start of every full run. Your stage can read them but should treat write access as load-csv’s domain — don’t add or modify rows from a later stage unless you understand what downstream stages expect.
Useful helpers
ssi_query($sql, $params); // wrapper around $wpdb->query — auto-dispatches to wpdb or mysqli
ssi_get_results($sql, $params); // wrapper around $wpdb->get_results
ssi_get_var($sql, $params); // wrapper around $wpdb->get_var
ssi_insert($table, $data, $format,
$on_dup, $silent, $ignore); // wrapper around $wpdb->insert with INSERT IGNORE option
ssi_is_safe_identifier($name); // SQL hardening — true if $name is safe to interpolate as a backtick identifier
ssi_perf_get($stage, $key, $default); // read per-stage performance options (workers, query_limit, etc.)
ssi_report_import_error($msg, $fatal); // log an error; if $fatal=true, abort the import
ssi_query is preferred over direct $wpdb->query() because it routes through whichever DB connection the import is currently using (wpdb for sequential stages, raw mysqli for parallel-worker stages — both for memory-leak avoidance).
Cross-references
- How imports work — the stage pipeline (user-facing) — the same content from the user’s perspective.
- Creating custom templates — for setting up the template class that registers stages.
- Creating add-ons — for stage registration from a plugin without a custom template.
- Stage selection presets — for the user-facing layer that filters this registry.