---
title: Scheduled jobs
slug: cms-12/docs/scheduled-jobs
docTags: 
createdAt: 2026-08-10T15:56:02.000Z
---

A sample Optimizely Content Management System (CMS) installation has several predefined scheduled jobs administered in the admin view. You can customize and configure scheduled jobs and create your own.

During the initialization of CMS, the system scans through jobs and checks for their next execution time. At the appointed time, the system executes the job. Alternatively, you can execute a job manually from the admin view. Scheduled jobs are executed in an **Anonymous** context. The site's web server must be up and running because scheduled jobs are executed in the same process as the site. To ensure this, use the **Application Initialization** or have a site supervisor periodically ping the site. In Azure Web Apps, enable **Always On**.

## Built-in scheduled jobs

A standard CMS installation has a set of built-in scheduled jobs, such as emptying the trash and managing the scheduled content publishing. These jobs are available from the CMS administration view.

## Implement a scheduled job

To implement a scheduled job,

1. Create a class that inherits from the `EPiServer.Scheduler.ScheduledJobBase` base class.
2. Decorate the class with the `ScheduledPlugInAttribute` and define if the job should be enabled and what interval should be used initially.
3. You can also assign a GUID to your job, so you can change the name or namespace of the job at a later time.

**Example:** A basic schedule with the possibility to stop a job by overriding `Stop` method, and the ability to report progress through the `StatusChanged` event.

```csharp
using System;
using EPiServer.Core;
using EPiServer.PlugIn;
using EPiServer.Scheduler;

namespace MyOptimizelySite.Jobs {
  [ScheduledPlugIn(DisplayName = "ScheduledJobExample", GUID = "d6619008-3e76-4886-b3c7-9a025a0c2603")]
  public class ScheduledJobExample: ScheduledJobBase {
    private bool _stopSignaled;

    //private readonly IContentLoader _contentLoader; 

    // Here you can add dependencies to constructor, In this example if the scheduled job has dependencies then they can be injected in the constructor. Here example of the job has dependency to IContentLoader  
    // public ScheduledJobExample(IContentLoader contentLoader)
    // {
    //  _contentLoader = contentLoader;
    //  IsStoppable = true;
    // }

    public ScheduledJobExample() {
      IsStoppable = true;
    }

    /// <summary>
    /// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
    /// </summary>
    public override void Stop() {
      _stopSignaled = true;
    }

    /// <summary>
    /// Called when a scheduled job executes
    /// </summary>
    /// <returns>A status message to be stored in the database log and visible from admin mode</returns>
    public override string Execute() {
      //Call OnStatusChanged to periodically notify progress of job for manually started jobs
      OnStatusChanged(String.Format("Starting execution of {0}", this.GetType()));

      //Add implementation

      //For long running jobs periodically check if stop is signaled and if so stop execution
      if (_stopSignaled) {
        return "Stop of job was called";
      }

      return "Change to message that describes outcome of execution";
    }
  }
}
```

The example scheduled job as it displays in the admin view:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/aS6WIuXf6j5AsVV_zfwL9_fb16765-imagex5iux.png)

:::hint{type="info"}
**Note**
In CMS 11, a job could behave differently when started manually compared to it being scheduled because scheduled jobs ran in an anonymous user context and manually started jobs ran as the current user. In CMS 12, the job never runs as the current user.
:::

You can run a job with special privileges, such as a job that deletes some files.

In CMS 11, you could write:

```csharp
PrincipalInfo.CurrentPrincipal = new GenericPrincipal(
  new GenericIdentity("Some Dummy User"),
  new [] {
    "WebEditors"
  }
);
```

In CMS 12, `PrincipalInfo.CurrentPrincipal` is read-only, so you need to access the principal through the `IPrincipalAccessor` like this:

```csharp
_principalAccessor.Principal = new GenericPrincipal(
  new GenericIdentity("Some Dummy User"),
  new [] {
    "WebEditors"
  }
);
```

And included in a scheduled job, it could look like this:

```csharp
[ScheduledPlugIn(DisplayName = "My Scheduled Job")]
public class MyJob: ScheduledJobBase {
  private readonly IPrincipalAccessor _principalAccessor;

  ...
  public MyJob(IPrincipalAccessor principalAccessor) {
    _principalAccessor = principalAccessor;
  }

  public override string Execute() {
      _principalAccessor.Principal = new GenericPrincipal(
        new GenericIdentity("Some Dummy User"),
        new [] {
          "WebEditors"
        }
      );

      // Do some suff that needs WebEditor privileges.
    }
    ...
}
```

See [Scheduled Jobs in CMS 12](https://www.gulla.net/en/blog/scheduled-jobs-in-optimizely-cms-12/) – blog post by [Tomas Hensrud Gulla](https://www.gulla.net/en/blog/)

## Multi-server scenario

If several sites share a database, such as in a load-balanced scenario, you can control which site executes scheduled jobs. To do this, set the `Enabled` property to **true** on the `SchedulerOptions` on the site that should execute the jobs, and to **false** on the other sites. The `SchedulerOptions` is an option and can be configured from the `appsettings.json` file or by code.

:::hint{type="info"}
**Note**
Running the job manually ignores the **Enabled** setting and runs the job on the particular server or instance.
:::

Enable scheduler by `appsettings.json` file:

```json
{
  "EpiServer": {
    "Cms": {
      "Scheduler": {
        "Enabled": "true"
      }
    }
  }
}
```

Enable scheduler by code:

```csharp
public class Startup {
  public void ConfigureServices(IServiceCollection services) {
    services.Configure<SchedulerOptions>(o => {
      o.Enabled = true;
    });
  }
}
```

If you configure several sites to run scheduled jobs, each job is scheduled for execution on all sites. However, during execution, the first site that starts executing a job marks it in the database as executing, so the other sites do not execute that job in parallel.

## Restartable jobs

If the application crashes or is recycled when a job is running, the scheduler runs the job at the next scheduled time by default. A restartable job is started again to make sure it can run to completion. The job can restart on any available server.

Set the `Restartable` property on the `ScheduledPlugIn` attribute. The job should also be implemented so that it can be started repeatedly. For example, if the job processes data, it should be able to continue where it was aborted. You should also implement a stoppable job, but be aware that the `Stop` method is only called for controlled shutdowns that ASP.NET can pick up, not for uncontrolled shutdowns such as crashes or other external changes.

```csharp
[ScheduledPlugIn(Restartable = true)]
```

In the unlikely event that the job is repeatedly canceled or the job itself is causing the shutdowns, there are a maximum of 10 start attempts per job. The job will run on the next scheduled time when that limit is reached.

## Custom content cache expiration

Content loaded from the database and added to the cache by scheduled jobs have a shorter cache expiration (default 1 minute) because it is unlikely that the content will be used again and to keep down memory usage of long-running jobs.

You can customize the expiration that is being set on content loaded from the database using the `ContentCacheScope` class.

```csharp
using(var x = new ContentCacheScope {
  SlidingExpiration = TimeSpan.FromMinutes(10)
}) {
  // _contentLoader can be injected by constructor injection
  var content = _contentLoader.Get(contentLink)
  //etc..
}
```
