apex-scheduled-jobs

Scheduling Apex classes using the Schedulable interface: implementing execute(), cron expressions, System.schedule(), monitoring CronTrigger records, job limits, and job chaining patterns. NOT for Batch Apex scheduling (use batch-apex-patterns) or Flow scheduled paths.

Best use case

apex-scheduled-jobs is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Scheduling Apex classes using the Schedulable interface: implementing execute(), cron expressions, System.schedule(), monitoring CronTrigger records, job limits, and job chaining patterns. NOT for Batch Apex scheduling (use batch-apex-patterns) or Flow scheduled paths.

Teams using apex-scheduled-jobs should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/apex-scheduled-jobs/SKILL.md --create-dirs "https://raw.githubusercontent.com/PranavNagrecha/AwesomeSalesforceSkills/main/skills/apex/apex-scheduled-jobs/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/apex-scheduled-jobs/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How apex-scheduled-jobs Compares

Feature / Agentapex-scheduled-jobsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Scheduling Apex classes using the Schedulable interface: implementing execute(), cron expressions, System.schedule(), monitoring CronTrigger records, job limits, and job chaining patterns. NOT for Batch Apex scheduling (use batch-apex-patterns) or Flow scheduled paths.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Apex Scheduled Jobs

Use this skill when designing, implementing, reviewing, or troubleshooting Apex classes that run on a time-based schedule using the `Schedulable` interface. The skill covers the full lifecycle: authoring, scheduling via cron expression, monitoring via `CronTrigger`, aborting, rescheduling, testing, and deployment considerations.

---

## Before Starting

Gather this context before working in this domain:

- **What is the scheduling target?** Schedulable should act as the timer and dispatcher, not the data processor. Large-volume work belongs in Batch Apex or a Queueable dispatched from `execute()`.
- **What is the org's current scheduled job count?** The platform limit is 100 scheduled jobs per org (shared across all types). Approaching this limit requires an audit before adding new jobs.
- **Does the job need callouts?** Scheduled Apex cannot make callouts directly. Callouts must be delegated to a `@future(callout=true)` method or a `Queueable` implementing `Database.AllowsCallouts`.
- **Who will own the schedule post-deployment?** Scheduled jobs are not deployed by the Metadata API. They must be created manually or via Anonymous Apex after deployment — plan for this in every release.
- **Is this a sandbox refresh scenario?** Scheduled jobs from production are not copied to a sandbox on refresh. Post-refresh scheduling must be scripted or run manually.

---

## Core Concepts

### The Schedulable Interface

A class becomes schedulable by implementing the `Schedulable` interface and defining a single `global void execute(SchedulableContext SC)` method. The `global` access modifier is required; `public` alone is insufficient.

```apex
global class MyScheduledJob implements Schedulable {
    global void execute(SchedulableContext SC) {
        // Dispatch work here — avoid heavy logic inline
    }
}
```

The `SchedulableContext` object provides `getTriggerId()`, which returns the `CronTrigger` record ID for the active job. This ID can be used inside `execute()` for self-identification or logging, but it cannot be used to modify or abort the job from within the same execution.

### Cron Expressions

Salesforce uses a seven-field cron expression:

```
Seconds  Minutes  Hours  Day_of_month  Month  Day_of_week  [Year]
```

Key rules:
- Day_of_month and Day_of_week are mutually exclusive — one must always be `?`.
- Seconds must be `0` — sub-minute scheduling is not supported.
- Field values are 0-indexed for seconds/minutes/hours; months and days of week use named constants or 1-based integers.

Common patterns:

| Schedule | Cron Expression | Notes |
|---|---|---|
| Daily at 2:00 AM | `'0 0 2 * * ?'` | Runs every day |
| Weekdays at 1:00 PM | `'0 0 13 ? * MON-FRI'` | Day_of_month is `?` |
| First day of each month at midnight | `'0 0 0 1 * ?'` | Day_of_week is `?` |
| Every Sunday at 3:30 AM | `'0 30 3 ? * SUN'` | Day_of_month is `?` |
| Hourly (not recommended) | `'0 0 * * * ?'` | Consumes scheduled job slot continuously |

Always schedule during off-peak hours to reduce contention with user traffic and other async jobs.

### System.schedule() and the CronTrigger Object

A scheduled job is created with:

```apex
String jobId = System.schedule('Job Display Name', cronExpression, new MyScheduledJob());
```

- The first argument is the display name shown in Setup > Scheduled Jobs. It must be unique per org — duplicate names throw a `System.AsyncException` at runtime.
- The method returns a `CronTrigger` record ID (not an `AsyncApexJob` ID).
- Jobs can be scheduled from Anonymous Apex, a trigger, a controller, or another Apex class — but **not** from within a Schedulable's own `execute()` method to reschedule itself (use a separate post-execute mechanism if rescheduling is needed).

Monitor active jobs with:

```soql
SELECT Id, CronJobDetail.Name, CronExpression, State, NextFireTime, PreviousFireTime
FROM CronTrigger
WHERE State = 'WAITING'
ORDER BY NextFireTime ASC
```

`CronTrigger.State` values: `WAITING`, `PAUSED`, `COMPLETE`, `ERROR`, `DELETED`, `BLOCKED`.

Abort a scheduled job with:

```apex
System.abortJob('CronTrigger_Id_Here');
```

### Mode Selection

This skill operates in three modes based on the practitioner's need:

- **Mode 1 — Implement:** Design and create a new scheduled job from scratch. Follow the Schedulable-as-dispatcher pattern and produce both the class and the scheduling call.
- **Mode 2 — Review/Audit:** Evaluate existing scheduled jobs in an org — query `CronTrigger`, check proximity to the 100-job limit, verify off-peak scheduling, and confirm that heavy work is delegated rather than inlined.
- **Mode 3 — Troubleshoot:** Diagnose a job that is not running, stuck in `BLOCKED` or `ERROR` state, or failing silently. Start with `CronTrigger.State`, then check `AsyncApexJob` for the most recent execution record.

---

## Common Patterns

### Schedulable as Dispatcher (Preferred Pattern)

**When to use:** Any case where the scheduled job processes records, performs DML, or does anything beyond trivial computation. This is the default pattern.

**How it works:**
1. The `Schedulable` class holds minimal state — typically a constructor parameter for scoping (e.g., record type, org-specific flag).
2. `execute()` instantiates and dispatches a Batch Apex class or enqueues a Queueable. It does not perform DML or SOQL at scale itself.
3. The Batch or Queueable carries all governor-intensive work within its own transaction limits.

```apex
global class NightlyLeadCleanupScheduler implements Schedulable {
    global void execute(SchedulableContext SC) {
        Database.executeBatch(new LeadCleanupBatch(), 200);
    }
}
```

**Why not the alternative:** Performing large SOQL queries or DML directly in `execute()` burns the Schedulable's governor limits (which are the same full limits as any Apex transaction). If the data volume grows, the job silently fails at governor limit boundaries with no automatic retry.

### Self-Identifying Job for Conditional Logic

**When to use:** A single Schedulable class is reused for multiple schedules with different parameters, and the job needs to know which schedule triggered it.

**How it works:**
1. The `SchedulableContext.getTriggerId()` returns the `CronTrigger.Id`.
2. Query `CronJobDetail` from `CronTrigger` to retrieve the job name and branch logic accordingly.

```apex
global class MultiPurposeScheduler implements Schedulable {
    global void execute(SchedulableContext SC) {
        CronTrigger ct = [
            SELECT CronJobDetail.Name
            FROM CronTrigger
            WHERE Id = :SC.getTriggerId()
        ];
        if (ct.CronJobDetail.Name.contains('Nightly')) {
            Database.executeBatch(new NightlyBatch(), 200);
        } else {
            System.enqueueJob(new WeeklyReportQueueable());
        }
    }
}
```

**Why not the alternative:** Creating one Schedulable class per variation leads to class sprawl. A parameterized or self-identifying class reduces maintenance surface.

### Abort-and-Reschedule for Schedule Changes

**When to use:** The cron schedule for a live job needs to change. There is no in-place modification API.

**How it works:**
1. Query `CronTrigger` to find the job ID by name.
2. Call `System.abortJob(jobId)`.
3. Call `System.schedule(sameName, newCronExpression, new MyScheduledJob())`.

```apex
// Run from Anonymous Apex or a deployment script
List<CronTrigger> jobs = [
    SELECT Id FROM CronTrigger
    WHERE CronJobDetail.Name = 'Nightly Lead Cleanup'
    AND State = 'WAITING'
];
if (!jobs.isEmpty()) {
    System.abortJob(jobs[0].Id);
}
System.schedule('Nightly Lead Cleanup', '0 0 3 * * ?', new NightlyLeadCleanupScheduler());
```

**Why not the alternative:** Attempting to update `CronExpression` directly on the `CronTrigger` record via DML is not supported. The only path to change a schedule is abort and reschedule.

---

## Decision Guidance

| Situation | Recommended Approach | Reason |
|---|---|---|
| Need to run Apex on a fixed time-based schedule | `Schedulable` + `System.schedule()` | Purpose-built platform feature for time-based Apex |
| Job processes large record volumes | Schedulable dispatches Batch Apex | Batch provides chunked limits and automatic retry |
| Job needs to make outbound callouts | Schedulable enqueues a `Queueable` with `AllowsCallouts` | Direct callouts from Scheduled Apex are not supported |
| Job schedule needs to change post-deployment | Abort existing job, schedule new one with updated cron | No in-place schedule modification API exists |
| Org is near the 100-job limit | Consolidate into a master scheduler class | Single scheduled job dispatches multiple logical tasks |
| Job runs in multiple sandboxes or prod | Plan post-deployment Anonymous Apex script | Scheduled jobs are not Metadata API-deployable |
| Need sub-hourly scheduling | Re-evaluate: consider Platform Events or streaming | Salesforce does not support sub-minute scheduling |
| Job errors silently and stops | Query `CronTrigger.State = 'ERROR'`, check `AsyncApexJob` | State field and job history show failure details |

---


## Recommended Workflow

Step-by-step instructions for an AI agent or practitioner activating this skill:

1. Gather context — confirm the org edition, relevant objects, and current configuration state
2. Review official sources — check the references in this skill's well-architected.md before making changes
3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
4. Validate — run the skill's checker script and verify against the Review Checklist below
5. Document — record any deviations from standard patterns and update the template if needed

---

## Review Checklist

Run through these before marking scheduled job work complete:

- [ ] Class declares `global` (not `public`) access and implements `Schedulable`.
- [ ] `execute(SchedulableContext SC)` signature is correct — method is `global void`.
- [ ] Heavy work (SOQL at scale, DML at scale) is delegated to Batch or Queueable, not done inline.
- [ ] Cron expression is valid: seconds field is `0`, Day_of_month and Day_of_week are mutually exclusive (`?`).
- [ ] Job name is unique in the org — duplicate names cause a runtime `AsyncException`.
- [ ] Job is scheduled during off-peak hours.
- [ ] Org is below 100 scheduled jobs — if close, a consolidation plan exists.
- [ ] No direct callouts in `execute()` — callouts are delegated to async contexts.
- [ ] Post-deployment scheduling script (Anonymous Apex) is prepared and documented.
- [ ] Test class uses `Test.startTest()` / `Test.stopTest()` and verifies `CronTrigger` state after scheduling.
- [ ] Sandbox refresh impact is understood and post-refresh scheduling is scripted.

---

## Salesforce-Specific Gotchas

1. **Scheduled jobs are not deployed by the Metadata API** — The Schedulable class deploys, but the active job record in `CronTrigger` does not. Every release that changes or introduces a scheduled job requires a post-deployment step (Anonymous Apex or a post-install script) to create or recreate the schedule.

2. **The 100-job limit is org-wide and shared** — All scheduled Apex jobs, scheduled flows, and scheduled reports count against the same 100-job org limit. Orgs with many automations can exhaust this silently; new `System.schedule()` calls throw a `System.AsyncException` when the limit is reached.

3. **Direct callouts from `execute()` are blocked at runtime** — A Schedulable that attempts an HTTP callout throws `System.CalloutException: Callout from scheduled Apex not supported`. This must be delegated to a `@future(callout=true)` or Queueable with `AllowsCallouts`.

4. **You cannot reschedule a job from within its own `execute()` method** — Calling `System.schedule()` inside `execute()` throws a runtime exception. If a job needs to compute its next run time dynamically, use a Queueable dispatched from `execute()` to do the rescheduling.

5. **Sandbox refreshes clear all scheduled jobs** — Scheduled jobs from production are not carried to a refreshed sandbox. Teams that rely on scheduled jobs in sandboxes for testing or integration must re-create them after each refresh.

---

## Output Artifacts

| Artifact | Description |
|---|---|
| Schedulable class implementation | Correctly structured class with `global` modifier and `execute(SchedulableContext SC)` |
| Cron expression | Validated expression matching the target schedule with Day_of_month / Day_of_week handling |
| System.schedule() call | Ready-to-run statement for Anonymous Apex or post-deployment script |
| CronTrigger monitoring query | SOQL to surface active jobs, state, and next fire time |
| Abort-and-reschedule script | Anonymous Apex pattern to change an existing job's schedule |
| Test class scaffold | Unit test with `Test.startTest()` / `Test.stopTest()` and CronTrigger assertion |

---

## Related Skills

- `apex/async-apex` — use when the question is which async mechanism to choose (Schedulable vs Queueable vs Batch vs future).
- `apex/batch-apex-patterns` — use when the scheduled job needs to process large data volumes with chunked limits.
- `apex/apex-queueable-patterns` — use when the scheduled job needs to make callouts or chain async steps.
- `apex/governor-limits` — use when the scheduled job is hitting CPU, SOQL, or DML limits during execution.
- `apex/debug-and-logging` — use when diagnosing why a scheduled job failed or produced unexpected results.

Related Skills

apex-managed-sharing-patterns

8
from PranavNagrecha/AwesomeSalesforceSkills

Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.

lwc-imperative-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).

scheduled-erp-sync-pattern

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when designing a recurring (15-minute / hourly / nightly) data exchange between Salesforce and an external ERP system (Oracle EBS, SAP, NetSuite, Workday, Dynamics, etc.) where Salesforce is the *initiator* and pulls or pushes deltas on a schedule. Covers the full pattern: scheduled Apex → Queueable callout chain → REST request to ERP → upsert into a staging custom object → downstream reconciliation; plus watermark management (timestamp / cursor / full-refresh modes), idempotency via External ID, retry with exponential backoff, dead-letter custom object, and the volume thresholds that should redirect you to Bulk API 2.0, Change Data Capture, or MuleSoft. Triggers: 'integration to oracle erp every 15 minutes', 'scheduled sync pattern enterprise erp', 'pull netsuite invoices into salesforce nightly', 'apex schedulable callout to sap', 'how do i sync salesforce contacts to workday hourly', 'design a polling integration to my erp'. NOT for one-shot ETL imports (use data/data-loader-bulk-api), NOT for real-time inbound from ERP via Platform Events / Pub-Sub API (use integration/platform-events-publish-subscribe), NOT for outbound *event-driven* push (use integration/change-data-capture-consumer-pattern), NOT for MuleSoft / iPaaS architecture decisions (use architect/mulesoft-vs-native-integration-decision). When the data volume routinely exceeds 10K records per cycle or sub-minute latency is required, explicitly route to the Streaming / CDC / iPaaS skills instead.

dataweave-for-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).

scheduled-flows

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when designing or reviewing schedule-triggered flows for recurring automation, replacement of time-based workflow patterns, bounded record selection, idempotent processing, and escalation to Apex when volume is too high. Triggers: 'scheduled flow design', 'nightly flow job', 'time based workflow replacement', 'schedule triggered flow limits'. NOT for record-triggered scheduled paths or large-scale batch processing that should be built directly in Batch Apex.

scheduled-flow-not-running-debug

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when a Schedule-Triggered Flow is configured but is not firing at the expected time, or appears active in Setup → Flows but never produces output. Covers where scheduled flows actually surface (Setup → Scheduled Jobs, NOT Setup → Apex Jobs), the AsyncApexJob / CronTrigger evidence trail, top causes (deactivated scheduling user, daylight-savings transitions, time-zone mismatches between scheduling user and org, fault-paths that quietly stop the schedule, daily async-Apex limit pressure), and recovery steps (re-schedule via Apex, run the flow manually with the same input, switch the scheduling user). Triggers: 'how do I schedule a flow to run every monday', 'scheduled flow not firing', 'flow scheduled but no execution', 'scheduled flow stopped working last week', 'monday 6am scheduled flow did not run after dst change'. NOT for designing a new scheduled flow's record scope or idempotency (use flow/scheduled-flows), NOT for record-triggered Scheduled Paths that don't fire (use flow/flow-time-based-patterns), NOT for general Batch Apex job monitoring (use admin/batch-job-scheduling-and-monitoring).

flow-invocable-from-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).

flow-apex-defined-types

8
from PranavNagrecha/AwesomeSalesforceSkills

Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.

scheduled-apex-failure-detection-and-monitoring

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when nightly batch / scheduled Apex jobs are failing without anyone noticing — covers why uncaught exceptions in `execute()` go to the debug log instead of email, how to query `AsyncApexJob` for `Status`, `NumberOfErrors`, and `ExtendedStatus`, when to implement `Database.RaisesPlatformEvents` so the platform publishes `BatchApexErrorEvent` on uncaught failures, how to subscribe to that event with an Apex trigger and notify operators, and how to layer a custom watcher schedule on top so silent-failure modes (job that never started, scheduled class deleted, queue stuck on `Queued`) still surface. Triggers: 'nightly batch failed at 2am with no notification', 'how do we know if a scheduled apex job is failing', 'BatchApexErrorEvent vs custom retry logic', 'Setup Apex Jobs only shows last 7 days, where else can I look', 'job is stuck in queued status nobody noticed for a week'. NOT for general Apex exception handling patterns (use apex/apex-exception-handling-and-logging), NOT for Batch Apex authoring or chunking strategy (use apex/batch-apex-design), NOT for Setup → Apex Jobs UI walkthrough as an admin task (use admin/batch-job-scheduling-and-monitoring), NOT for retry logic itself (use apex/scheduled-apex-retry-patterns once authored).

platform-events-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when publishing or subscribing to Salesforce Platform Events from Apex, comparing Platform Events with Change Data Capture, or designing event-triggered error handling and monitoring. Triggers: 'EventBus.publish', 'platform event trigger', 'CDC vs Platform Events', 'replay ID', 'high-volume event'. NOT for Flow-only publish/subscribe automation.

health-cloud-apex-extensions

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when extending Health Cloud via Apex: implementing HealthCloudGA managed-package interfaces, automating care plan lifecycle hooks, processing referrals using Industries Common Components invocable actions, or enforcing HIPAA-compliant logging governance for clinical Apex code. Trigger keywords: CarePlanProcessorCallback, HealthCloudGA namespace, ReferralRequest, ReferralResponse, care plan invocable actions, clinical Apex extension, Health Cloud Apex API. NOT for standard Apex triggers or generic Apex development unrelated to Health Cloud managed-package extension points.

fsl-apex-extensions

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when writing Apex that calls Field Service Lightning scheduling APIs — AppointmentBookingService, ScheduleService, GradeSlotsService, or OAAS — to book, schedule, grade, or optimize service appointments programmatically. Trigger keywords: FSL Apex namespace, GetSlots, schedule service appointment via code, appointment booking API, FSL optimization API. NOT for standard Apex patterns unrelated to FSL, admin-level scheduling policy configuration, or declarative FSL scheduling.