Day 16 - Schedulers In AEM

Howdy fellow devs 👋. Welcome to day 16 of this AEM Developer Series. Today, we are going to delve into the nitty gritty details of Schedulers in AEM.

What are schedulers, you ask 🤔? In simple terms, a scheduler is a service to schedule jobs. Thus, if we want some work to happen at a particular time or at a regular interval, then Schedulers are the way to go.

In AEM, we can create scheduler in two ways -

  1. Whiteboard Pattern - In this, we create a Runnable thread to perform our task. This is similar to the Java Thread concept.
  2. Scheduler API - In this, we use Apache Commons’ Scheduler API to perform our task. It uses open-source Quartz library.

Both of these methods can be seen here. However, from AEM 6.2 onwards Felix SCR annotations are deprecated and are replaced with OSGi annotations. Therefore, in this post, we will be discussing the clean way of creating schedulers that OSGi annotations provide.

Scheduler in AEM

To create a scheduler in AEM, we will follow the below steps -

  1. Create an OSGi configuration to read the scheduler specific values from the user i.e. cron expression, the name of the scheduler, custom parameter etc.
  2. Create a sling scheduler which displays the custom parameter at an interval specified by the cron expression.

Code Example

Let’s suppose your organization wants a report comprising workflow status and this report needs to be sent to a particular group of users via email every four hours. How will you achieve this? If you thought, I’d use a scheduler, then KUDOS to you. If you were not able to figure it out, then my sympathies are with you (Just Kidding 😄).

Enough chit chat, let’s dive into code and create our first scheduler.

  1. Navigate to core module in your AEM multi module project and create one interface named org.redquark.aem.tutorials.core.configs.WorkflowStatusConfiguration and paste the following code in it -
package org.redquark.aem.tutorials.core.configs;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

import static org.redquark.aem.tutorials.core.configs.WorkflowStatusConfiguration.CONFIGURATION_DESCRIPTION;
import static org.redquark.aem.tutorials.core.configs.WorkflowStatusConfiguration.CONFIGURATION_NAME;

        name = CONFIGURATION_NAME,
public @interface WorkflowStatusConfiguration {

    String CONFIGURATION_NAME = "Workflow Status Configuration";
    String CONFIGURATION_DESCRIPTION = "This configuration captures the details for getting workflow status and sending email";

            name = "Scheduler Name",
            description = "Enter a unique identifier that represents name of the scheduler",
            type = AttributeType.STRING
    String schedulerName() default CONFIGURATION_NAME;

            name = "Enabled",
            description = "Check the box to enable the scheduler",
            type = AttributeType.BOOLEAN
    boolean enabled() default false;

            name = "Cron Expression",
            description = "Cron expression which will decide how the scheduler will run",
            type = AttributeType.STRING
    String cronExpression() default "0 * * * * ?";

            name = "To Email",
            description = "Enter the email address of recipient in TO field",
            type = AttributeType.STRING
    String toEmail() default DEFAULT_EMAIL_ADDRESS;

            name = "To Email",
            description = "Enter the email address of recipient in CC field",
            type = AttributeType.STRING
    String ccEmail() default DEFAULT_EMAIL_ADDRESS;

            name = "From Email",
            description = "Enter the email addresses of the sender",
            type = AttributeType.STRING
    String fromEmail() default DEFAULT_EMAIL_ADDRESS;

            name = "Subject",
            description = "Enter the subject of the email",
            type = AttributeType.STRING
    String subject() default CONFIGURATION_NAME;

This is nothing but an OSGi configuration (to know more about them, revisit Day 11 = Custom OSGi Configuration).

In this configuration, we are capturing the following fields -

1. Scheduler Name --> this will be used to uniquely define a scheduler
2. Enabled --> this flag, if set true, activates the scheduler
3. Cron Expression --> This expression will define when and how the scheduler will be executed
4. To Email --> email address of recipient in "to" of email
5. Cc Email --> email address of recipient in "cc" of email
6. From Email --> email address of the sender
7. Subject --> subject line of the email

We have also provided the default values of each field which will be taken if no custom field is defined.

  1. Once the configuration is defined, we will now write our scheduler which will make use of above configuration. Create a new class named org.redquark.aem.tutorials.core.schedulers.WorkflowStatusScheduler and paste the following code in it -
package org.redquark.aem.tutorials.core.schedulers;

import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.redquark.aem.tutorials.core.configs.WorkflowStatusConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import java.time.LocalDateTime;

import static org.redquark.aem.tutorials.core.constants.AppConstants.EQUALS;
import static org.redquark.aem.tutorials.core.constants.AppConstants.NEW_LINE;
import static org.redquark.aem.tutorials.core.schedulers.WorkflowStatusScheduler.NAME;

        immediate = true,
        service = Runnable.class,
        property = {
                Constants.SERVICE_ID + EQUALS + NAME
@Designate(ocd = WorkflowStatusConfiguration.class)
public class WorkflowStatusScheduler implements Runnable {

    protected static final String NAME = "Workflow Status Scheduler";

    private static final String TAG = WorkflowStatusScheduler.class.getSimpleName();
    private static final Logger LOGGER = LoggerFactory.getLogger(WorkflowStatusScheduler.class);
    Scheduler scheduler;
    WorkflowService workflowService;
    ResourceResolverService resourceResolverService;
    EmailService emailService;
    private String schedulerName;
    private String toEmail;
    private String ccEmail;
    private String fromEmail;
    private String subject;

    protected void activate(WorkflowStatusConfiguration configuration) {
        LOGGER.debug("{}: initializing properties for scheduler", TAG);
        this.schedulerName = configuration.schedulerName();
        LOGGER.debug("{}: name of the scheduler: {}", TAG, schedulerName);
        // Details for email
        this.toEmail = configuration.toEmail();
        this.ccEmail = configuration.ccEmail();
        this.fromEmail = configuration.fromEmail();
        this.subject = configuration.subject();

    protected void modified(WorkflowStatusConfiguration configuration) {"{}: modifying scheduler with name: {}", TAG, schedulerName);
        // Remove the scheduler registered with old configuration
        // Add the scheduler registered with new configuration

    protected void deactivate(WorkflowStatusConfiguration configuration) {
        LOGGER.debug("{}: removing scheduler: {}", TAG, schedulerName);

    private void addScheduler(WorkflowStatusConfiguration configuration) {
        // Check if the scheduler has enable flag set to true
        if (configuration.enabled()) {
  "{}: scheduler: {} is enabled", TAG, schedulerName);
            // Configure the scheduler to use cron expression and some other properties
            ScheduleOptions scheduleOptions = scheduler.EXPR(configuration.cronExpression());
            // Scheduling the job
            scheduler.schedule(this, scheduleOptions);
  "{}: scheduler {} is added", TAG, schedulerName);
        } else {
  "{}: scheduler {} is disabled", TAG, schedulerName);

    private void removeScheduler(WorkflowStatusConfiguration configuration) {"{}: removing scheduler {}", TAG, schedulerName);

    private String getWorkflowStatus() {
        // This string will store the status for all workflows and other data
        StringBuilder workflowDetails = new StringBuilder();
        try {
            // Get the JCR session
            Session session = resourceResolverService.getResourceResolver().adaptTo(Session.class);
            // Get the workflow session
            WorkflowSession workflowSession = workflowService.getWorkflowSession(session);
            // States by which we want to query the workflows
            String[] states = {"RUNNING", "COMPLETED"};
            // Get the list of all the workflows by states
            Workflow[] workflows = workflowSession.getWorkflows(states);
            // Loop through all the workflows
            for (Workflow workflow : workflows) {
                        .append("ID: ")
                        .append("Payload: ")
                        .append("State: ")
        } catch (WorkflowException e) {
            LOGGER.error("{}: exception occurred: {}", TAG, e.getMessage());
        return workflowDetails.toString();

    public void run() {
        // Getting the workflow status
        String workflowStatus = getWorkflowStatus();
        // Make the content ready
        String content = "Hi, " + NEW_LINE + "Following is the workflow status at: " + + NEW_LINE + workflowStatus;
        // Send emails
        emailService.sendEmail(toEmail, ccEmail, fromEmail, subject, content);"{}: workflow status email is sent", TAG);

Let us understand the code step-by-step.

(i) First, we are registering the class as a service and implementing the java.lang.Runnable interface.

(ii) At the same time, using @Designate annotation, we are linking the OSGi configuration created in the previous section with this class.

(iii) Now, we are injecting the dependency.

(iv) In the activate() method, we are reading the required values. Then we are getting the various values from the configuration.

(v) The modified() method first removes the scheduler registered with the old configuration and then registers it with the new configuration in case the OSGi configuration is modified.

(vi) In the addScheduler() method, we are registering the scheduler using the Scheduler API.

(vii) The run() method will be defining our task. Here, we getting the workflow status from the workflow API and sending the email using our custom defined EmailService.

  1. Create EmailService and its implementation EmailServiceImpl as follows -

public interface EmailService {

    void sendEmail(
            String toEmail,
            String ccEmail,
            String fromEmail,
            String subject,
            String content

import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.redquark.aem.tutorials.core.constants.AppConstants.EQUALS;
import static;

        service = EmailService.class,
        property = {
                Constants.SERVICE_ID + EQUALS + SERVICE_NAME
public class EmailServiceImpl implements EmailService {

    protected static final String SERVICE_NAME = "Email Service";
    private static final String TAG = EmailServiceImpl.class.getSimpleName();
    private static final Logger LOGGER = LoggerFactory.getLogger(EmailServiceImpl.class);

    MessageGatewayService messageGatewayService;

    public void sendEmail(String toEmail, String ccEmail, String fromEmail, String subject, String content) {
        try {
            // Setting up the email message
            Email email = new SimpleEmail();
            // Get the details to send email
            // Inject the message gateway service and send email
            MessageGateway<Email> messageGateway = messageGatewayService.getGateway(Email.class);
            // Send the email
        } catch (EmailException e) {
            LOGGER.error("{}: exception occurred: {}", TAG, e.getMessage());

This is a simple service which sends and email using MessageGatewayService API. Nothing rocket science here 😄.

  1. Deploy the code using maven and navigate to http://localhost:4502/system/console/configMgr and search for the string “Workflow Status Configuration” and open it. Fill out the fields and save. Below is the screenshot of my configuration -
AEM Workflow Status Configuration
  1. Since we are sending email, we need an SMTP server for it. Hmm, where would you get your own SMTP server? Worry not, Google got you covered. They provide their SMTP server for testing to anyone who has a GMail account. Great, huh 😃!. Below are the details -
* Outgoing Mail (SMTP) Server:
* Use Authentication: Yes
* Use Secure Connection: Yes (TLS or SSL depending on your mail client/website SMTP plugin)
* Username: your GMail account (e.g.
* Password: your GMail password
* Port: 465 (SSL required) or 587 (TLS required)

We just need to configure these details in AEM. In http://localhost:4502/system/console/configMgr, search for the string “Day CQ Mail Service” and add the above details. Below is the screenshot of my configuration -

AEM Day CQ Mail Service
  1. Now, wait for one minute, you will see an email in your inbox with the details of the workflow status. (You might get an exception because Google doesn’t allow less secure apps to send email to your GMail account. To troubleshoot this, see Less secure apps & your Google Account).


And that’s it! Phew 😩 quite a long post. But congratulations!! 🙋 we have successfully created a Sling Scheduler and I hope you enjoyed this post.

I would love to hear your thoughts on this and would like to have suggestions from you to make it better.

As usual, you can find the complete code on my GitHub. If you find it useful, consider giving it a star ⭐.

Happy Learning 😊 and Namaste 🙏.

Created and maintained by@Anirudh Sharma
I love to learn and share. Hence, this site has no ads, no affiliation links, or any BS. If you like what you see, give me a thumbs up.

GitHub iconMedium iconTwitter iconFacebook iconLinkedIn iconStackoverflow icon