Sitefinity has the ability to create cronjobs that run on timers in the background, they call them ScheduledTasks. They handle things like publishing\unpublishing on specific dates, but you can hook into the system yourself to run tasks as well!

If you want to see what your site is doing, since there's no UI for it, have a peek inside the sf_scheduled_tasks table.

So here's a sample of what I have going on a Live site. The concept is a mail system that runs on a timer to look for new messages, then flushes a queue. All that code though is stripped out though, but it would exist here.

This expands on the Official Documentation by making sure there are no other instances of this task in the database, we REALLY didn't want to have multiple instances of this class executing at the same time. Maybe it's overkill, but that's better than multiple tasks

AutomailerScheduledTask.csView on GitHub
using System;
using System.Diagnostics;
using System.Linq;
using Telerik.Sitefinity.Abstractions;
using Telerik.Sitefinity.Scheduling;
using Telerik.Sitefinity.Scheduling.Model;

namespace SitefinityWebApp.ScheduledTasks { public class AutomailerScheduledTask : ScheduledTask { public static string _taskName = "SitefinityWebApp.ScheduledTasks.AutomailerScheduledTask"; public static bool _loggingEnabled = true; //This should probably be loaded via a config entry... private static readonly string scheduleSpecType = "crontab";

    /// <summary>
    /// This is the method that gets called to run whatever you need to do
    /// </summary>
    public override void ExecuteTask()
    {
        AutomailerScheduledTask.LogMessage("ExecuteTask START");

        //Clear duplicate tasks, just in case before we run the actual task
        try
        {
            using (var manager = SchedulingManager.GetManager())
            {
                //Get all tasks that are still running, that are not this ID
                var tasks = manager.GetTaskData().Where(x => x.Title == AutomailerScheduledTask._taskName && x.Status != TaskStatus.Failed && x.Id != this.Id);
                foreach (var t in tasks)
                {
                    AutomailerScheduledTask.LogMessage("FOUND DUPLICATE TASK ID: {0}".Arrange(t.Id));
                    manager.DeleteTaskData(t);
                    manager.SaveChanges();
                }
            }
        }
        catch (Exception ex)
        {
            //Handle Error
            AutomailerScheduledTask.LogMessage(ex.Message);
        }


        AutomailerScheduledTask.LogMessage("About to run the postmaster methods");
        try
        {
            // #########################################
            // THIS IS WHERE YOU PUT YOUR CODE TO EXECUTE
            // #########################################

        }
        catch (Exception ex)
        {
            AutomailerScheduledTask.LogMessage($"$$$ ExecuteTask CRASH $$$ {ex.Message}");
        }

        AutomailerScheduledTask.LogMessage("ExecuteTask END");
    }

    /// <summary>
    /// This is the code that adds the job to the sf_scheduled_tasks table
    /// </summary>
    public void ScheduleCrontabTask()
    {
        AutomailerScheduledTask.LogMessage("ScheduleCrontabTask");

        using (var manager = SchedulingManager.GetManager())
        {
            var task = manager.GetTaskData().FirstOrDefault(x => x.Title == AutomailerScheduledTask._taskName);
            if (task == null)
            {
                var cronjobExpression = "*/5 * * * *"; //Cronjob expression https://crontab.cronhub.io/

                AutomailerScheduledTask.LogMessage("Creating new Task START");
                var newTask = (AutomailerScheduledTask)Activator.CreateInstance(this.GetType());
                newTask.Id = Guid.NewGuid(); //Can't set this to be static... tried
                newTask.ScheduleSpecType = scheduleSpecType;
                newTask.Title = AutomailerScheduledTask._taskName;
                newTask.IsRunning = false;
                newTask.SetCustomData(DateTime.Now.ToString());
                newTask.ScheduleSpec = cronjobExpression;
                newTask.ExecuteTime = this.GetExecuteTime();
                newTask.Description = "Automailer... mail parser";
                manager.AddTask(newTask);
                manager.SaveChanges();

                AutomailerScheduledTask.LogMessage("Creating new Task END");
            }
            else
            {
                AutomailerScheduledTask.LogMessage("Updating Existing Task START");
                if (task.ExecuteTime < DateTime.Now.ToUniversalTime())
                {
                    task.ExecuteTime = this.GetExecuteTime();
                }

                task.IsRunning = false;
                task.StatusMessage = null;
                manager.SaveChanges();
                AutomailerScheduledTask.LogMessage("Updating Existing Task END");
            }
        }
    } 

    public static void LogMessage(string message)
    {
        if (AutomailerScheduledTask._loggingEnabled)
        {
            var log = "########### MAILER ##############\n" + message + "\n##########################################";
            Telerik.Sitefinity.Abstractions.Log.Write(log, ConfigurationPolicy.Trace);
            Debug.WriteLine(log);
        }
    }

    public DateTime GetExecuteTime()
    {
        var today = DateTime.Now.ToUniversalTime().AddSeconds(10);

        return new DateTime(today.Year, today.Month, today.Day, today.Hour, today.Minute, 0);
    }

    /// <summary>
    /// Registers all custom scheduled tasks 
    /// </summary>
    public static void ScheduleTask()
    {
        var task = new AutomailerScheduledTask();
        task.ScheduleCrontabTask();
    }

    /// <summary>
    /// When Sitefinity fires back up, this deletes any tasks that match this name so you don't run duplicates
    /// </summary>
    public static void DeleteAllTasksOnSiteLoad()
    {
        using (var manager = SchedulingManager.GetManager())
        {
            var tasks = manager.GetTaskData().Where(x => x.Title == AutomailerScheduledTask._taskName && x.Status != TaskStatus.Failed);
            AutomailerScheduledTask.LogMessage("DeleteAllScheduledTaskOnSiteLoad deleting {0} tasks".Arrange(tasks.Count()));
            foreach (var t in tasks)
            {
                try
                {
                    manager.DeleteTaskData(t);
                    manager.SaveChanges();
                }catch(Exception ex)
                {
                AutomailerScheduledTask.LogMessage(ex.Message);
                }
            }
        }
    }
}

}

So you need to register in inside your Global.asax, and the code can obviously just live wherever since it's compiled. Depending on the frequency of it's execution you can dial back the logging I have to not bloat out your log folder. In the official code on the site I use a Sitefinity Configuration element so I can turn it on or off without restarting the site.