
Symfony:symfony Scheduler Skill
- 389 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:symfony-scheduler is an agent skill that configures Symfony Scheduler recurring tasks and Messenger-backed async handlers for developers who need cron-like PHP background work without manual crontab wiring.
About
symfony:symfony-scheduler is a Symfony-focused agent skill from makfly/superpowers-symfony for defining recurring jobs with the Scheduler component native since Symfony 7.x. The workflow walks through async contracts, idempotent Messenger handlers, routing, retry policies, failure transports, and poison-message handling with at-least-once delivery assumptions. Developers reach for symfony:symfony-scheduler when replacing fragile server crontab entries with in-app schedules, stabilizing background retries, or integrating observability around async PHP workloads. Output includes updated async configuration, handler code, retry and failure policy decisions, and operational validation notes. The skill references companion docs for complexity tiers and deep implementation patterns when progressive disclosure is needed.
- Symfony Scheduler
- recurring jobs
- cron replacement
- PHP backend
- task triggers
Symfony:Symfony Scheduler by the numbers
- 389 all-time installs (skills.sh)
- Ranked #26 of 68 PHP & Laravel skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonysymfony-schedulerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 389 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you schedule recurring Symfony jobs without crontab?
Configure Symfony Scheduler for recurring jobs, cron-like tasks, and background work in PHP apps without manual crontab wiring.
Who is it for?
PHP backend engineers shipping Symfony 7.x APIs or SaaS apps that need reliable recurring background work integrated with Messenger.
Skip if: Teams on pre-7 Symfony versions without Scheduler, pure frontend work, or infrastructure that only needs OS-level cron with no PHP integration.
When should I use this skill?
User asks to add Symfony Scheduler, recurring Messenger tasks, cron replacement, retry policies, or failure transports in a Symfony PHP app.
What you get
Async Scheduler and Messenger configuration, idempotent handler implementations, retry and failure-transport policy, and operational validation evidence.
- Scheduler and Messenger configuration
- Idempotent handler code
- Retry and failure-transport policy notes
By the numbers
- Targets Symfony Scheduler native since Symfony 7.x
- Four-step default workflow from contract definition through operational validation
Files
Symfony Scheduler (Symfony)
Use when
- Implementing asynchronous workflows with Messenger/Scheduler/Cache.
- Stabilizing retries and failure transports.
Default workflow
1. Define async contract and delivery semantics. 2. Implement idempotent handlers and routing strategy. 3. Configure retries, failure transport, and observability. 4. Validate success/failure replay scenarios.
Guardrails
- Assume at-least-once delivery, not exactly-once.
- Keep handlers deterministic and side-effect aware.
- Surface poison-message handling strategy.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- Async config/handlers updated.
- Retry/failure policy decisions.
- Operational validation evidence.
References
reference.mddocs/complexity-tiers.md
Reference
Symfony Scheduler
Available in Symfony 7.1+ as a native component.
Installation
composer require symfony/schedulerBasic Schedule
Define a Schedule
<?php
// src/Scheduler/DefaultScheduleProvider.php
namespace App\Scheduler;
use App\Message\CleanupExpiredSessions;
use App\Message\GenerateDailyReport;
use App\Message\SyncInventory;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
#[AsSchedule('default')]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
// Every 5 minutes
->add(RecurringMessage::every('5 minutes', new SyncInventory()))
// Daily at 2 AM
->add(RecurringMessage::every('1 day', new GenerateDailyReport())
->from(new \DateTimeImmutable('02:00')))
// Every hour
->add(RecurringMessage::every('1 hour', new CleanupExpiredSessions()))
// Cron expression
->add(RecurringMessage::cron('0 */6 * * *', new ProcessQueuedEmails()))
;
}
}Message Classes
<?php
// src/Message/GenerateDailyReport.php
namespace App\Message;
final class GenerateDailyReport
{
public function __construct(
public readonly ?\DateTimeImmutable $forDate = null,
) {}
}
// src/MessageHandler/GenerateDailyReportHandler.php
namespace App\MessageHandler;
use App\Message\GenerateDailyReport;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GenerateDailyReportHandler
{
public function __invoke(GenerateDailyReport $message): void
{
$date = $message->forDate ?? new \DateTimeImmutable('yesterday');
$this->reportService->generate($date);
$this->logger->info('Daily report generated', ['date' => $date->format('Y-m-d')]);
}
}Trigger Types
Time-Based Triggers
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger;
// Every X interval
RecurringMessage::every('5 minutes', new MyMessage())
RecurringMessage::every('1 hour', new MyMessage())
RecurringMessage::every('1 day', new MyMessage())
RecurringMessage::every('1 week', new MyMessage())
// Cron expressions
RecurringMessage::cron('*/5 * * * *', new MyMessage()) // Every 5 minutes
RecurringMessage::cron('0 * * * *', new MyMessage()) // Every hour
RecurringMessage::cron('0 0 * * *', new MyMessage()) // Daily at midnight
RecurringMessage::cron('0 0 * * 0', new MyMessage()) // Weekly on Sunday
RecurringMessage::cron('0 0 1 * *', new MyMessage()) // Monthly on 1st
// With timezone
RecurringMessage::cron('0 9 * * 1-5', new MyMessage())
->timezone(new \DateTimeZone('Europe/Paris'))
// Starting from specific time
RecurringMessage::every('1 day', new MyMessage())
->from(new \DateTimeImmutable('06:00'))
// Until specific time
RecurringMessage::every('1 hour', new MyMessage())
->until(new \DateTimeImmutable('2024-12-31'))Custom Trigger
<?php
// src/Scheduler/Trigger/BusinessHoursTrigger.php
namespace App\Scheduler\Trigger;
use Symfony\Component\Scheduler\Trigger\TriggerInterface;
class BusinessHoursTrigger implements TriggerInterface
{
public function __construct(
private TriggerInterface $inner,
) {}
public function __toString(): string
{
return 'business_hours(' . $this->inner . ')';
}
public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
{
$next = $this->inner->getNextRunDate($run);
if ($next === null) {
return null;
}
// Skip weekends
while ((int) $next->format('N') >= 6) {
$next = $next->modify('+1 day')->setTime(9, 0);
}
// Only between 9 AM and 6 PM
$hour = (int) $next->format('H');
if ($hour < 9) {
$next = $next->setTime(9, 0);
} elseif ($hour >= 18) {
$next = $next->modify('+1 day')->setTime(9, 0);
}
return $next;
}
}Multiple Schedules
<?php
// src/Scheduler/MaintenanceScheduleProvider.php
#[AsSchedule('maintenance')]
class MaintenanceScheduleProvider implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
->add(RecurringMessage::cron('0 3 * * *', new DatabaseBackup()))
->add(RecurringMessage::cron('0 4 * * 0', new CleanupLogs()))
;
}
}Run specific schedule:
bin/console messenger:consume scheduler_maintenanceRunning the Scheduler
As Messenger Transport
The scheduler creates a special transport:
# Run the default schedule
bin/console messenger:consume scheduler_default
# Run specific schedule
bin/console messenger:consume scheduler_maintenance
# With worker options
bin/console messenger:consume scheduler_default --time-limit=3600Supervisor Configuration
[program:scheduler]
command=php /var/www/app/bin/console messenger:consume scheduler_default --time-limit=3600
user=www-data
numprocs=1
autostart=true
autorestart=true
startretries=10
stderr_logfile=/var/log/scheduler.err.log
stdout_logfile=/var/log/scheduler.out.logStateful Schedules
Track last run and prevent overlap:
<?php
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Scheduler\Schedule;
#[AsSchedule('default')]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
public function __construct(
private LockFactory $lockFactory,
private CacheInterface $cache,
) {}
public function getSchedule(): Schedule
{
return (new Schedule())
->stateful($this->cache) // Remember last run times
->lock($this->lockFactory->createLock('scheduler')) // Prevent overlap
->add(RecurringMessage::every('1 hour', new HeavyTask()))
;
}
}Testing Schedules
<?php
use Symfony\Component\Scheduler\RecurringMessage;
class ScheduleTest extends TestCase
{
public function testDailyReportScheduledCorrectly(): void
{
$provider = new DefaultScheduleProvider();
$schedule = $provider->getSchedule();
$messages = $schedule->getRecurringMessages();
// Find the daily report message
$dailyReport = array_filter(
iterator_to_array($messages),
fn($m) => $m->getMessage() instanceof GenerateDailyReport
);
$this->assertCount(1, $dailyReport);
// Check next run time
$message = reset($dailyReport);
$trigger = $message->getTrigger();
$nextRun = $trigger->getNextRunDate(new \DateTimeImmutable());
$this->assertEquals('02', $nextRun->format('H'));
}
}Monitoring
<?php
// src/EventSubscriber/SchedulerMonitoringSubscriber.php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\PostRunEvent;
use Symfony\Component\Scheduler\Event\PreRunEvent;
class SchedulerMonitoringSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
PreRunEvent::class => 'onPreRun',
PostRunEvent::class => 'onPostRun',
];
}
public function onPreRun(PreRunEvent $event): void
{
$this->logger->info('Starting scheduled task', [
'message' => get_class($event->getMessage()),
]);
}
public function onPostRun(PostRunEvent $event): void
{
$this->logger->info('Completed scheduled task', [
'message' => get_class($event->getMessage()),
'duration' => $event->getDuration(),
]);
}
}Best Practices
1. Stateful schedules: Use cache to track last run 2. Lock heavy tasks: Prevent overlapping executions 3. Supervisor: Use for production reliability 4. Monitor execution: Log start/end times 5. Idempotent tasks: Safe to re-run if needed 6. Timezone awareness: Be explicit about timezones
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- php bin/console messenger:consume --limit=1
- php bin/console messenger:failed:show
- ./vendor/bin/phpunit --filter=Messenger
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
How it compares
Choose symfony:symfony-scheduler over generic cron docs when jobs must live inside Symfony Messenger with retries, failure transports, and PHP-native observability.
FAQ
Does symfony:symfony-scheduler require Symfony 7.x?
symfony:symfony-scheduler targets the Symfony Scheduler component native since Symfony 7.x. The skill defines schedules and triggers integrated with Messenger for recurring PHP background work instead of manual crontab entries.
What delivery semantics does symfony:symfony-scheduler assume?
symfony:symfony-scheduler assumes at-least-once Messenger delivery, not exactly-once. Handlers must stay idempotent and side-effect aware, with explicit retry, failure-transport, and poison-message handling documented.