Scheduler — AEM as a Cloud Service
Overview
Schedules in AEM CS must be declared as OSGi component properties — Scheduler.schedule() is not persisted across restarts. There is no @SlingScheduled annotation in the CS SDK.
Three properties control every scheduler:
| Property | Required value | Why |
|---|
scheduler.expression | Valid Quartz cron string | Declares the trigger |
scheduler.concurrent:Boolean=false | :Boolean type hint required | Without the hint OSGi treats it as String and concurrent runs are not suppressed |
scheduler.runOn | SINGLE or LEADER for any write or external call | Default ALL fires on every publish pod simultaneously |
Classification — choose before making any changes
Path A — Runnable + OSGi component properties when ALL are true:
- Cron is a hardcoded constant or single
@AttributeDefinition-backed value
- Only one schedule per class
- Class
implements Runnable
- No
ScheduleOptions.config(), no per-execution job payload
→ Read path-a.md and follow its steps.
Path B — Sling Jobs via JobManager when ANY is true in the legacy source:
- Cron comes from runtime config (
config.cronExpression())
- Multiple cron expressions per class
- Legacy code needs per-execution job data, config-driven scheduling, or a Scheduler + JobConsumer split
- Business logic needs job context or properties at execution time
@Modified re-registers schedules with new config values
→ Read path-b.md and follow its steps.
One pattern per session. If the codebase has both kinds, fix one class at a time.
Review Checklist
Use the path-specific checklists in path-a.md and path-b.md for scheduler mechanics.
Cross-cutting checks:
Path A checks:
Path B checks:
Troubleshooting
| Symptom | Log to search | Fix direction |
|---|
| Scheduler never fires after deployment | none (silent) | Inspect the component's runtime state: if UNSATISFIED, the cause is usually a missing config or a misspelled config field name (scheduler_expression() in Path A, cronExpression() in Path B); if ACTIVE but no executions, the cron property name is misspelled or scheduler.expression is invalid |
| Fires N times per trigger (N = pod count) | none | Add scheduler.runOn=SINGLE to @Component property array |
| Two instances run simultaneously | none | Add scheduler.concurrent:Boolean=false — the :Boolean type hint is mandatory |
| Stops firing with no code change; unrelated workflows also stall | RejectedExecutionException in sling-default pool entries | Thread pool starvation — see diagnosis below |
Thread pool starvation root cause chain:
- A scheduler makes a blocking call (HTTP/LDAP/DB) without a read timeout
scheduler.concurrent missing/true → new thread per trigger
- All
ApacheSlingdefault threads stuck
block policy=ABORT silently rejects new submissions — no log, no execution
Diagnose: Developer Console → Thread Pool → ApacheSlingdefault → check active count vs max pool size and block policy.
Fix: add HTTP read timeout to the blocking scheduler; set scheduler.concurrent:Boolean=false; change block policy to RUN via OSGi config in Git.
Common Pitfalls
@SlingScheduled used — the annotation does not exist in the CS SDK; class compiles but schedule is never registered (silent failure).
scheduler.schedule() in @Activate — dynamic registration is not persisted across restarts; schedule disappears on next deployment.
Missing scheduler.runOn on a write-side scheduler — default ALL fires on every publish pod; 3 pods = 3× the JCR writes / external calls per trigger.
Static ResourceResolver field — closed by the repo after idle timeout; subsequent run() invocations throw ClosedResourceResolverException.
scheduler.concurrent=false without :Boolean type hint — OSGi treats it as String, concurrent execution is not suppressed.
Modern Alternatives
| Requirement | Use |
|---|
| Stateless periodic task, missed runs on restart OK | Sling Scheduler — Path A |
| Config-driven cron or multiple schedules | Sling Jobs — Path B |
| Task must survive instance restart | Sling Jobs (JobManager.createJob().schedule().cron()) |
| Exactly-once across entire cluster | Sling Jobs + JobConsumer |
| Automatic retry on failure | Sling Jobs (retry per queue config) |
| Per-execution parameters | Sling Jobs (JobBuilder.properties()) |
| Event-driven + time-driven | Sling Jobs (triggered by both addJob() and schedule()) |