Skip to main content

How to Retrieve & Monitor Scheduled Jobs in Salesforce LWC (CronTrigger Guide)

In plain words: A Scheduled Jobs Monitor in LWC is a custom dashboard component that retrieves and displays active background cron jobs from your Salesforce org. By querying the CronTrigger and CronJobDetail objects in Apex, developers can display upcoming run times, execution states, and cron expressions directly on admin homepages or dev utilities without navigating to Salesforce Setup.

In enterprise Salesforce environments, scheduled Apex jobs handle nightly data syncs, automated batch runs, cleanup utilities, and reporting calculations. While administrators can view scheduled jobs in Salesforce Setup under Scheduled Jobs, building a dedicated Lightning Web Component allows teams to embed live job monitoring into custom DevOps consoles, monitor execution health in real time, and trigger manual abort or rescheduling routines on demand.

1. Understanding the Scheduled Job Data Model

When you schedule an Apex class using System.schedule() or the declarative UI, Salesforce stores schedule information across two connected system objects:

  • CronTrigger: Represents the active scheduled job instance. Contains metadata including CronExpression, NextFireTime, PreviousFireTime, State (such as WAITING, ACQUIRED, EXECUTING, or PAUSED), and TimesTriggered.
  • CronJobDetail: The parent record linked to CronTrigger via CronJobDetailId. It stores the human-readable job name (CronJobDetail.Name) and the job type (CronJobDetail.JobType).
360 Scheduled Job Architecture Card:
  • Underlying Entity: CronTrigger joined with CronJobDetail.
  • Data Layer: Strongly typed Apex wrapper to flatten relationship fields for LWC datatable binding.
  • Security Enforcement: WITH USER_MODE and @AuraEnabled(cacheable=true).
  • Platform Limit: Up to 100 concurrently scheduled Apex jobs per org.

2. Implementing the Apex Controller with Flattened Wrappers

Standard <lightning-datatable> components cannot read nested relationship fields (e.g., CronJobDetail.Name) directly from raw sObject records without custom formatting. Using an Apex wrapper class flattens the data structure cleanly before sending it to the client.

Step 1: Create the Apex Controller (ScheduledJobsController.cls)
public with sharing class ScheduledJobsController {

    public class JobWrapper {
        @AuraEnabled public String id { get; set; }
        @AuraEnabled public String jobName { get; set; }
        @AuraEnabled public String cronExpression { get; set; }
        @AuraEnabled public Datetime nextFireTime { get; set; }
        @AuraEnabled public Datetime previousFireTime { get; set; }
        @AuraEnabled public String state { get; set; }
        @AuraEnabled public Integer timesTriggered { get; set; }

        public JobWrapper(CronTrigger ct) {
            this.id = ct.Id;
            this.jobName = ct.CronJobDetail.Name;
            this.cronExpression = ct.CronExpression;
            this.nextFireTime = ct.NextFireTime;
            this.previousFireTime = ct.PreviousFireTime;
            this.state = ct.State;
            this.timesTriggered = ct.TimesTriggered;
        }
    }

    @AuraEnabled(cacheable=true)
    public static List<JobWrapper> getScheduledJobs() {
        List<JobWrapper> jobList = new List<JobWrapper>();

        List<CronTrigger> triggers = [
            SELECT Id, 
                   CronJobDetail.Name, 
                   CronExpression, 
                   NextFireTime, 
                   PreviousFireTime, 
                   State, 
                   TimesTriggered 
            FROM CronTrigger 
            WITH USER_MODE
            ORDER BY NextFireTime ASC NULLS LAST
            LIMIT 100
        ];

        for (CronTrigger ct : triggers) {
            jobList.add(new JobWrapper(ct));
        }

        return jobList;
    }
}

3. Building the Lightning Web Component

The LWC component consumes the wrapper data using the wire service and displays the scheduled jobs in an interactive, responsive datatable with formatted dates and refresh capabilities.

Step 2: Component HTML Template (scheduledJobsMonitor.html)
<template>
    <lightning-card title="Active Scheduled Jobs Monitor" icon-name="standard:service_appointment">
        <lightning-button-icon
            slot="actions"
            icon-name="utility:refresh"
            alternative-text="Refresh Data"
            title="Refresh Jobs"
            onclick={handleRefresh}>
        </lightning-button-icon>

        <div class="slds-p-around_medium">
            <!-- Loading State -->
            <template lwc:if={isLoading}>
                <div class="slds-is-relative slds-p-vertical_large">
                    <lightning-spinner alternative-text="Loading scheduled jobs..." size="small"></lightning-spinner>
                </div>
            </template>

            <!-- Data Table -->
            <template lwc:if={hasJobs}>
                <div class="slds-scrollable_y" style="max-height: 400px;">
                    <lightning-datatable
                        key-field="id"
                        data={jobs}
                        columns={columns}
                        hide-checkbox-column="true">
                    </lightning-datatable>
                </div>
            </template>

            <!-- Empty State -->
            <template lwc:elseif={noJobsFound}>
                <div class="slds-text-align_center slds-p-vertical_medium slds-text-color_weak">
                    No active scheduled jobs found in this organization.
                </div>
            </template>
        </div>
    </lightning-card>
</template>
Step 3: Component JavaScript Controller (scheduledJobsMonitor.js)
import { LightningElement, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getScheduledJobs from '@salesforce/apex/ScheduledJobsController.getScheduledJobs';

const COLUMNS = [
    { label: 'Job Name', fieldName: 'jobName', type: 'text', sortable: true },
    { label: 'State', fieldName: 'state', type: 'text', initialWidth: 120 },
    { label: 'Cron Pattern', fieldName: 'cronExpression', type: 'text', initialWidth: 150 },
    { 
        label: 'Next Fire Time', 
        fieldName: 'nextFireTime', 
        type: 'date',
        typeAttributes: {
            year: 'numeric',
            month: 'short',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            hour12: true
        }
    },
    { 
        label: 'Previous Fire Time', 
        fieldName: 'previousFireTime', 
        type: 'date',
        typeAttributes: {
            year: 'numeric',
            month: 'short',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            hour12: true
        }
    },
    { label: 'Runs', fieldName: 'timesTriggered', type: 'number', initialWidth: 90 }
];

export default class ScheduledJobsMonitor extends LightningElement {
    columns = COLUMNS;
    jobs = [];
    isLoading = true;
    wiredJobsResult;

    @wire(getScheduledJobs)
    wiredScheduledJobs(result) {
        this.wiredJobsResult = result;
        this.isLoading = false;
        const { data, error } = result;

        if (data) {
            this.jobs = data;
        } else if (error) {
            console.error('Error fetching scheduled jobs:', error);
            this.jobs = [];
        }
    }

    get hasJobs() {
        return !this.isLoading && this.jobs && this.jobs.length > 0;
    }

    get noJobsFound() {
        return !this.isLoading && (!this.jobs || this.jobs.length === 0);
    }

    async handleRefresh() {
        this.isLoading = true;
        await refreshApex(this.wiredJobsResult);
        this.isLoading = false;
    }
}
Step 4: Metadata Configuration (scheduledJobsMonitor.js-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

4. Common Traps & Platform Best Practices

Datatable Dot-Notation Trap: Using CronJobDetail.Name in Datatable Columns
The base <lightning-datatable> component does not support dot-notation path traversing (such as fieldName: 'CronJobDetail.Name'). Setting a nested field name directly in your column definitions leaves that table column blank. Always flatten relationship fields into top-level properties using an Apex wrapper class or JavaScript map before passing data into the datatable.
Null NextFireTime Trap: Completed or One-Time Cron Triggers
Once a one-time scheduled job finishes executing, its NextFireTime becomes null and its State transitions to DELETED or COMPLETE. Always use NULLS LAST in your SOQL order clause to ensure actively pending jobs remain prioritized at the top of the monitor.
Core Rule: Query CronTrigger joined with CronJobDetail, flatten the output via an Apex wrapper to enable clean datatable column binding, and use refreshApex() to provide one-click status refreshes without reloading the browser.
  • Implement User-Mode Security: Append WITH USER_MODE to your SOQL query to enforce object and field-level permissions.
  • Programmatic Job Cancellation: To allow administrators to cancel jobs directly from the LWC, add an action button in the datatable that passes the id to an Apex method invoking System.abortJob(jobId).
  • Format Date-Time Columns: Use type: 'date' with typeAttributes in your column definition so timestamps automatically adapt to each user's local timezone and locale format.

Summary

Building a custom Scheduled Jobs Monitor in Lightning Web Components gives administrators and developers real-time visibility into asynchronous operations. By querying CronTrigger in Apex, flattening nested properties with a structured wrapper class, and binding the results to a modern <lightning-datatable>, teams can build lightweight administrative utilities that simplify Salesforce org maintenance.