Develop Custom Scheduled Jobs
Why develop scheduled jobs
Section titled βWhy develop scheduled jobsβMany CMS operations need to run periodically without manual intervention β cleaning up expired content, syncing data from external systems, generating reports, or rebuilding search indexes. Scheduled jobs run in the background on a configurable schedule and report their status in the admin UI.
What you will do
Section titled βWhat you will doβ- Create a class that inherits from
ScheduledJobBase - Implement the
Executemethod with your business logic - Support cancellation for long-running jobs
- Configure the schedule in the CMS admin
Create a basic scheduled job
Section titled βCreate a basic scheduled jobβA scheduled job that cleans expired content
using Optimizely.Cms.Core;
using Optimizely.Cms.Core.Repositories;
using Optimizely.Cms.Core.ScheduledJobs;
namespace MySite.ScheduledJobs;
[ScheduledPlugIn(
DisplayName = "Clean Expired Content",
Description = "Removes content past its expiration date",
GUID = "c1d2e3f4-a5b6-7890-cdef-112233445566")]
public class CleanExpiredContentJob : ScheduledJobBase
{
private readonly IContentRepository _contentRepo;
private bool _stopSignaled;
public CleanExpiredContentJob(
IContentRepository contentRepo)
{
_contentRepo = contentRepo;
IsStoppable = true;
}
public override string Execute()
{
var removedCount = 0;
var descendants = _contentRepo
.GetDescendents(ContentReference.RootPage);
foreach (var contentLink in descendants)
{
if (_stopSignaled)
return $"Job stopped. Removed {removedCount} items.";
var content = _contentRepo.Get<IContent>(
contentLink);
if (content is IVersionable versionable &&
versionable.StopPublish < DateTime.UtcNow)
{
_contentRepo.Delete(
contentLink, forceDelete: true);
removedCount++;
}
}
return $"Completed. Removed {removedCount} expired items.";
}
public override void Stop()
{
_stopSignaled = true;
}
} Key points:
- The
[ScheduledPlugIn]attribute registers the job with the CMS. The GUID must be unique and stable across deployments. Executereturns a string that displays as the job status message in admin.- Set
IsStoppable = trueand implementStop()so administrators can cancel long-running jobs.
Inject dependencies
Section titled βInject dependenciesβScheduled jobs support constructor injection. The CMS resolves dependencies from the service container.
Job with injected services
using Optimizely.Cms.Core.ScheduledJobs;
using Microsoft.Extensions.Logging;
namespace MySite.ScheduledJobs;
[ScheduledPlugIn(
DisplayName = "Sync External Data",
Description = "Pulls product data from external API",
GUID = "d2e3f4a5-b6c7-8901-defa-223344556677")]
public class ExternalDataSyncJob : ScheduledJobBase
{
private readonly IExternalApiClient _apiClient;
private readonly IContentRepository _contentRepo;
private readonly ILogger<ExternalDataSyncJob> _logger;
public ExternalDataSyncJob(
IExternalApiClient apiClient,
IContentRepository contentRepo,
ILogger<ExternalDataSyncJob> logger)
{
_apiClient = apiClient;
_contentRepo = contentRepo;
_logger = logger;
}
public override string Execute()
{
_logger.LogInformation("Starting external data sync");
var products = _apiClient.GetProducts();
var synced = 0;
foreach (var product in products)
{
SyncProduct(product);
synced++;
OnStatusChanged(
$"Synced {synced} of {products.Count}");
}
_logger.LogInformation(
"Sync complete: {Count} products", synced);
return $"Synced {synced} products successfully.";
}
private void SyncProduct(ProductDto product)
{
// Create or update content from external data
}
} Call OnStatusChanged during execution to update the progress message visible in the admin UI.
Report progress
Section titled βReport progressβFor jobs that process many items, update the status regularly so administrators can monitor progress.
Progress reporting
public override string Execute()
{
var items = GetItemsToProcess();
var total = items.Count;
var processed = 0;
foreach (var item in items)
{
if (_stopSignaled)
return $"Stopped at {processed}/{total}.";
ProcessItem(item);
processed++;
if (processed % 50 == 0)
{
OnStatusChanged(
$"Processing: {processed}/{total} " +
$"({processed * 100 / total}%%)");
}
}
return $"Done. Processed {total} items.";
} Configure the schedule
Section titled βConfigure the scheduleβAfter deploying, configure the job in the CMS admin:
- Navigate to Admin > Scheduled Jobs
- Find your job by its display name
- Set the schedule interval (daily, hourly, or a custom cron expression)
- Enable the job and click Save
- Optionally click Start Manually to test the first run
Common issues
Section titled βCommon issuesβ| Issue | Cause | Fix |
|---|---|---|
| Job not listed in admin | Missing [ScheduledPlugIn] attribute | Add the attribute with a unique GUID |
| Job runs but status is empty | Execute returns null | Always return a status message string |
| Job cannot be stopped | IsStoppable not set | Set IsStoppable = true in the constructor |
| Timeout on long jobs | Default timeout too short | Break work into batches and track progress |