Action Scheduler Architecture
This document explains the core initialization flow and internal architecture of Action Scheduler (AS) based on the following source files:
action-scheduler.php(entry point)classes/abstracts/ActionScheduler.php(core class)classes/ActionScheduler_Versions.php(version management)classes/ActionScheduler_DataController.php(data layer control)
The goal is to help developers understand how AS bootstraps, how it chooses a single active version when multiple plugins bundle it, how the singleton accessors work, and how the autoloader maps class names to files.
1. Bootstrap / Initialization Flow
1.1 Entry point (action-scheduler.php)
The plugin entry point is responsible for registering the current AS version and initializing the latest registered version at the correct time.
Key steps in order:
-
Guard to avoid redefinition
- The file only runs if
action_scheduler_register_3_dot_9_dot_3()is not already defined and WordPress is loaded (i.e.,add_action()exists).
- The file only runs if
-
Load the version registry
- If the
ActionScheduler_Versionsclass does not yet exist, the file is required, and itsinitialize_latest_version()method is hooked onplugins_loadedwith priority 1. - This ensures that after all plugins have had a chance to register their AS version, the highest version wins and is initialized.
- If the
-
Register this version
- On
plugins_loadedwith priority 0, the entry point callsaction_scheduler_register_3_dot_9_dot_3(). - That function registers the version string (
3.9.3) with a callbackaction_scheduler_initialize_3_dot_9_dot_3()viaActionScheduler_Versions::instance()->register().
- On
-
Theme fallback (non-plugin use)
- If
plugins_loadedhas already fired and no plugin has loaded AS yet, the entry point initializes this version immediately, firesaction_scheduler_pre_theme_init, and then callsActionScheduler_Versions::initialize_latest_version(). - This allows AS to be bundled by themes while still participating in the “latest version wins” flow where possible.
- If
1.2 Version initialization callback
Each version callback (for example action_scheduler_initialize_3_dot_9_dot_3()) is responsible for:
- Safety check: only initialize if
ActionScheduleris not already defined. - Loading the core class:
require_once classes/abstracts/ActionScheduler.php. - Starting the system:
ActionScheduler::init( __FILE__ ).
This makes the entry point responsible only for version registration and delegating the actual setup to the core class.
1.3 Core ActionScheduler::init() flow
Once ActionScheduler::init() is called, the core bootstrapping happens in a single static method.
High-level flow:
-
Store plugin file path
self::$plugin_fileis set to the entry point file path.
-
Register the autoloader
spl_autoload_register( array( __CLASS__, 'autoload' ) );
-
Early hook
do_action( 'action_scheduler_pre_init' );
-
Load procedural API & data controller
require_once functions.php.ActionScheduler_DataController::init()sets up data-store selection and migration logic (details in Section 5).
-
Instantiate core singletons / services
store(),logger(),runner(),admin_view()return singleton instances.- A new
ActionScheduler_RecurringActionSchedulerinstance is created (not stored globally).
-
Hook service init methods
- If
inithas not run yet, AS attaches each service’sinit()method to the WordPressinithook (with controlled priorities). - If WordPress
initalready ran, AS directly calls each service’sinit()method immediately.
- If
-
Mark data store initialized and fire
action_scheduler_init- After the store is initialized, AS sets
self::$data_store_initialized = trueand firesdo_action( 'action_scheduler_init' ). - This action is the canonical signal that the procedural API is safe to use.
- After the store is initialized, AS sets
-
Optional deprecated functions
deprecated/functions.phpis loaded ifaction_scheduler_load_deprecated_functionsfilter returns true.
-
WP-CLI command registration
- If running under WP-CLI, AS registers commands and optional migration commands.
-
Post-migration cleanup
- If a DB logger is in use and migration is complete, the WP comment cleanup flow may be initialized.
2. Core Singleton Pattern (Factory, Store, Lock, Logger, Runner)
Action Scheduler centralizes access to its core services through static accessors on the ActionScheduler class.
2.1 ActionScheduler::factory()
- Returns the singleton instance of
ActionScheduler_ActionFactory. - Lazily instantiated; stored in
self::$factory.
2.2 ActionScheduler::store()
- Returns
ActionScheduler_Store::instance(). - The actual store class is filtered by
action_scheduler_store_class, controlled in part byActionScheduler_DataController.
2.3 ActionScheduler::lock()
- Returns
ActionScheduler_Lock::instance(). - Lock implementation is based on the active lock class (an abstract base defines the contract).
2.4 ActionScheduler::logger()
- Returns
ActionScheduler_Logger::instance(). - Logger class selection is also filterable;
ActionScheduler_DataControllersets defaults when migration is complete.
2.5 ActionScheduler::runner()
- Returns
ActionScheduler_QueueRunner::instance(). - Responsible for executing scheduled actions.
2.6 ActionScheduler::admin_view()
- Returns
ActionScheduler_AdminView::instance(). - Manages the admin UI.
2.7 Singleton enforcement
ActionScheduler itself is a static-only class:
__construct()is private.__clone()and__wakeup()trigger errors to prevent duplication.
This ensures there is a single global entry point for all core services.
3. Autoloader Logic
The autoloader is defined by ActionScheduler::autoload() and relies on naming conventions to map class names to directories.
3.1 Namespace handling
- If a class name contains a namespace separator (
), only classes in theAction_Schedulernamespace are handled. - The autoloader strips the namespace and loads by the short class name.
3.2 Directory resolution rules
The autoloader uses suffixes and prefixes to decide which directory to search:
-
Deprecated classes
- Class ends with
Deprecated→deprecated/directory.
- Class ends with
-
Abstract classes
- Determined by
is_class_abstract()list →classes/abstracts/.
- Determined by
-
Migration classes
- Determined by
is_class_migration()segment list →classes/migration/.
- Determined by
-
Schedules
- Class ends with
Schedule→classes/schedules/.
- Class ends with
-
Actions
- Class ends with
Action→classes/actions/.
- Class ends with
-
Schema
- Class ends with
Schema→classes/schema/.
- Class ends with
-
ActionScheduler core classes*
- Class begins with
ActionScheduler→classes/by default. - If the second underscore segment matches one of:
WPCLI→classes/WP_CLI/DBLogger,DBStore,HybridStore,wpPostStore,wpCommentLogger→classes/data-stores/
- Class begins with
-
CLI helper classes
- Determined by
is_class_cli()list →classes/WP_CLI/.
- Determined by
-
Bundled libraries
CronExpression*→lib/cron-expression/WP_Async_Request*→lib/
If the resolved file exists, it is included; otherwise the autoloader returns without error.
4. The action_scheduler_init Hook
The action_scheduler_init action is the official signal that Action Scheduler is fully initialized and the procedural API can be safely used.
4.1 When it fires
- It fires after the data store is initialized.
- If WordPress
inithas not yet fired:- AS hooks a closure to
initat priority 1. - That closure sets
self::$data_store_initialized = trueand firesaction_scheduler_init.
- AS hooks a closure to
- If WordPress
inithas already fired:- AS initializes services immediately and fires
action_scheduler_initright away.
- AS initializes services immediately and fires
4.2 Why it matters
ActionScheduler::is_initialized()will emit a _doing_it_wrong notice if the procedural API is used before this hook.- Core services (store, logger, runner) are not guaranteed to be available before it.
- Recurring action scheduling uses this hook (e.g.,
ActionScheduler_RecurringActionScheduleradds itself toaction_scheduler_init).
5. Version Competition: Multiple Plugins Bundling Action Scheduler
Action Scheduler is commonly bundled in multiple plugins. The design ensures that only one version initializes, and that it is the highest registered version.
5.1 How versions register
Each bundled copy registers its version on plugins_loaded priority 0:
ActionScheduler_Versions::instance()->register( $version, $callback )
The registry stores:
- A map of version strings → initialization callbacks.
- A map of source file paths → version strings (via
debug_backtrace()to record the registering file).
5.2 How the “winner” is chosen
On plugins_loaded priority 1, ActionScheduler_Versions::initialize_latest_version():
- Determines the latest version via
version_compareon all registered versions. - Calls the callback associated with the latest version.
This ensures that only the highest version initializes and all lower versions remain inert.
5.3 Theme fallback behavior
If AS is bundled by a theme, plugin hooks might already be done. In that case:
- The entry point initializes the version immediately.
- It then fires
action_scheduler_pre_theme_initand still callsActionScheduler_Versions::initialize_latest_version()to allow plugins to register their versions.
5.4 Practical implication for plugin authors
- Bundling AS is safe because version registration is deterministic.
- Only the highest registered version initializes, preventing multiple instances.
ActionScheduler_Versionskeeps source tracking that can be used (in newer versions) to determine which plugin/theme supplied the active copy.
6. Data Layer Control (Migration & Store Selection)
ActionScheduler_DataController is responsible for deciding which storage backend and handling migration between storage systems.
6.1 Core responsibilities
- Expose the canonical store class name (
ActionScheduler_DBStore) and logger class name (ActionScheduler_DBLogger). - Decide whether migration is complete or should run.
- Register store/logger filters when migration is complete.
- Hook
action_scheduler/progress_tickto free memory during long-running CLI jobs.
6.2 Migration state
- A site option
action_scheduler_migration_statustracks completion. is_migration_complete()checks if it equals'complete'.mark_migration_complete()andmark_migration_incomplete()manage the flag.
6.3 Initialization logic
During ActionScheduler_DataController::init():
-
If migration is complete:
- Adds filters to force the DB store and DB logger classes.
- Hooks
deactivate_pluginto reset migration state (to prevent reverting to a 2.x store when a plugin is deactivated).
-
If migration is not complete but dependencies are met:
- Starts the migration controller (
Action_SchedulerMigrationController::init()).
- Starts the migration controller (
-
Hooks
action_scheduler/progress_ticktomaybe_free_memory()for memory management during bulk processing.
6.4 Memory management for CLI
maybe_free_memory()frees memory after a configurable number of ticks.- It resets
$wpdb->queriesand clears caches onWP_Object_Cachewhere available. - Optional sleep time can be set for large operations.
7. Summary Flow Diagram (Textual)
plugins_loaded (priority 0)
└─ action_scheduler_register_X() → ActionScheduler_Versions::register()
plugins_loaded (priority 1)
└─ ActionScheduler_Versions::initialize_latest_version()
└─ action_scheduler_initialize_X()
└─ ActionScheduler::init( plugin file )
├─ register autoloader
├─ do_action('action_scheduler_pre_init')
├─ require functions.php
├─ ActionScheduler_DataController::init()
├─ instantiate store/logger/runner/admin/recurring scheduler
├─ init via WP `init` (or immediate if already fired)
└─ do_action('action_scheduler_init')
This is the canonical boot flow for Action Scheduler. Every other internal component depends on the guarantees established by this sequence.