Skip to content
RustPluginGenerator

How to make a Rust scheduled commands plugin (timed console tasks)

A plugin that runs server console commands on a real-world time of day (HH:mm) or every N minutes, with per-entry enable switches, built from config on OnServerInitialized and driven by a single repeating timer.

Server owners routinely need something to happen at a fixed wall-clock time: a nightly server.save, a restart warning at 03:55, a broadcast at midday, or a repeating message every 15 minutes. Doing this with an external cron job means another service to keep alive and no visibility from inside the game server. This plugin keeps the schedule inside Oxide: each entry in the config has a time (either HH:mm or a minute interval), the console command to run, whether it runs as the server console or as a configured player, and an enabled flag. On startup it loads the list into a ScheduledTask list and starts one repeating timer that wakes once per minute. At each wake it checks the clock, runs every due entry, and records the last execution so a restart cannot double-fire the same minute. Console and chat commands let admins list, run and reload the schedule live.

The plugin code

ScheduledCommands.cs
using System.Linq;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Scheduled Commands", "RustPluginGenerator", "1.0.0")]
    [Description("Runs configured console commands at fixed times of day or on repeating minute intervals.")]
    public class ScheduledCommands : RustPlugin
    {
        private const string PermAdmin = "scheduledcommands.admin";

        private PluginConfig config;
        private Timer tickTimer;
        private readonly List<ScheduledTask> schedule = new List<ScheduledTask>();

        private class PluginConfig
        {
            [JsonProperty("Tasks")]
            public List<TaskEntry> Tasks { get; set; } = new List<TaskEntry>
            {
                new TaskEntry { Command = "server.save", Time = "03:55", Source = TaskSource.Console, Enabled = false },
                new TaskEntry { Command = "say Server will restart in 5 minutes", Time = "03:55", Source = TaskSource.Console, Enabled = false },
                new TaskEntry { Command = "say Welcome to the server", Time = "15", Source = TaskSource.Console, Enabled = false }
            };
        }

        private class TaskEntry
        {
            [JsonProperty("Command")]
            public string Command { get; set; }

            [JsonProperty("Time")]
            public string Time { get; set; }

            [JsonProperty("Source")]
            public TaskSource Source { get; set; } = TaskSource.Console;

            [JsonProperty("PlayerId")]
            public string PlayerId { get; set; } = string.Empty;

            [JsonProperty("Enabled")]
            public bool Enabled { get; set; } = true;

            [JsonProperty("LastRun")]
            public string LastRun { get; set; } = string.Empty;
        }

        private enum TaskSource
        {
            Console,
            Player
        }

        private class ScheduledTask
        {
            public TaskEntry Entry;
            public bool IsInterval;
            public TimeSpan DailyTime;
            public int IntervalMinutes;
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["NoTasks"] = "No scheduled tasks are configured.",
                ["TaskLine"] = "#{0} [{1}] {2} -> {3} (enabled: {4}, last run: {5})",
                ["OnlyAdmin"] = "You do not have permission to use this command.",
                ["BadIndex"] = "Task index {0} does not exist.",
                ["Reloaded"] = "Schedule reloaded: {0} task(s).",
                ["RunNow"] = "Task #{0} executed."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            BuildSchedule();
            tickTimer = timer.Every(60f, Tick);
        }

        private void Unload()
        {
            if (tickTimer != null && !tickTimer.Destroyed)
                tickTimer.Destroy();

            tickTimer = null;
            schedule.Clear();
        }

        private void BuildSchedule()
        {
            schedule.Clear();

            if (config?.Tasks == null) return;

            for (int i = 0; i < config.Tasks.Count; i++)
            {
                var entry = config.Tasks[i];
                if (entry == null || string.IsNullOrEmpty(entry.Command) || string.IsNullOrEmpty(entry.Time))
                    continue;

                var task = new ScheduledTask { Entry = entry };

                if (entry.Time.Contains(":"))
                {
                    if (!TimeSpan.TryParseExact(entry.Time, "hh\\:mm", CultureInfo.InvariantCulture, out task.DailyTime) &&
                        !TimeSpan.TryParse(entry.Time, CultureInfo.InvariantCulture, out task.DailyTime))
                    {
                        PrintWarning($"Task #{i} has invalid time '{entry.Time}', skipped");
                        continue;
                    }
                }
                else
                {
                    int minutes;
                    if (!int.TryParse(entry.Time, out minutes) || minutes <= 0)
                    {
                        PrintWarning($"Task #{i} has invalid interval '{entry.Time}', skipped");
                        continue;
                    }

                    task.IsInterval = true;
                    task.IntervalMinutes = minutes;
                }

                schedule.Add(task);
            }

            Puts($"Loaded {schedule.Count} scheduled task(s)");
        }

        private void Tick()
        {
            if (schedule.Count == 0) return;

            DateTime now = DateTime.Now;
            string stamp = now.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);

            for (int i = 0; i < schedule.Count; i++)
            {
                var task = schedule[i];
                var entry = task.Entry;

                if (!entry.Enabled || string.IsNullOrEmpty(entry.Command))
                    continue;

                if (entry.LastRun == stamp)
                    continue;

                bool due = task.IsInterval
                    ? IsIntervalDue(entry, task.IntervalMinutes, now)
                    : task.DailyTime.Hours == now.Hour && task.DailyTime.Minutes == now.Minute;

                if (!due) continue;

                entry.LastRun = stamp;
                RunTask(entry);
            }

            SaveConfig();
        }

        private bool IsIntervalDue(TaskEntry entry, int intervalMinutes, DateTime now)
        {
            if (string.IsNullOrEmpty(entry.LastRun))
                return true;

            DateTime last;
            if (!DateTime.TryParseExact(entry.LastRun, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out last))
                return true;

            return (now - last).TotalMinutes >= intervalMinutes;
        }

        private void RunTask(TaskEntry entry)
        {
            BasePlayer caller = null;

            if (entry.Source == TaskSource.Player && !string.IsNullOrEmpty(entry.PlayerId))
                caller = BasePlayer.FindAwakeOrSleeping(entry.PlayerId);

            if (entry.Source == TaskSource.Player && caller != null)
                caller.SendConsoleCommand(entry.Command);
            else
                ConsoleSystem.Run(ConsoleSystem.Option.Server, entry.Command);

            Puts($"Executed: {entry.Command}");
        }

        [ConsoleCommand("scheduledcommands.list")]
        private void CmdList(ConsoleSystem.Arg arg)
        {
            if (!CheckAccess(arg)) return;

            if (config?.Tasks == null || config.Tasks.Count == 0)
            {
                arg.ReplyWith(Msg("NoTasks"));
                return;
            }

            for (int i = 0; i < config.Tasks.Count; i++)
            {
                var entry = config.Tasks[i];
                if (entry == null) continue;
                arg.ReplyWith(Msg("TaskLine", null, i, entry.Time, entry.Command, entry.Source, entry.Enabled, string.IsNullOrEmpty(entry.LastRun) ? "-" : entry.LastRun));
            }
        }

        [ConsoleCommand("scheduledcommands.run")]
        private void CmdRun(ConsoleSystem.Arg arg)
        {
            if (!CheckAccess(arg)) return;

            int index = arg.GetInt(0, -1);
            if (config?.Tasks == null || index < 0 || index >= config.Tasks.Count || config.Tasks[index] == null)
            {
                arg.ReplyWith(Msg("BadIndex", null, index));
                return;
            }

            RunTask(config.Tasks[index]);
            SaveConfig();
            arg.ReplyWith(Msg("RunNow", null, index));
        }

        [ConsoleCommand("scheduledcommands.reload")]
        private void CmdReload(ConsoleSystem.Arg arg)
        {
            if (!CheckAccess(arg)) return;

            if (tickTimer != null && !tickTimer.Destroyed)
                tickTimer.Destroy();

            LoadConfig();
            BuildSchedule();
            tickTimer = timer.Every(60f, Tick);
            arg.ReplyWith(Msg("Reloaded", null, schedule.Count));
        }

        [ChatCommand("schedule")]
        private void CmdSchedule(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("OnlyAdmin", player.UserIDString));
                return;
            }

            if (config?.Tasks == null || config.Tasks.Count == 0)
            {
                SendReply(player, Msg("NoTasks", player.UserIDString));
                return;
            }

            for (int i = 0; i < config.Tasks.Count; i++)
            {
                var entry = config.Tasks[i];
                if (entry == null) continue;
                SendReply(player, Msg("TaskLine", player.UserIDString, i, entry.Time, entry.Command, entry.Source, entry.Enabled, string.IsNullOrEmpty(entry.LastRun) ? "-" : entry.LastRun));
            }
        }

        private bool CheckAccess(ConsoleSystem.Arg arg)
        {
            if (arg == null) return false;
            if (!arg.IsAdmin) return false;
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
                return false;
            return true;
        }
    }
}

Hooks used: OnServerInitialized

Config file

Created automatically on first load at oxide/config/ScheduledCommands.json.

ScheduledCommands.json
{
  "Tasks": [
    {
      "Command": "server.save",
      "Time": "03:55",
      "Source": 0,
      "PlayerId": "",
      "Enabled": false,
      "LastRun": ""
    },
    {
      "Command": "say Server will restart in 5 minutes",
      "Time": "03:55",
      "Source": 0,
      "PlayerId": "",
      "Enabled": false,
      "LastRun": ""
    },
    {
      "Command": "say Welcome to the server",
      "Time": "15",
      "Source": 0,
      "PlayerId": "",
      "Enabled": false,
      "LastRun": ""
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
scheduledcommands.listPrint every configured task with its time, enabled state and last run time to the console or RCON.scheduledcommands.admin
scheduledcommands.run <index>Run the task at the given zero-based index immediately, ignoring its schedule.scheduledcommands.admin
scheduledcommands.reloadReload the config file and rebuild the internal schedule without restarting the server.scheduledcommands.admin
/scheduleIn-game shortcut that lists the configured tasks to the calling player.scheduledcommands.admin
Permissions
PermissionGrants
scheduledcommands.adminGrants access to the scheduled commands console commands and the /schedule chat command.

How the code works

  1. Config model for timed console entries

    The nested PluginConfig holds a single Tasks list of TaskEntry. Each entry has Command (the console command line to execute), Time (HH:mm in 24-hour server time, or a number such as 15 meaning every 15 minutes), Source (the TaskSource enum: Console or Player), optional PlayerId used as the command caller, Enabled and an internal LastRun string. Keeping everything in one list makes the plugin easy to edit by hand or reload from disk without touching code.

  2. Converting times in BuildSchedule

    BuildSchedule converts each entry into a ScheduledTask once. If the Time string contains a colon it is parsed with TimeSpan.TryParseExact using the hh\:mm format, falling back to TimeSpan.TryParse. Otherwise it is read as an integer interval in minutes and flagged with IsInterval. Entries with an empty command or time, an unparseable clock value, or an interval that is not a positive integer are skipped and reported with PrintWarning at load time only, so one typo never breaks the whole schedule.

  3. One repeating timer instead of many

    OnServerInitialized calls BuildSchedule and starts a single timer.Every(60f, Tick). On each tick Tick compares the current DateTime.Now with the stored values instead of keeping dozens of timers. Since the finest schedule granularity is one minute, a 60-second period is exact enough and stays cheap on a busy server. Unload destroys the timer if it is still alive and clears the schedule list.

  4. Avoiding double runs on restart

    Before executing, Tick writes the current minute into Entry.LastRun as yyyy-MM-dd HH:mm, then skips any entry whose LastRun already equals that stamp. A plugin reload or server restart inside the same minute therefore does not fire the command twice for daily tasks. Interval tasks use IsIntervalDue, which compares the parsed LastRun against the interval in minutes, and runs immediately on first boot when LastRun is empty. SaveConfig persists the timestamps in oxide/config.

  5. Running a command with the right caller

    RunTask checks Entry.Source. When it is Player and PlayerId resolves through BasePlayer.FindAwakeOrSleeping, the command is sent with caller.SendConsoleCommand. Otherwise it is executed as the server console via ConsoleSystem.Run(ConsoleSystem.Option.Server, ...). Server-console commands such as say, server.save and restart should stay on Console; Player is only needed when a command reads the calling player.

  6. Admin control and live reload

    [ConsoleCommand("scheduledcommands.list")] prints every task with its time, source, enabled state and last run time. scheduledcommands.run <index> calls RunTask on one entry for testing and saves the config. scheduledcommands.reload destroys the current timer, calls LoadConfig and BuildSchedule, then starts a fresh timer, so admins can edit the JSON and apply it without a restart. CheckAccess requires arg.IsAdmin and, for in-game callers, the scheduledcommands.admin permission.

Install it on your server

  1. Save ScheduledCommands.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/ScheduledCommands.json (Carbon: carbon/configs/ScheduledCommands.json). After editing, reload it:oxide.reload ScheduledCommands c.reload ScheduledCommands # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default scheduledcommands.admin c.grant group default scheduledcommands.admin # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Daily restart warning

    Run a broadcast at fixed times of day before a scheduled server restart.

    Generate this version
  • Recurring server announcements

    Rotate through a list of chat broadcasts on a fixed minute interval.

    Generate this version
  • Timed supply drops and events

    Trigger a game event on a wall-clock schedule without cron.

    Generate this version

FAQ

Does this use my schedule timezone?

The plugin uses DateTime.Now on the machine that runs the Rust server, so schedules follow the host's local time zone. Set the server time zone in the OS or plan around it; the plugin does not implement its own offset.

Where do I edit the schedule?

Edit oxide/config/ScheduledCommands.json, then run scheduledcommands.reload from the server console or RCON. No restart is required and the running timer is replaced cleanly.

How do I install the plugin on Oxide or Carbon?

Save the code as ScheduledCommands.cs in oxide/plugins, let the server compile it, then edit the generated config and reload. Carbon loads the same .cs file from its plugins folder; no change is needed.

Will a missed minute fire later?

No. Daily tasks fire only when DailyTime matches the current hour and minute. If the server is down at that minute, the run is skipped until the next matching minute the following day.

What does Source Player require?

A PlayerId (64-bit Steam ID) that the plugin resolves through BasePlayer.FindAwakeOrSleeping. If no player is found, the command falls back to the server console. Leave Source on Console for server.save, say or restart.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Write a Rust Oxide plugin ScheduledCommands that runs console commands at HH:mm times or every N minutes from config, with Source, PlayerId, Enabled and LastRun fields, a /schedule chat command and admin console commands to list, run and reload.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize scheduled commands

Customize