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
Guard to avoid redefinition
- 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__ ).
Load the version registry
1.3 Core ActionScheduler::init() flow
Register this version
Theme fallback (non-plugin use)
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)
Guard to avoid redefinition
2.1 ActionScheduler::factory()
- 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).
2.2 ActionScheduler::store()
- 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.
2.3 ActionScheduler::lock()
- 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().
2.4 ActionScheduler::logger()
- 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.
2.5 ActionScheduler::runner()
self::$plugin_fileis set to the entry point file path.
2.6 ActionScheduler::admin_view()
spl_autoload_register( array( __CLASS__, 'autoload' ) );
2.7 Singleton enforcement
Load the version registry
do_action( 'action_scheduler_pre_init' );
Register this version
3. Autoloader Logic
Theme fallback (non-plugin use)
3.1 Namespace handling
require_once functions.php.ActionScheduler_DataController::init()sets up data-store selection and migration logic (details in Section 5).
3.2 Directory resolution rules
Store plugin file path
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.
- 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/
Register the autoloader
4. The action_scheduler_init Hook
Early hook
4.1 When it fires
store(),logger(),runner(),admin_view()return singleton instances.- A new
ActionScheduler_RecurringActionSchedulerinstance is created (not stored globally).
4.2 Why it matters
- 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.
5. Version Competition: Multiple Plugins Bundling Action Scheduler
Load procedural API & data controller
5.1 How versions register
Instantiate core singletons / services
- 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.
Hook service init methods
deprecated/functions.phpis loaded ifaction_scheduler_load_deprecated_functionsfilter returns true.
5.2 How the “winner” is chosen
Mark data store initialized and fire action_scheduler_init
- Determines the latest version via
version_compareon all registered versions. - Calls the callback associated with the latest version.
Optional deprecated functions
5.3 Theme fallback behavior
WP-CLI command registration
- If running under WP-CLI, AS registers commands and optional migration commands.
5.4 Practical implication for plugin authors
- If a DB logger is in use and migration is complete, the WP comment cleanup flow may be initialized.
6. Data Layer Control (Migration & Store Selection)
Post-migration cleanup
6.1 Core responsibilities
- 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).
6.2 Migration state
- 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.
6.3 Initialization logic
Each version callback (for example action_scheduler_initialize_3_dot_9_dot_3()) is responsible for:
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
- 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().
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 makes the entry point responsible only for version registration and delegating the actual setup to the core class.