A8CSP Background Jobs Engine WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
WordPress developer framework for running long-running, chunked background tasks via pluggable scheduler adapters (Action Scheduler or WP-Cron), with a work queue and task-run runner.
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/a8cteam51/a8csp-background-jobs-engine/releases/download/v1.0.0/a8csp-background-jobs-engine.zipA background-work engine for WordPress sites: Jobs, Schedules, and Chunked Jobs using Action Scheduler when available, with a documented best-effort WP-Cron fallback.
What it is
A Job is one named unit of background work. A JobDefinition composes its name, kind, execution object, and policy declaration. Standard execution objects implement JobExecutionInterface; Chunked Job execution objects implement the standalone ChunkedJobExecutionInterface role to split work into independently processed chunks. JobDefinition::closure() provides a closure-backed standard Job, with an optional JobOptions declaration. A Schedule dispatches registered work on a fixed recurrence. Every piece of work belongs to a scope (your plugin slug); the engine composes {scope}:{name} into one identity so plugins using distinct scopes do not collide. A scope is a single-writer partition: exactly one plugin declares the schedules for a given scope, and a sync call for that scope is authoritative over every registration inside it.
Consumers use four connected surfaces:
- The Engine handle —
a8csp_bgje( $scope )returns a scope-boundEnginewithjobs(),schedules(), andruns()capability managers. - The public models and execution roles — compose work with
JobDefinition,JobKind, andJobOptions; implementJobExecutionInterfaceorChunkedJobExecutionInterface; declare schedules withSchedule,Recurrence, andCatchUpPolicy; callbacks depend onRunContextInterfaceorChunkedRunContextInterface; run-producing commands returnRunsnapshots, terminal failures useRunFailure, and verb failures returnWP_Error. - The procedural aliases — twelve verb-noun
a8csp_bgje_*()functions take$scopefirst, accept the sameJobDefinitionregistration value as the Jobs manager and the same variadicSchedulevalues as the Schedules manager, and invoke the capability-manager verbs. - The lifecycle hooks — observe runs through the
a8csp_bgje/*actions.
The data boundary is deliberate: capability managers accept typed definition, policy, and schedule values, and payloads the engine hands to consumer code are typed objects such as Run, RunContext, and RunFailure; execution callbacks depend on RunContextInterface or ChunkedRunContextInterface. The procedural aliases accept the same typed definition and schedule values as their capability-manager counterparts.
Delivery uses Action Scheduler when it is ready and falls back to WP-Cron otherwise. At-least-once delivery is guaranteed only under Action Scheduler; WP-Cron is best-effort. An occurrence on a temporarily unavailable backend is dormant, not lost.
The engine's state persists in non-autoloaded wp_options rows under the reserved a8csp_bgje_ prefix, so it adds no weight to ordinary page loads.
Installation
The canonical install is the plugin ZIP attached to a GitHub Release. Download it, upload it as a WordPress plugin, and activate it. The release ZIP bundles production Composer dependencies and the translation template, so it needs no Composer step. Installed copies receive release updates through the dashboard like any plugin.
For a source checkout, clone into wp-content/plugins/a8csp-background-jobs-engine and install production dependencies:
cd wp-content/plugins
git clone https://github.com/a8cteam51/a8csp-background-jobs-engine.git a8csp-background-jobs-engine
cd a8csp-background-jobs-engine
composer install --no-dev
Action Scheduler is optional and preferred when ready; when absent, the engine runs on WP-Cron alone. Action Scheduler 4.1.0 is the supported floor. Chunked Jobs schedule each successor as a unique action that differs from the running one only in its sequence argument, and Action Scheduler made unique scheduling args-aware in 4.0.0; that is the functional requirement. The floor sits above it because 4.1.0 hardened deserialization of stored schedule data, and the engine declines to drive an elected copy below that. An initialized copy below 4.1.0 is treated as unusable rather than trusted: the engine runs on WP-Cron and flags the elected copy as unsupported in its scheduling diagnostics. Action Scheduler elects the highest version among every bundled copy on the site, so the deciding version is not necessarily the one shipped beside any single plugin.
Depending on the engine
The engine is a separate plugin, and a8csp_bgje() is a plain function: if the engine is deactivated, every consumer call is a fatal error on init. Declare the dependency in your plugin header so WordPress enforces it:
/**
* Plugin Name: My Plugin
* Requires Plugins: a8csp-background-jobs-engine
*/
WordPress refuses to activate your plugin without the engine and blocks deactivating the engine while you depend on it. If the dependency is genuinely optional, guard instead of declaring it:
if ( function_exists( 'a8csp_bgje' ) ) {
a8csp_bgje( 'my-plugin' )->jobs()->register( $definition );
}
Never define a8csp_bgje() yourself as a fallback; a second definition of the same name is a fatal error the moment both plugins load.
When to call the engine
a8csp_bgje( $scope ) constructs a handle lazily and infallibly. Capability-manager verb calls belong on the WordPress init hook or later. Invalid scopes and unavailable engine state return WP_Error from the first verb instead of failing handle construction. The request that activates the engine stays dormant until the next request.
Register work and synchronize schedules from init on every request: registration is per-request, and schedule synchronization treats the supplied schedules as the scope's complete declaration. A refusal there is returned, not logged, and init is rarely a place to act on one; schedules()->inspect() answers later, on a request that can, whether the declaration is actually in force. Prefer init priority 2 or later so Action Scheduler's init:1 store initialization has run; synchronizing before it routes that request's occurrences to WP-Cron.
Register unconditionally. A delivery that arrives on a request where its definition was never registered fails that run terminally with unknown_job and consumes no automatic attempt, because the engine cannot distinguish work you removed from work you forgot to declare. Guarding registration behind is_admin(), a page check, or a capability check is the usual way this happens, and it fails intermittently rather than consistently: background deliveries run in whichever request context the backend chooses, including admin-ajax.
Pass your plugin's lowercase slug as the scope (matching [a-z0-9][a-z0-9-]*, at most 32 bytes, never starting with the reserved a8csp-bgje prefix). Job, Chunked Job, and Schedule names match [a-z0-9_-]+ and are at most 64 bytes.
Quick start
use A8C\SpecialProjects\BackgroundJobsEngine\JobDefinition;
use A8C\SpecialProjects\BackgroundJobsEngine\JobExecutionInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\RunContextInterface;
final class RefreshCacheExecution implements JobExecutionInterface {
public const string NAME = 'refresh-cache';
public function handle( array $start_args, RunContextInterface $context ): void {
my_plugin_refresh_cache(
(int) ( $start_args['site_id'] ?? 0 ),
(string) $context->get_run_id()
);
}
}
add_action( 'init', static function (): void {
$bg = a8csp_bgje( 'my-plugin' );
$registered = $bg->jobs()->register(
JobDefinition::job( RefreshCacheExecution::NAME, new RefreshCacheExecution() )
);
if ( is_wp_error( $registered ) ) {
error_log( $registered->get_error_message() );
}
}, 2 );
function my_plugin_queue_cache_refresh( int $site_id ): void {
$bg = a8csp_bgje( 'my-plugin' );
$run = $bg->jobs()->dispatch( RefreshCacheExecution::NAME, array( 'site_id' => $site_id ) );
if ( is_wp_error( $run ) ) {
error_log( $run->get_error_message() );
return;
}
update_option( 'my_plugin_last_cache_run', (string) $run->id );
}
The execution class's public NAME constant co-locates its stable scope-local name with its behavior. Definitions, imperative dispatches, and schedules reference the same constant.
Each successful dispatch returns an immutable Run snapshot with identity, id (a RunId value), and status. Automatic attempts for one admitted run keep the same run ID. Every capability-manager verb returns its success value or WP_Error for an expected validation, registration, readiness, or engine failure. Errors have a stable string code and an engine-authored message, and may carry redaction-safe structured context.
Examples
Five complete, copy-paste-adaptable scenarios follow. Each uses the scope slug my-plugin.
1. A recurring schedule, full lifecycle
Define the job and synchronize the scope's complete schedule declaration on every init.
use A8C\SpecialProjects\BackgroundJobsEngine\CatchUpPolicy;
use A8C\SpecialProjects\BackgroundJobsEngine\JobDefinition;
use A8C\SpecialProjects\BackgroundJobsEngine\JobExecutionInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\Recurrence;
use A8C\SpecialProjects\BackgroundJobsEngine\RunContextInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\Schedule;
final class PruneTransientsExecution implements JobExecutionInterface {
public const string NAME = 'prune-transients';
public function handle( array $start_args, RunContextInterface $context ): void {
my_plugin_prune_expired_transients();
}
}
add_action( 'init', static function (): void {
$bg = a8csp_bgje( 'my-plugin' );
$registered = $bg->jobs()->register(
JobDefinition::job( PruneTransientsExecution::NAME, new PruneTransientsExecution() )
);
if ( is_wp_error( $registered ) ) {
error_log( $registered->get_error_message() );
return;
}
$synced = $bg->schedules()->sync(
new Schedule(
'nightly-prune',
Recurrence::every( DAY_IN_SECONDS ),
PruneTransientsExecution::NAME,
catch_up: CatchUpPolicy::RunOnce,
),
);
if ( is_wp_error( $synced ) ) {
error_log( $synced->get_error_message() );
}
}, 2 );
register_deactivation_hook( __FILE__, static function (): void {
$removed = a8csp_bgje_sync_schedules( 'my-plugin' );
if ( is_wp_error( $removed ) ) {
error_log( $removed->get_error_message() );
}
} );
An unanchored schedule first runs one interval after synchronization. An anchored schedule uses the first strictly future point on its UTC phase grid. There is no first-run timestamp field. Omitting a schedule from the next complete declaration removes it; synchronizing with no schedules removes all schedules for the scope without cancelling admitted runs. Under WP-Cron, removal reaches events in WordPress's own cron option; a replacement cron store installed through the pre_* cron filters keeps its own events, and the engine reports removal as complete without seeing them. If the plugin is inactive, wp a8csp-bgje schedules remove my-plugin --yes performs the same schedule convergence.
2. A one-shot callable, dispatched asynchronously
JobDefinition::closure() is the class-free authoring form. Its handler receives the invocation arguments and a typed RunContextInterface.
use A8C\SpecialProjects\BackgroundJobsEngine\ErrorCode;
use A8C\SpecialProjects\BackgroundJobsEngine\JobDefinition;
use A8C\SpecialProjects\BackgroundJobsEngine\RunContextInterface;
add_action( 'init', static function (): void {
$registered = a8csp_bgje_register_job(
'my-plugin',
JobDefinition::closure(
'email-digest',
static function ( array $start_args, RunContextInterface $context ): void {
my_plugin_send_digest(
(int) $start_args['user_id'],
(string) $context->get_run_id()
);
}
)
);
if ( is_wp_error( $registered ) ) {
error_log( $registered->get_error_message() );
}
}, 2 );
function my_plugin_queue_digest( int $user_id ): void {
$run = a8csp_bgje_dispatch_job(
'my-plugin',
'email-digest',
array( 'user_id' => $user_id )
);
if ( is_wp_error( $run ) ) {
// A run already owns this lane and is doing the work, so there is nothing to queue.
if ( ErrorCode::OverlapHeld->value === $run->get_error_code() ) {
return;
}
error_log( $run->get_error_message() );
return;
}
update_user_meta( $user_id, 'my_plugin_last_digest_run', (string) $run->id );
}
JobDefinition::closure() accepts the same optional JobOptions as JobDefinition::job(), so a closure-backed Job declares retry, overlap and runtime policy without an execution class. a8csp_bgje_dispatch_job() dispatches immediately; a8csp_bgje_dispatch_job_at() accepts an absolute Unix timestamp. A successful return proves admission, not completion. Keep arguments small and portable: pass identifying keys rather than bulk data.
3. A chunked job over a chunk queue
A Chunked Job implements queue generation and one-chunk processing. Queue mutations through ChunkedRunContextInterface commit only when process_chunk() returns normally.
use A8C\SpecialProjects\BackgroundJobsEngine\ChunkedJobExecutionInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\ChunkedRunContextInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\JobDefinition;
use A8C\SpecialProjects\BackgroundJobsEngine\JobOptions;
use A8C\SpecialProjects\BackgroundJobsEngine\OverlapPolicy;
use A8C\SpecialProjects\BackgroundJobsEngine\RetryPolicy;
use A8C\SpecialProjects\BackgroundJobsEngine\RunContextInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\RunId;
final class RecountCommentsExecution implements ChunkedJobExecutionInterface {
public const string NAME = 'recount-comments';
public function generate_queue( array $start_args, RunContextInterface $context ): iterable {
$post_ids = get_posts(
array(
'post_type' => (string) ( $start_args['post_type'] ?? 'post' ),
'fields' => 'ids',
'numberposts' => -1,
)
);
foreach ( $post_ids as $post_id ) {
yield array( 'post_id' => (int) $post_id );
}
}
public function process_chunk( array $chunk_args, ChunkedRunContextInterface $context ): void {
wp_update_comment_count_now( (int) $chunk_args['post_id'] );
if ( ! empty( $chunk_args['spawn_followup'] ) ) {
$context->append_chunk(
array(
'post_id' => (int) $chunk_args['post_id'],
'verify' => true,
)
);
}
}
}
add_action( 'init', static function (): void {
$registered = a8csp_bgje_register_job(
'my-plugin',
JobDefinition::chunked_job(
RecountCommentsExecution::NAME,
new RecountCommentsExecution(),
new JobOptions(
max_runtime: 300,
retry: new RetryPolicy(
max_attempts: 5,
base_delay: MINUTE_IN_SECONDS,
multiplier: 2,
max_delay: HOUR_IN_SECONDS
),
overlap: OverlapPolicy::Reject,
overlap_key: static fn ( array $args ): ?string => is_string( $args['post_type'] ?? null )
? $args['post_type']
: null,
)
)
);
if ( is_wp_error( $registered ) ) {
error_log( $registered->get_error_message() );
}
}, 2 );
function my_plugin_dispatch_recount(): void {
$run = a8csp_bgje_dispatch_job(
'my-plugin',
RecountCommentsExecution::NAME,
array( 'post_type' => 'post' )
);
if ( is_wp_error( $run ) ) {
error_log( $run->get_error_message() );
return;
}
update_option( 'my_plugin_last_recount_run', (string) $run->id );
}
add_action(
'a8csp_bgje/completed/my-plugin:recount-comments',
static function ( RunId $run_id, array $start_args ): void {
my_plugin_cleanup_recount( (string) $run_id, $start_args );
},
10,
2
);
Chunks run one at a time with a short pause between them. ChunkedRunContextInterface also exposes prepend_chunk(), get_run_id(), and get_start_args(). A failed chunked job starts a fresh run from its retained arguments and retained priority through runs()->retry_failed(); the retry replays both values directly instead of resolving priority again. A retained entry without a priority field uses engine default 10.
ChunkedJobExecutionInterface defines queue generation and chunk processing; post-queue work belongs on the completed hook above, whose payload and delivery guarantees are covered in Reacting to run lifecycles.
4. Reacting to run lifecycles
Lifecycle reactions are hook-driven: the engine pushes every outcome, so consumers listen instead of polling. Every admitted run ends in exactly one of four terminal states — completed, failed, cancelled, or superseded — and each fires its identity-specific hook first, then its generic hook.
An execution object may also declare RunCompletionInterface and receive its own completions without
naming a hook. That is a subscription the engine makes on the consumer's behalf at
jobs()->register(), not a second mechanism: the payload, the ordering against other listeners and
the at-least-once delivery are the hook's. See Completion role.
use A8C\SpecialProjects\BackgroundJobsEngine\RunFailure;
use A8C\SpecialProjects\BackgroundJobsEngine\RunId;
add_action( 'init', static function (): void {
// Completed: the payload carries the run's RunId and, when one exists, the previously
// completed run — enough to keep a scope-side pointer without polling.
add_action(
'a8csp_bgje/completed/my-plugin:email-digest',
static function ( RunId $run_id, array $start_args, ?RunId $previous_completed_run_id ): void {
update_option( 'my_plugin_last_completed_digest', (string) $run_id );
},
10,
3
);
// Failed: both variants receive the same self-identifying RunFailure. See "Handling
// failures" for the retry-policy side of this hook.
add_action(
'a8csp_bgje/failed/my-plugin:email-digest',
static function ( RunFailure $failure ): void {
my_plugin_alert_on_digest_failure( (string) $failure->run_id, $failure->summary );
},
10,
1
);
// Cancelled: an operator or scope code withdrew the run while it was not executing.
add_action(
'a8csp_bgje/cancelled/my-plugin:email-digest',
static function ( RunId $run_id, array $start_args ): void {
my_plugin_release_digest_reservation( (string) $run_id );
},
10,
2
);
// Superseded: a Replace-policy dispatch displaced this run; the replacement carries its
// own lifecycle, so clean up anything keyed to the displaced run's id.
add_action(
'a8csp_bgje/superseded/my-plugin:email-digest',
static function ( RunId $run_id, array $start_args ): void {
my_plugin_discard_partial_digest( (string) $run_id );
},
10,
2
);
}, 2 );
Terminal hooks are durable under Action Scheduler and best-effort under WP-Cron, and crash-recovery
replay can deliver them more than once — key every reaction to the run ID so repeated delivery
converges. The generic variants (a8csp_bgje/completed, …) prepend string $identity (except
failed, whose payload already self-identifies) and serve one listener across every identity.
Acting on runs, rather than reacting, stays imperative: runs()->cancel() (or
a8csp_bgje_cancel_run()) withdraws a retained run, runs()->retry_failed() starts a fresh run
from a retained failure, and runs()->inspect() / runs()->last_completed() answer point-in-time
questions from tooling — inspection returns run_not_retained for an absent run, while
last_completed() answers from a slot the capped history buffers do not evict, so a run of failures
long enough to fill one does not carry the last completion out of the answer, and the Run it
returns carries ended_at. A history row written before that slot existed is answered from the
retained buffer instead, with a null ended_at, until the identity's next completion fills the
slot. It returns null only when neither holds a completion. Day-2
operator workflows live in the CLI — see "WP-CLI".
5. Handling failures
The identity-specific a8csp_bgje/failed/{identity} hook fires before the generic a8csp_bgje/failed hook; both receive the same RunFailure object. Crash-recovery replay can reconstruct an equivalent value. Throw NonRetryableException from handle(), generate_queue(), or process_chunk() to fail permanently without consuming the remaining automatic attempts. An uncaught portability or size rejection is also deterministic: the failing invocation counts once, the engine schedules no automatic retry, and the remaining allowance stays unused. A queue that crosses the byte limit by more than its index envelope is refused by the chunked run context as the mutation is made; one that crosses it only within that envelope is refused when the attempt commits, and reports payload_rejected at the scheduling stage.
use A8C\SpecialProjects\BackgroundJobsEngine\JobDefinition;
use A8C\SpecialProjects\BackgroundJobsEngine\JobExecutionInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\NonRetryableException;
use A8C\SpecialProjects\BackgroundJobsEngine\RunContextInterface;
use A8C\SpecialProjects\BackgroundJobsEngine\RunFailure;
final class PublishWebhookExecution implements JobExecutionInterface {
public const string NAME = 'publish-webhook';
public function handle( array $start_args, RunContextInterface $context ): void {
if ( my_plugin_webhook_is_revoked( (string) $start_args['endpoint_id'] ) ) {
throw new NonRetryableException( 'The endpoint is permanently unavailable.' );
}
my_plugin_publish_webhook( (string) $start_args['endpoint_id'], (string) $context->get_run_id() );
}
}
add_action( 'init', static function (): void {
$registered = a8csp_bgje_register_job(
'my-plugin',
JobDefinition::job( PublishWebhookExecution::NAME, new PublishWebhookExecution() )
);
if ( is_wp_error( $registered ) ) {
error_log( $registered->get_error_message() );
}
add_action(
'a8csp_bgje/failed/my-plugin:' . PublishWebhookExecution::NAME,
static function ( RunFailure $failure ): void {
error_log( sprintf( 'Run %s consumed %d attempt(s).', $failure->run_id, $failure->attempts ) );
},
10,
1
);
}, 2 );
The failure summary is engine-authored and redacted; it never contains raw exception text. Terminal hooks can replay across crash recovery, so listeners use the run ID to converge repeated delivery. Their delivery is durable under Action Scheduler and best-effort under WP-Cron. Completed, failed, cancelled, and superseded reactions reach consumers through their lifecycle hooks; a completed run additionally drives RunCompletionInterface on an execution object that declares it, which is a listener on the same hook.
The engine retains up to 20 failed runs per scope-qualified identity for manual retry, subject also to a 1,000,000-byte ceiling on the complete serialized retention row. It evicts oldest entries first until both bounds hold. If a new entry cannot fit even by itself, the engine rejects that entry instead of retaining it and leaves the existing row intact. A retry that successfully starts a fresh run attempts to remove its retained source entry; a failed removal is logged. Retention is best-effort: a retention write failure is logged rather than made fatal.
The procedural functions
a8csp_bgje( string $scope ): Engine returns the lazy scope-bound handle. Each alias below takes $scope first, converts wire run identifiers to their public values where needed, invokes the matching capability-manager verb, and returns the same shape.
The procedural facade is grouped by concept: includes/job.php provides background-work registration plus kind-agnostic immediate and absolute-time dispatch, includes/schedule.php provides schedule synchronization and dispatch, and includes/run.php provides run inspection, retry, and cancellation.
| Capability-manager verb | Procedural alias | Returns |
|---|---|---|
jobs()->register( JobDefinition $definition ) |
a8csp_bgje_register_job( string $scope, JobDefinition $definition ) |
true \| WP_Error |
jobs()->dispatch( string $name, array $start_args = array(), ?int $priority = null ) |
a8csp_bgje_dispatch_job( string $scope, string $name, array $start_args = array(), ?int $priority = null ) |
Run \| WP_Error |
jobs()->dispatch_at( string $name, int $run_at, array $start_args = array(), ?int $priority = null ) |
a8csp_bgje_dispatch_job_at( string $scope, string $name, int $run_at, array $start_args = array(), ?int $priority = null ) |
Run \| WP_Error |
schedules()->sync( Schedule ...$schedules ) |
a8csp_bgje_sync_schedules( string $scope, Schedule ...$schedules ) |
true \| WP_Error |
schedules()->dispatch( string $name ) |
a8csp_bgje_dispatch_schedule( string $scope, string $name ) |
Run \| WP_Error |
schedules()->inspect() |
a8csp_bgje_inspect_schedules( string $scope ) |
array \| WP_Error |
runs()->inspect( string $name, RunId $run_id ) |
a8csp_bgje_inspect_run( string $scope, string $name, string $run_id ) |
Run \| WP_Error |
runs()->set_data( string $name, RunId $run_id, string $key, array $value ) |
a8csp_bgje_set_run_data( string $scope, string $name, string $run_id, string $key, array $value ) |
true \| WP_Error |
runs()->get_data( string $name, RunId $run_id, string $key ) |
a8csp_bgje_get_run_data( string $scope, string $name, string $run_id, string $key ) |
array \| null \| WP_Error |
runs()->last_completed( string $name ) |
a8csp_bgje_last_completed_run( string $scope, string $name ) |
Run \| null \| WP_Error |
runs()->retry_failed( string $name, RunId $run_id ) |
a8csp_bgje_retry_failed_run( string $scope, string $name, string $run_id ) |
Run \| WP_Error |
runs()->cancel( string $name, RunId $run_id ) |
a8csp_bgje_cancel_run( string $scope, string $name, string $run_id ) |
Run \| WP_Error |
Registration is typed on both surfaces. Compose a JobDefinition through job(), chunked_job(), closure(), or the engine-kind primitive for_kind(), then pass that value unchanged to the manager or procedural function.
Dispatch resolves the registered kind before admission. Priority resolution and schedule-fingerprint behavior are documented under Priority is advisory.
Public models, roles, and contexts
Imports are optional; public types can also be referenced fully qualified:
$run_id = \A8C\SpecialProjects\BackgroundJobsEngine\RunId::tryFrom( $persisted_run_id );
All public type names below are relative to the A8C\SpecialProjects\BackgroundJobsEngine namespace.
This table is the public PHP type index. Every listed type is marked @api and belongs to the PHP API tier of the canonical SemVer contract; any other autoloadable engine type is internal unless this README explicitly documents it as public.
| Type | Public shape |
|---|---|
Engine |
Scope-bound readonly handle returned by a8csp_bgje() with its jobs(), schedules(), and runs() capability managers. |
Jobs |
Scope-bound readonly manager for registration, immediate dispatch, and absolute-time dispatch. |
Schedules |
Scope-bound readonly manager for schedule synchronization, immediate dispatch, and read-only registration inspection. |
Runs |
Scope-bound readonly manager for run inspection, per-run data storage, retry, and cancellation. |
JobDefinition |
Final readonly registration declaration with public string $name, JobKind $kind, KindExecutionInterface $execution, and JobOptions $options. Its non-public constructor is exposed through job(), chunked_job(), closure(), and for_kind(), each taking an optional JobOptions. |
JobKind |
Final readonly kind key with public string $value, built-in job() and chunked_job() constructors, and from( string $value ) for a grammar-valid key. It carries no execution contract. |
JobOptions |
Final readonly policy declaration constructed with optional named parameters ?int $max_runtime, ?RetryPolicy $retry, ?OverlapPolicy $overlap, ?\Closure $overlap_key, and ?int $priority. Null uses the documented default for each policy except priority, where it defers to the priority resolution ladder ending at engine default 10. max_runtime supplies per-invocation crash-reclamation credit, not an execution limit. Construction stores max_runtime and priority without validating their declared bounds: jobs()->register() rejects an invalid max_runtime or job-default priority, schedules()->sync() rejects an invalid schedule priority, and jobs()->dispatch() rejects an invalid explicit priority. The consumer limits give the exact bounds and effective clamp. |
KindExecutionInterface |
Empty marker shared by the standard and chunked execution roles so a kind-agnostic declaration can require execution membership while registration resolves the kind-specific role. |
JobExecutionInterface |
Standard execution role extending KindExecutionInterface and requiring only handle( array $start_args, RunContextInterface $context ): void. |
RunCompletionInterface |
Optional post-run role requiring only on_completed( RunId $run_id, array $start_args, ?RunId $previous_completed_run_id ): void. Extends nothing, so any execution object may declare it. Registration subscribes the declaring object to a8csp_bgje/completed/{identity}; the parameters are that hook's payload. |
ChunkedJobExecutionInterface |
Standalone chunked execution role extending KindExecutionInterface and requiring only generate_queue( array $start_args, RunContextInterface $context ): iterable and process_chunk( array $chunk_args, ChunkedRunContextInterface $context ): void; it does not extend JobExecutionInterface. |
Schedule |
Readonly schedule declaration constructed from name, recurrence, target job, args, catch_up, and priority; only name, recurrence, and job are required, and the rest default to an empty array, CatchUpPolicy::RunOnce, and null. |
Recurrence |
Readonly fixed-interval recurrence created with every( int $seconds ) or every_anchored( int $seconds, int $anchor ); an anchor is reduced modulo the interval. |
Run |
Readonly snapshot with string $identity, RunId $id, RunStatus $status, and ?int $ended_at. ended_at is the Unix timestamp the run reached its terminal state; it is populated by runs()->last_completed() and null on projections that do not carry one, such as a dispatch result. |
RunId |
Final readonly stringable wrapper for a canonical run identifier; from( string ) requires canonical input, tryFrom( string ) returns null for another shape, and string casting returns the wire value. |
RunFailure |
Readonly value with string $identity, RunId $run_id, int $attempts, RunFailureStage $stage, ErrorCode $code, string $summary, and generic diagnostic payload ?array $details. |
RunFailureStage |
Final readonly interned open string-backed stage carried by RunFailure::$stage; its grammar, engine stages, and third-party keys are described below this table. |
RetryPolicy |
Readonly value constructed from max_attempts, base_delay, multiplier, and max_delay; defaults are 3, MINUTE_IN_SECONDS, 2, and HOUR_IN_SECONDS. max_attempts includes the initial attempt. It bounds consecutive failures at one point of a run rather than failures across the whole run: a successful chunk or queue generation clears the consumed attempts, so a chunked job allows this many consecutive failures at each chunk. Attempts, base delay, and multiplier are at least 1, and maximum delay is at least the base delay. It exposes delay_ceiling_for_attempt( int $attempt ): int. |
RunContextInterface |
get_run_id(): RunId and get_start_args(): array. |
RunContext |
Final context constructed with RunId $run_id and array $start_args; implements RunContextInterface. |
ChunkedRunContextInterface |
Extends RunContextInterface with append_chunk( array $chunk_args ): void and prepend_chunk( array $chunk_args ): void. |
NonRetryableException |
Runtime exception that marks client work as permanently failed. |
The engine supplies the only implementation of ChunkedRunContextInterface; consumers must not implement it, and methods may be added in minor versions.
JobKind::from() validates kind-key grammar but does not install a kind. Only engine-installed kinds can be registered. The generic definition path is registration data, while kind handlers and their SPI stay internal.
The backed enums are:
| Enum | Cases and backing values |
|---|---|
RunStatus |
Running = 'running', Completed = 'completed', Failed = 'failed', Cancelled = 'cancelled', Superseded = 'superseded' |
OverlapPolicy |
Allow = 'allow', Reject = 'reject', Replace = 'replace' |
CatchUpPolicy |
RunOnce = 'run_once', Skip = 'skip' |
ErrorCode |
InvalidArgument = 'invalid_argument', AlreadyRegistered = 'already_registered', EngineUnavailable = 'engine_unavailable', UnknownJob = 'unknown_job', UnknownSchedule = 'unknown_schedule', OverlapHeld = 'overlap_held', AdmissionConflict = 'admission_conflict', PayloadRejected = 'payload_rejected', BackendUnavailable = 'backend_unavailable', BackendRejected = 'backend_rejected', StorageFailed = 'storage_failed', RunNotRetained = 'run_not_retained', RunNotCancellable = 'run_not_cancellable', UnsupportedOperation = 'unsupported_operation', ExecutionFailed = 'execution_failed' |
Consumers treat unknown backing values as generic failures for ErrorCode and as generic non-terminal or terminal states, as appropriate, for RunStatus.
RunStatus has no pending case. Running means admitted and not yet terminal, so a run dispatched for a future time reports running from admission onward, including while it waits for that time to arrive.
RunFailureStage is a final, interned, open string-backed value. from( string ) wraps a lowercase snake key with at most one dot qualifier and throws \ValueError for malformed input; tryFrom( string ) returns null instead. Engine stages are available through execution(), queue_generation(), crash_reclamation(), and scheduling(). Grammar-valid third-party stages such as acme.export_sync remain intact.
JobKind::from(), RunId::from(), and RunFailureStage::from() throw \ValueError for malformed strings. \ValueError extends \Error, not \Exception; use the corresponding tryFrom() parser when malformed consumer input should return null.
Authoring work
Definitions and kinds
Every registration supplies one immutable JobDefinition. The typed constructors are:
JobDefinition::job( string $name, JobExecutionInterface $execution, ?JobOptions $options = null )JobDefinition::chunked_job( string $name, ChunkedJobExecutionInterface $execution, ?JobOptions $options = null )JobDefinition::closure( string $name, \Closure $handler, ?JobOptions $options = null )JobDefinition::for_kind( string $name, JobKind $kind, KindExecutionInterface $execution, ?JobOptions $options = null )
The job() and chunked_job() constructors bind the built-in kind to its typed execution role. The closure handler receives (array $start_args, RunContextInterface $context); the adapter that implements its execution role is internal. for_kind() is the generic registration-data primitive. Only engine-installed kinds register successfully, and consumer code cannot install or implement kind handlers.
JobKind::job() and JobKind::chunked_job() return the installed built-in keys. JobKind::from() wraps a grammar-valid key matching [a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)?; it does not prove that a handler is installed. Passing an object that does not implement KindExecutionInterface to for_kind() fails definition construction with \TypeError. Registration of an uninstalled kind returns WP_Error with invalid_argument and names the kind. A marker-implementing object of the wrong role reaches registration, where the resolved handler returns WP_Error with invalid_argument and names both the kind and its expected execution interface.
Job execution and policy
JobExecutionInterface requires exactly handle( array $start_args, RunContextInterface $context ): void. The definition supplies the name and policy, so the execution role carries no naming or policy methods, and the only lifecycle reaction it may carry is the optional RunCompletionInterface described below. A normal return succeeds. A handler that catches its own failure and returns normally therefore records no failed attempt and leaves the retry budget untouched. Only a throwable that escapes the handler fails the attempt and follows the retry policy, except NonRetryableException, which fails permanently.
JobOptions carries five independent optional policies: max_runtime, retry, overlap, overlap_key, and priority. Null uses the documented default for each policy except priority, where it leaves the other resolution rungs operative. max_runtime supplies the per-invocation crash-reclamation credit: the engine credits the handler for this duration before its heartbeat begins aging through a separately resolved lock-staleness window. It never interrupts the handler. jobs()->register() rejects a non-null value below one second, so 0 is invalid rather than unlimited. Null selects 300 seconds, and declarations above 21,600 seconds (6 hours) remain valid but clamp to that effective credit. An unlimited credit would leave a crashed run's valid lock unreclaimable: under the default OverlapPolicy::Reject, every later matching dispatch would be refused because maintenance could never classify the valid lock as stale.
Maintenance also crash-fails a persisted Running, executing run when schema-invalid lock bytes hide its lock heartbeat and its run-row heartbeat is stale. Delivery ownership stamps the lock and run row from the same credited timestamp, so the run-row heartbeat is valid liveness evidence in that case. A stale non-executing run with no pending-action descriptor follows the same recovery path; a fresh row remains preserved. The later lock phase reads active-run values in bounded batches, retains only unreadable-row and Running-lane liveness, and exact-deletes an inspected malformed generation only when no Running row occupies its lane. An unreadable active-run row preserves every malformed lane for that identity. A changed or absent generation loses the exact-delete fence silently, while an option-delete failure emits a redacted operator warning.
While a non-executing row is fresh and preserved, wp a8csp-bgje runs cancel can resolve it only when the identity has a live executable registration matching the persisted kind; otherwise cancellation returns unknown_job. Once the heartbeat is stale, maintenance can crash-reclaim the engine-installed job and chunked_job kinds without that consumer registration and then reclaim the orphaned malformed lock.
Neither JobOptions nor Schedule validates priority during construction. jobs()->register() rejects an out-of-range job default, schedules()->sync() rejects an out-of-range schedule value, and jobs()->dispatch() rejects an out-of-range explicit argument. The remaining defaults are a RetryPolicy with 3 maximum attempts, a 60-second base delay, multiplier 2, and 3,600-second maximum delay, OverlapPolicy::Reject, and a null overlap-key resolver. The priority field defaults to null; the resolution ladder ends at engine default 10. A null resolver uses the canonical argument hash. The a8csp_bgje/retry_policy filter receives the resolved policy before `a8csp
This README is longer than the copy stored here. Read the rest on GitHub →
Releases
6 releases. Each count is every asset in that release; expand a row for the breakdown.