aionphp/aion

Code-defined, runtime-managed Laravel schedules with execution history and an operational dashboard.

Maintainers

Package info

github.com/rahimi-ali/aion

pkg:composer/aionphp/aion

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-27 21:29 UTC

This package is auto-updated.

Last update: 2026-08-27 21:32:29 UTC


README

Aion is a Laravel 13 package for observing and runtime-managing code-defined scheduled tasks.

Laravel remains the scheduler. Aion adds a stable managed identity, optional runtime cron and enable/disable overrides, factual run history, bounded persisted output, and a focused operational dashboard.

The core rule is deliberately strict:

Code defines what may run. Runtime state only controls when or whether it runs.

The database cannot create tasks, change commands or arguments, instantiate classes, or alter execution semantics. Removing a definition from code makes its stored override inert.

Requirements

  • PHP 8.3 or newer
  • Laravel 13.23 or newer
  • Livewire 4 (installed by this package)
  • a database connection supported by Laravel

Installation

composer require aionphp/aion
php artisan aion:install
php artisan migrate

aion:install publishes:

  • config/aion.php;
  • the two migrations;
  • app/Providers/AionServiceProvider.php;
  • compiled dashboard assets under public/vendor/aion.

It also adds the application provider, using the application's root namespace, to bootstrap/providers.php. Migration filenames are fixed, so repeated publication does not create timestamped duplicates. Re-run with --force only when you intentionally want to overwrite published files.

Laravel's default Composer post-update-cmd republishes the conventional laravel-assets tag. If the host application skips or removes that script, republish Aion's compiled dashboard assets after an upgrade so the live package views and public JavaScript/CSS stay in sync:

php artisan vendor:publish --provider="Aion\AionServiceProvider" --tag=aion-assets --force

Do not use aion:install --force for routine upgrades; it also overwrites application-owned configuration, migrations, and the published provider.

Defining managed schedules

Definitions live in the published provider, which is loaded in web and console requests. Fully configure a native Laravel event, then register it:

<?php

namespace App\Providers;

use Aion\Facades\Aion;
use App\Console\Commands\CleanupOrders;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\ServiceProvider;

final class AionServiceProvider extends ServiceProvider
{
    public function boot(Schedule $schedule): void
    {
        Aion::register(
            key: 'orders.cleanup',
            event: $schedule
                ->command(CleanupOrders::class)
                ->dailyAt('03:00')
                ->withoutOverlapping()
                ->onOneServer(),
            label: 'Clean up orders',
            description: 'Clean up expired order state.',
        );
    }
}

label is optional and derived from the key. Keys are lowercase, limited to 191 characters, and may contain letters, numbers, ., :, _, and -. The lowercase rule prevents collisions under case-insensitive database collations.

The registration call captures the code cron after the event has been configured. Continue to use Laravel's native APIs for timezone, conditions, environments, overlap locks, single-server behavior, background execution, maintenance behavior, and hooks. Aion does not mirror that fluent API.

Authorization

Viewing and management are independent:

use Aion\Facades\Aion;
use Illuminate\Http\Request;

Aion::viewUsing(
    fn (Request $request): bool =>
        $request->user()?->can('viewScheduler') ?? false,
);

Aion::manageUsing(
    fn (Request $request): bool =>
        $request->user()?->can('manageScheduler') ?? false,
);

A manager can also view the dashboard. Every page and Livewire hydration checks view access, and every mutation independently checks manage access. No user class or guard is assumed.

The published provider leaves these callbacks commented as secure examples. When neither callback is configured, Aion allows local development only. All other environments fail closed. Configure the callbacks before exposing Aion outside local development. A throwing authorization callback is reported and denied.

Configuration

return [
    'dashboard' => env('AION_DASHBOARD_ENABLED', true),
    'domain' => env('AION_DOMAIN'),
    'path' => env('AION_PATH', 'aion'),
    'middleware' => ['web'],
    'database' => [
        'connection' => env('AION_DB_CONNECTION'),
    ],
    'trim' => [
        'runs' => 10080, // minutes
        'incomplete_runs' => null, // null preserves running/incomplete rows
    ],
    'output' => [
        'max_bytes' => 1024 * 1024,
    ],
];
  • Set dashboard to false to omit Aion's routes, views, Livewire components, and dashboard middleware while keeping runtime overrides, execution history, and pruning active.
  • A null domain uses the application's normal domain.
  • The path is relative and defaults to /aion.
  • Middleware is an arbitrary array. auth is not hardcoded.
  • The database connection falls back to Laravel's default connection.
  • Retention is expressed in minutes; 10080 is seven days. Normal retention applies only to terminal runs. Set trim.incomplete_runs to a minute count only when stale running/incomplete rows should also be removed.

For session-authenticated dashboards, retain web so CSRF and the session are available. Livewire already runs that group on its update endpoint, so Aion does not replay it. Additional resolvable configured middleware classes are persisted across updates; Livewire's upstream limitation means parameterized custom middleware must also establish authentication in a way available to Livewire updates. Aion's authorization callbacks are still evaluated on every request.

As with other route configuration, rebuild Laravel's route and configuration caches after changing the dashboard, domain, path, or middleware values in a cached production application.

Runtime overrides

The dashboard can set only:

  • enabled: true, false, or null for the code default;
  • cron: a valid five-field cron expression or null for the code default.

Only actual differences are stored. Saving a value equal to its code default removes that field's override; when both fields return to defaults, the row is deleted.

Dashboard mutations compare each changed field with the stored value that was rendered. A concurrent change to the same field returns 409 instead of being overwritten; changes to different fields merge. Reset checks both fields, so a stale page cannot erase a newer operational override.

The schedule detail page keeps these values visibly separate:

Code default + optional runtime override = effective schedule

Cron previews use the code-defined timezone. “Next occurrence” describes cron only: normal Laravel conditions, environments, maintenance mode, overlap locks, and single-server coordination may still skip it.

Changing overrides from code

Deployment scripts and Tinker can set the same values directly. These calls are last-write-wins and deliberately skip the dashboard's stale-write check, because there is no rendered page to be stale against:

use Aion\Facades\Aion;

Aion::putOverride('orders.cleanup', cron: '0 4 * * *', enabled: false);
Aion::clearOverride('orders.cleanup');

Prefer the dashboard, or Aion::changeOverride() with expected values, whenever a concurrent operator change should be rejected rather than silently overwritten. Both paths validate cron, refuse keys absent from the code registry, collapse code-default values, and dispatch the same override events.

Execution history and output

Aion records running, succeeded, and failed. A running row with no finish is shown as “Running / incomplete”; Aion does not invent a failure after catastrophic termination.

Laravel's schedule:test command is instrumented too. It invokes an event directly without consulting filters, so a runtime disable does not block a manual test run; the override is still resolved so the recorded effective cron is the real one rather than the code default. Non-zero test runs are finalized immediately with their output and a generic failure. Once Aion's start callback has run, later hook exceptions use the failed command fallback because Laravel emits no scheduled-task failure event on that path. An application before hook registered ahead of Aion can abort schedule:test before Aion receives a task identity, so that execution cannot be recorded.

For command and executable schedules, Aion uses Laravel's native output redirection. Default output is redirected to a private 0600 per-run temporary file, then scrubbed to valid UTF-8, stripped of ANSI control sequences, and persisted as plain text up to output.max_bytes. The dashboard always escapes it.

output.max_bytes bounds only the text persisted in the database. It does not bound the temporary file while a task is running; that file can grow with the task's output. Aion deletes its file after normal finalization. A crash or background completion failure can leave it until the associated row is pruned. Running/incomplete rows and files are preserved by default unless trim.incomplete_runs is explicitly configured. Every prune also removes validated numeric run files on the local node when their row no longer exists on the writer, so shared-database deployments eventually clean node-local orphans when pruning runs on that node.

Aion does not redirect default output for schedules configured with Laravel's Event::user(), because the target sudo user may be unable to write Aion's scheduler-owned 0600 file. Their output is unavailable unless the application configures its own accessible destination.

Application-defined output destinations and hooks are preserved and their files are never deleted by normal recording. Foreground output may be persisted from such a destination, although the application remains responsible for concurrent-writer attribution. Background overwrite and append destinations are always marked unavailable because a fresh completion process cannot prove which bytes belong to that run.

Callback and queued-job schedules record scheduler execution but mark stdout unavailable. A scheduled queued job measures dispatch into the queue, not later queue processing.

Persisted output and exception messages can contain secrets if application code prints them. Treat the dashboard and database as operationally sensitive.

Retention

Run pruning is automatic once daily on every scheduler node as an ordinary, unmanaged Laravel schedule. It can also be run manually:

php artisan aion:prune
php artisan aion:prune --batch=500

Normal pruning measures terminal retention from finished_at and deletes only old terminal scheduler_runs rows in deterministic, bounded ID batches. Running/incomplete rows are retained unless trim.incomplete_runs is set to an explicit age threshold; because Aion has no portable process-liveness signal, that opt-in threshold can also remove a legitimately long-running task. Retention values must be non-negative integers; invalid configuration stops the command before it deletes anything. When a selected row is deleted, Aion also deletes only its numeric run file under Aion's private output directory. A local orphan sweep retries failed deletes and handles rows pruned by another shared-database node. Automatic pruning deliberately has no shared overlap mutex: concurrent database deletes are idempotent, and every node must sweep its own local output directory. scheduler_overrides is configuration and is never pruned.

Extension events

Aion dispatches normal Laravel events:

  • Aion\Events\ManagedScheduleStarting
  • Aion\Events\ManagedScheduleSucceeded
  • Aion\Events\ManagedScheduleFailed
  • Aion\Events\ScheduleOverrideChanged
  • Aion\Events\ScheduleOverrideCleared

They expose task, definition, run, override, and exception context as applicable. Use synchronous application listeners for audit logs, alerts, metrics, or security monitoring. The definition context contains Laravel events and closures and is not queue-serializable. A throwing extension listener is reported and cannot change the task or override outcome.

Deployment and scheduler behavior

schedule:run batch-loads overrides before Laravel evaluates due events. schedule:work launches a fresh schedule:run process every minute, so it observes new values without deployment or cache clearing. Sub-minute schedules keep one effective snapshot for the current minute and refresh on the next top-level run.

Background commands complete through Laravel's separate schedule:finish process. Aion uses a task-key-derived stable mutex identity and Laravel's hidden Context propagation to preserve the exact run ID and Aion-owned output path across that boundary. That launch context remains authoritative if a deployment changes the same task key to foreground execution, a callback, or a different output destination before completion; Aion finalizes the original run and restores the newly configured output afterward. If application code provides a custom mutex name resolver, it remains code-owned and should remain stable across deployments so Laravel can run the event's native completion hooks. When a changed custom mutex matches no current event, Aion marks the exact run failed and cleans its private output instead of leaving it indefinitely incomplete.

If an application after/success/failure hook aborts schedule:finish before Aion's normal finalizer runs, or Laravel finds no event for a changed custom mutex, Aion uses the remaining hidden run context at CommandFinished to mark that exact run failed with a generic completion message. Laravel does not expose the original hook exception on that event; it still reports the original exception through the application's normal console exception handler.

Background commands configured with Event::user() also depend on sudo preserving Laravel's __LARAVEL_CONTEXT environment value. Typical sudo environment filtering removes it, so this combination cannot be finalized reliably unless deployment policy explicitly preserves that value. Application-owned background output remains unavailable even when finalization succeeds.

Failure behavior is intentional:

  • override lookup failure: all managed tasks skip; unmanaged Laravel schedules continue;
  • run-history/output persistence failure: the task continues and the monitoring error is reported;
  • dashboard data failure: the dashboard returns 503 instead of presenting defaults as effective runtime state;
  • migrations missing: override lookup therefore fails closed until php artisan migrate completes.

Security boundary

Aion does not provide task creation, editable commands or arguments, class selection, shell input, timezone editing, overlap/single-server editing, Run Now, retries, notifications, metrics graphs, queue management, or scheduler replacement.

Mutation routes accept only changed cron and enabled values plus their matching expected stored values. They require manage authorization, use the configured middleware's CSRF protection, validate cron server-side, reject stale writes, and reject keys not registered in the current code registry. Captured output is never rendered as HTML, and serialized job payloads are never exposed.

See docs/architecture.md for lifecycle and ownership details.

Development

composer install
composer cs-check
composer analyse
composer test

PHPStan runs at level 8 with Larastan and strict rules. PHP CS Fixer follows the repository's PSR-12/risky and strict-type conventions. GitHub Actions runs style, static analysis, asset parity, and the test suite across supported PHP versions.