🧩 Framework Components
Scheduled Jobs Dev
📖 Docs

Scheduled Jobs - Optimizely CMS 12

Exam Area: Content Area 4 – Framework Components (15%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970328 | /4970355
Published: Feb 17, 2026


Overview (Academy Key Points)

  • Mechanisms: Background processing and content lifecycle hooks.
  • Platform: Built on .NET Dependency Injection in CMS 12.
  • Architecture: Master scheduled jobs and IContentEvents for scalability.
  • Constructor Injection is fully supported in CMS 12 Scheduled Jobs.

1. What Are Scheduled Jobs?

Scheduled Jobs are tasks that run automatically on a schedule within the CMS. They run outside the standard web request in an anonymous context. Common uses include:


2. ScheduledJobBase – Foundation (Academy – 4970328)

The class must inherit from EPiServer.Scheduler.ScheduledJobBase and be decorated with the [ScheduledPlugIn] attribute. This attribute registers the job in the CMS database and makes it manageable via the Admin UI.

[ScheduledPlugIn(
    DisplayName = "Archive Expired Content",
    GUID = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d")]
public class ContentArchivingJob : ScheduledJobBase
{
    private bool _stopSignaled;
    private readonly IContentRepository _repository;

    // CMS 12 supports full constructor injection in scheduled jobs
    public ContentArchivingJob(IContentRepository repository)
    {
        _repository = repository;
        IsStoppable = true;
    }

    public override string Execute()
    {
        OnStatusChanged("Starting content archival...");
        
        // Logical processing loop
        foreach (var item in GetItemsToArchive())
        {
            if (_stopSignaled) return "Job stopped manually by administrator.";
            // Archive logic here...
        }

        return "Archival completed successfully.";
    }

    // Override Stop() for graceful termination — required with large datasets
    public override void Stop()
    {
        _stopSignaled = true;
    }
}

Key methods:


3. ScheduledPlugIn Attribute

[ScheduledPlugIn(
    DisplayName = "My Job",              // Display name in Admin
    Description = "Job description",     // Description
    GUID = "unique-guid-here",           // Unique GUID — do NOT change after deploy
    DefaultIntervalType = ScheduledIntervalType.Hours,
    DefaultInterval = 24,                // Run every 24 hours
    Restartable = true,                  // Resume if app restarts (requires idempotent logic)
    SortIndex = 100                      // Order in the Admin list
)]

ScheduledIntervalType:

ScheduledIntervalType.Minutes | Hours | Days | Weeks | Months | Never

4. Dependency Injection in Scheduled Jobs

[ScheduledPlugIn(
    DisplayName = "Content Cleanup Job",
    GUID = "AAAABBBB-1234-5678-CCCC-000000000001")]
public class ContentCleanupJob : ScheduledJobBase
{
    private readonly IContentRepository _contentRepository;
    private readonly ILogger<ContentCleanupJob> _logger;

    // Constructor Injection (CMS 12 — fully supported)
    public ContentCleanupJob(
        IContentRepository contentRepository,
        ILogger<ContentCleanupJob> logger)
    {
        _contentRepository = contentRepository;
        _logger = logger;
        IsStoppable = true;
    }

    public override string Execute()
    {
        _logger.LogInformation("Content cleanup job started");
        var deletedCount = CleanupExpiredContent();
        return $"Cleaned up {deletedCount} expired content items.";
    }
}

5. PaaS Governance – Jobs with Elevated Permissions (Academy – 4970328)

Jobs run under an anonymous context by default. If an operation requires a specific role (e.g., "WebAdmins"), the developer must use IPrincipalAccessor to assign a security principal inside Execute().

public class ContentSyncJob : ScheduledJobBase
{
    private readonly IPrincipalAccessor _principalAccessor;
    private readonly IContentRepository _contentRepository;

    public ContentSyncJob(IPrincipalAccessor principalAccessor, IContentRepository contentRepository)
    {
        _principalAccessor = principalAccessor;
        _contentRepository = contentRepository;
        IsStoppable = true;
    }

    public override string Execute()
    {
        var originalPrincipal = _principalAccessor.Principal;
        try
        {
            // Elevate permissions for this job
            _principalAccessor.Principal = new GenericPrincipal(
                new GenericIdentity("ScheduledJobUser"),
                new[] { "WebAdmins", "WebEditors" }
            );

            // Do work that requires elevated permissions
            // ...
        }
        finally
        {
            _principalAccessor.Principal = originalPrincipal;
        }
        
        return "Sync completed.";
    }
}

Multi-instance environments: The Optimizely scheduler ensures only one instance executes a given job at a time. Logic must be idempotent to handle server restarts.


6. Content Events – IContentEvents (Academy – 4970328)

Content events allow you to hook into state changes of content items (pages, blocks, media) — the primary method for content governance and triggering external integrations.

Event Registration in IInitializableModule:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class EventInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var events = context.Locate.Advanced.GetInstance<IContentEvents>();
        events.SavingContent += OnSavingContent;
        events.PublishedContent += OnPublishedContent;
    }

    public void Uninitialize(InitializationEngine context)
    {
        var events = context.Locate.Advanced.GetInstance<IContentEvents>();
        events.SavingContent -= OnSavingContent;       // ⚠️ IMPORTANT: Must unsubscribe to avoid memory leaks
        events.PublishedContent -= OnPublishedContent;
    }

    private void OnSavingContent(object sender, ContentEventArgs e)
    {
        // Enforce business rules
        if (e.Content is ArticlePage article && string.IsNullOrEmpty(article.MetaDescription))
        {
            e.CancelAction = true;
            e.CancelReason = "Meta Description is required for SEO compliance.";
        }
    }

    private void OnPublishedContent(object sender, ContentEventArgs e)
    {
        // Trigger external integrations (e.g. Optimizely Graph sync)
        // Use async/background queue to avoid blocking the request thread
    }
}

7. Strategic Usage – Event Table (Academy – 4970328)

EventTimingUse Case
SavingContentPre-persistenceData validation and blocking illegal saves
SavedContentPost-persistenceTriggering local cache invalidations
PublishingContentPre-approvalFinal business logic checks before going live
PublishedContentPost-liveExternal API triggers (e.g. Optimizely Graph sync)

8. Performance Best Practices (Academy – 4970328)


9. Programmatic Job Management

public class JobManagementService
{
    private readonly IScheduledJobRepository _jobRepository;
    private readonly IScheduledJobExecutor _jobExecutor;

    public JobManagementService(IScheduledJobRepository jobRepository, IScheduledJobExecutor jobExecutor)
    {
        _jobRepository = jobRepository;
        _jobExecutor = jobExecutor;
    }

    public async Task RunJobAsync(Guid jobGuid)
    {
        var job = _jobRepository.Get(jobGuid);
        await _jobExecutor.StartAsync(job, new JobExecutionOptions
        {
            Trigger = ScheduledJobTrigger.User
        });
    }

    public void EnableJob(Guid jobId)
    {
        var job = _jobRepository.Get(jobId);
        job.IsEnabled = true;
        _jobRepository.Save(job);
    }
}

Review Questions

  1. Which attribute marks a Scheduled Job? ([ScheduledPlugIn])
  2. Which class does a job inherit from? (ScheduledJobBase)
  3. Which method is overridden to execute the job? (Execute() — returns a string displayed in the Status column)
  4. How can a job be stopped mid-execution? (IsStoppable = true, override Stop(), check _stopSignaled)
  5. What user context does a Scheduled Job run under by default? (Anonymous — must use IPrincipalAccessor if elevated permissions are needed)
  6. Why must events be unsubscribed in Uninitialize()? (To avoid memory leaks from dangling delegates)
  7. Which event is used to validate content before saving? (SavingContent — set e.CancelAction = true to block)
  8. What does Restartable = true in [ScheduledPlugIn] mean? (The job can resume if the app restarts — requires idempotent logic)