Unit 6 · Lesson 2

The Command Scheduler

The Command Scheduler is the engine that makes Command-Based work. It decides which commands run, enforces subsystem ownership so two commands can't conflict, calls the right lifecycle methods at the right time, and handles interruptions gracefully. Understanding it deeply means understanding why Command-Based is reliable at competition scale.

By the end of this lesson, you will:

  • Trace the exact sequence of operations that occur inside one scheduler run cycle
  • Explain how subsystem requirements prevent command conflicts — and what happens when a conflict occurs
  • Explain what a default command is, when it runs, and when it doesn't
  • Use .schedule(), .cancel(), and .isScheduled() correctly
  • Describe what happens to a running command when it is interrupted by a new command requiring the same subsystem

The Scheduler's One Job: Manage Active Commands

The Command Scheduler maintains a list of currently-running commands. Every time CommandScheduler.getInstance().run() is called from robotPeriodic(), it works through a precise sequence of operations. Understanding that sequence is understanding Command-Based.

The scheduler is a singleton — there is exactly one instance for the entire robot program. Every subsystem registers itself with it at construction. Every command declares which subsystems it requires. The scheduler uses those declarations to make all its conflict-resolution decisions.

The Run Loop — Step by Step

Click each step below to see what the scheduler does at that point in a single 20 ms cycle.

CommandScheduler.run() — one 20 ms cycle
Step 1
Poll triggers
Check all registered trigger conditions
Step 2
Run subsystem periodic()
Every registered subsystem runs its loop
Step 3
Execute active commands
execute() called on every running command
Step 4
Check isFinished()
Commands that return true are ended
Step 5
Schedule new commands
Triggered commands enter the queue
Step 6
Run default commands
Idle subsystems get their default back
← click a step or press Animate to walk through the cycle

Subsystem Requirements: The Conflict Prevention System

When a command declares that it requires a subsystem, it is making a claim: "While I am running, no other command may use this subsystem." The scheduler enforces this claim automatically.

When a new command is scheduled that requires a subsystem already in use, the scheduler runs through a decision tree:

  1. Is the currently-running command interruptible? (All commands are interruptible by default.)
  2. If yes: the old command's end(true) is called (the true means interrupted), and the new command starts.
  3. If no (withInterruptBehavior(kCancelIncoming)): the new command is silently rejected.
💡 Interruption is not a bug — it's a feature

The most common use of interruption: the operator is running an automated intake sequence (IntakeGamePieceCommand requiring IntakeSubsystem), and mid-sequence the driver presses the eject button. The eject command requires the same subsystem. The scheduler interrupts the intake sequence, calls end(true) to safely stop the rollers, and starts the eject command. No manual state tracking. No flags. The architecture handles it.

ScenarioScheduler Behavior
New command requires a free subsystemCommand scheduled immediately, initialize() called
New command requires a subsystem with an interruptible command runningRunning command's end(true) called, new command starts
New command requires a subsystem with a non-interruptible command runningNew command silently rejected — nothing happens
New command requires multiple subsystems — one free, one busy (interruptible)Running command on busy subsystem interrupted, new command takes both
Command's isFinished() returns trueend(false) called, subsystem freed, default command re-scheduled

Default Commands

A default command is the command that runs on a subsystem whenever nothing else is claiming it. It is re-scheduled automatically any time the subsystem becomes free.

The most common default command in FRC is DriveWithJoysticksCommand on the DriveSubsystem. While no autonomous or special command is running the drivetrain, the driver's stick controls it. The moment an autonomous driving command takes over, the default command is pushed out. When that command ends, the default command returns automatically — no code needed to re-enable driver control.

RobotContainer.java — default commands and scheduling
public class RobotContainer {

  private final DriveSubsystem m_drive = new DriveSubsystem();
  private final IntakeSubsystem m_intake = new IntakeSubsystem();
  private final XboxController m_driver = new XboxController(0);

  public RobotContainer() {
    // Set the default command for the drivetrain.
    // This runs whenever no other command uses m_drive.
    m_drive.setDefaultCommand(
      new DriveWithJoysticksCommand(m_drive, m_driver)
    );
    configureButtonBindings();
  }

  private void configureButtonBindings() {
    // When A is held, run IntakeCommand (requires m_intake)
    new JoystickButton(m_driver, XboxController.Button.kA.value)
      .whileTrue(new IntakeCommand(m_intake));

    // When B is pressed once, schedule EjectCommand
    new JoystickButton(m_driver, XboxController.Button.kB.value)
      .onTrue(new EjectCommand(m_intake));
  }

  public Command getAutonomousCommand() {
    return m_autoChooser.getSelected();
  }
}
← click a highlighted token
⚠️ Default commands must never end on their own

A default command's isFinished() must always return false. If it returns true, the command ends, the subsystem becomes free with no default, and the robot has no driver control. This silent failure is one of the most confusing bugs in Command-Based. If your default drive command exits unexpectedly, check your isFinished() first.

Scheduling and Cancellation

Commands can be scheduled programmatically (not just from triggers) and cancelled programmatically. These patterns are used in autonomous routines and test code.

MethodWhere UsedWhat It Does
command.schedule()autonomousInit(), test codeSubmits the command to the scheduler. If the required subsystem is free, it starts immediately on the next run().
command.cancel()Safety handlers, mode switchesCalls end(true) on the command and frees its subsystems. Safe to call even if the command isn't running.
command.isScheduled()Diagnostic code, conditionalsReturns true if the command is currently registered with the scheduler.
CommandScheduler.getInstance().cancelAll()teleopInit(), disabledInit()Cancels every running command. Useful for clean mode transitions.
🔍 LRI Perspective: "Mode transitions need a clean state"

A pattern I recommend for every competition robot: call CommandScheduler.getInstance().cancelAll() inside teleopInit() and autonomousInit(). This guarantees that any command that was running in a previous mode is fully ended before the new mode starts. Without it, an autonomous command that wasn't fully cleaned up can leave motors running when TeleOp begins. At competition, that means the drive team enables and the robot immediately does something unexpected. One line prevents this entirely.

🔌 System Check — Scheduler Verification

Confirm the scheduler is running correctly before testing any commands:

  • Enable the robot in any mode and open the Driver Station console. You should see no "Loop overrun" warnings. If you do, a periodic method or command execute() is taking more than 20 ms. Find the slow call and move it out of the hot path.
  • In Shuffleboard, look for the "Scheduler" entry under SmartDashboard. WPILib's scheduler automatically publishes a list of active commands if you call SmartDashboard.putData("Scheduler", CommandScheduler.getInstance()) in robotInit(). Add this line — seeing active command names in real time is invaluable for debugging trigger bindings.
  • Verify default commands are installed: deploy the code, enable in TeleOp, and confirm the drivetrain responds to the joystick immediately. If it doesn't, your default command's isFinished() may be returning true, or it may not have been set on the subsystem at all.

Knowledge Check

1. The driver is running DriveWithJoysticksCommand (the default command on DriveSubsystem). An autonomous assist command is scheduled that also requires DriveSubsystem. What does the scheduler do to DriveWithJoysticksCommand?

  • A Both commands run simultaneously — the new command's output is averaged with the drive command's output.
  • B DriveWithJoysticksCommand.end(true) is called, the driver loses control of the drivetrain, and the autonomous command takes over.
  • C The autonomous command is rejected because the default command cannot be interrupted.
  • D Both commands are cancelled and the drivetrain becomes free with no active command.

2. Your DriveWithJoysticksCommand is the default command on DriveSubsystem. After an auto-aim command runs and finishes normally, what happens next without any additional code on your part?

  • A The DriveSubsystem becomes permanently idle until the driver presses a button to re-enable manual control.
  • B The scheduler detects that DriveSubsystem is now free and automatically re-schedules DriveWithJoysticksCommand — the driver regains control on the next loop iteration.
  • C You must manually call m_drive.setDefaultCommand() again after any command that uses the drivetrain finishes.
  • D The auto-aim command restarts automatically because default commands loop by default.

3. You add CommandScheduler.getInstance().cancelAll() to teleopInit(). A teammate says this is unnecessary because commands end themselves when they finish. Why is the call still important?

  • A It is unnecessary — commands always clean up when the mode changes, and the teammate is correct.
  • B An autonomous command that was still running when TeleOp was enabled would continue running unless explicitly cancelled — potentially driving motors or actuating mechanisms the driver isn't expecting when they first take control.
  • C cancelAll() is required to re-initialize subsystems between modes.
  • D The scheduler itself stops automatically when robot mode changes — cancelAll() only affects commands scheduled after the mode change.
💪 Practice Prompt

Observe the Scheduler in Action

  1. In your Command-Based project from Lesson 1, add SmartDashboard.putData("Scheduler", CommandScheduler.getInstance()) to robotInit(). Deploy, open Shuffleboard, and find the Scheduler widget. It shows all currently-active command names. Leave this in your codebase permanently — it is the most useful diagnostic tool in Command-Based.
  2. Add CommandScheduler.getInstance().cancelAll() to both teleopInit() and autonomousInit() in Robot.java. This is a 2910 standard practice. Add a comment explaining why.
  3. Create a trivial test: write two commands that both require the same dummy subsystem. Schedule the first from a button binding (onTrue). Schedule the second from a different button binding (onTrue). Press the first button, then press the second while the first is running. Watch the Scheduler widget in Shuffleboard to confirm the first command's name disappears and the second's appears. You have just observed interruption in action.
  4. Stretch goal: Make one of those test commands non-interruptible by calling .withInterruptBehavior(InterruptionBehavior.kCancelIncoming) on it when scheduling. Repeat the experiment. Confirm that the second command cannot interrupt the first — the Scheduler widget shows only the first command name throughout.