← CLI

CLI FEATURE

Workers & Scheduling

Semitexa is not only request-response code. The CLI also owns the long-running workers and operator interventions that keep the platform moving.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Semitexa CLI also owns the long-running side of the platform: queues, schedules, webhook delivery, mail workers, and tenant-context execution.

How it works

Dedicated commands expose each stage separately: inspect schedules, plan due runs, start workers, inspect webhook inbox/outbox state, replay deliveries, and run commands in tenant scope.

Why it matters

This makes background systems operable. Operators can inspect, intervene, and replay explicitly instead of treating async infrastructure as invisible magic behind the web server.

Key concepts

queue:work
Runs the async events worker for queued handlers.
scheduler:plan
Materializes due schedule occurrences into concrete run rows before workers execute them.
tenant:run
Executes any command inside a specific tenant context.

Operator Runtime

Long-running platform work is part of the same command surface

Async queues, scheduler pools, outbound webhooks, mail delivery, and tenant-scoped execution all surface through explicit commands. That makes the runtime operable without custom one-off scripts.

Dedicated workers. queue:work, webhook:work, and mail:work turn background processing into explicit operator processes rather than hidden side-effects.

Planner plus executor. The scheduler surface is separated into list, plan, run-now, and work so teams can inspect and intervene before blindly starting daemons.

Context-aware execution. tenant:run lets operators execute commands inside a concrete tenant context instead of manually injecting environment assumptions.

Command Purpose Why it matters
bin/semitexa queue:work nats async Run the async events worker against a chosen transport and queue. Keeps event-driven background work explicit and separately operable.
bin/semitexa scheduler:list && bin/semitexa scheduler:plan Inspect configured schedules, then materialize due runs. Good operational sequence before starting or debugging the scheduler worker.
bin/semitexa webhook:show outbox --status=pending && bin/semitexa webhook:work Inspect webhook backlog, then run delivery worker. Makes outbound integration behavior reviewable rather than opaque.
bin/semitexa tenant:run acme cache:clear --twig Run a command inside a tenant context. Critical when the platform behavior depends on tenant-aware configuration or data isolation.

Bring up the scheduler loop deliberately

bin/semitexa scheduler:list
bin/semitexa scheduler:plan
bin/semitexa scheduler:work default

Inspect and replay webhook traffic

bin/semitexa webhook:show inbox --limit=10
bin/semitexa webhook:replay:inbound <delivery-uuid>

Operate background work in one tenant

bin/semitexa tenant:run acme queue:work
bin/semitexa tenant:run acme cache:clear --twig
queue:work worker:processes scheduler:list scheduler:plan scheduler:work webhook:work tenant:run

Verified against Semitexa Ultimate 2026.09.19.1020

Workers & Scheduling

Semitexa is not only request-response code. The CLI also owns the long-running workers and operator interventions that keep the platform moving.

How it works

queue:work, webhook:work, and mail:work are separate explicit processes rather than hidden side-effects of the web runtime. The scheduler surface is split into scheduler:list (inspect), scheduler:plan (materialize), and scheduler:work (run the loop) so teams can inspect state before pushing harder. tenant:run executes any command inside a concrete tenant context, including queue and cache operations.

In a project started with bin/semitexa server:start, these workers are already running: a worker service, in a container of its own so it can be scaled apart from the web server, runs every line bin/semitexa worker:processes prints. Each package names what it needs with #[AsWorkerProcessSource]: core one queue:work per queue its listeners and handlers use, mail mail:work, media media:work, the scheduler scheduler:plan (once a minute) and one scheduler:work for each pool its jobs name (scheduler:work reads one pool; a pool chosen only when a job is dispatched is named by the package that dispatches it, as workflow does for its timeout pools), webhooks webhook:work. It runs whatever EVENTS_ASYNC says: with EVENTS_ASYNC=0 and no EVENTS_TRANSPORT the queue consumers name nothing (the transport is in-memory, one per process), and the scheduler and webhooks still run; EVENTS_TRANSPORT=nats or database gives them their queue:work all the same. WORKER_REPLICAS in .env sets how many worker containers run.

Why this matters

Operability matters as much as functionality. Separate inspect, plan, execute, and replay actions mean the platform can be observed and intervened on deliberately instead of forcing operators to restart processes and hope for the best.

© Harold Abelson: "Programs must be written for people to read, and only incidentally for machines to execute."

queue:work Command Implementation slice
<?phpdeclare(strict_types=1);namespace Semitexa\Core\Application\Console\Command;use Semitexa\Core\Attribute\AsCommand;use Symfony\Component\Console\Command\Command;use Symfony\Component\Console\Input\InputArgument;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Input\InputOption;use Symfony\Component\Console\Output\OutputInterface;use Symfony\Component\Console\Style\SymfonyStyle;#[AsCommand(name: 'queue:work', description: 'Run a queue worker (processes queued listeners and handlers enqueued with execution: queued)')]class QueueWorkCommand extends Command{    protected function configure(): void    {        $this->setName('queue:work')            ->setDescription('Run a queue worker (processes queued listeners and handlers enqueued with execution: queued)')            ->addArgument('transport', InputArgument::OPTIONAL, 'Transport: nats or in-memory (default from EVENTS_ASYNC)', null)            ->addArgument('queue', InputArgument::OPTIONAL, 'Queue name (default from EVENTS_QUEUE_DEFAULT)', null)            ->addOption('timeout', 't', InputOption::VALUE_OPTIONAL, 'Worker timeout in seconds', null);    }    protected function execute(InputInterface $input, OutputInterface $output): int    {        $io = new SymfonyStyle($input, $output);        $transport = $input->getArgument('transport');        $queue = $input->getArgument('queue');        $io->title('Events worker (queue)');        try {            $worker = new \Semitexa\Core\Queue\QueueWorker();            $worker->setOutput($output);            $worker->run($transport, $queue);        } catch (\Throwable $e) {            $io->error('Worker failed: ' . $e->getMessage());            return Command::FAILURE;        }        return Command::SUCCESS;    }}

Ops Pattern

Healthy command topology for background systems

The value here is operability: separate inspect, plan, execute, and replay actions so the platform can be observed before it is pushed harder.

Separate “show/list” commands from “work/replay/run-now” commands so operators can inspect state before mutating it.

Treat workers as first-class processes with their own commands, not as accidental sidecars hidden behind the web runtime.

Use tenant:run when operational intent is tenant-specific instead of hoping ambient context is correct.

Scheduler surfaces are stronger when planning and execution remain explicit and individually observable.

How it works

Dedicated commands expose each stage separately: inspect schedules, plan due runs, start workers, inspect webhook inbox/outbox state, replay deliveries, and run commands in tenant scope.

Why it matters

This makes background systems operable. Operators can inspect, intervene, and replay explicitly instead of treating async infrastructure as invisible magic behind the web server.

Key concepts

queue:work
Runs the async events worker for queued handlers.
scheduler:plan
Materializes due schedule occurrences into concrete run rows before workers execute them.
tenant:run
Executes any command inside a specific tenant context.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal