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.
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:
- Is the currently-running command interruptible? (All commands are interruptible by default.)
- If yes: the old command's
end(true)is called (thetruemeans interrupted), and the new command starts. - If no (
withInterruptBehavior(kCancelIncoming)): the new command is silently rejected.
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.
| Scenario | Scheduler Behavior |
|---|---|
| New command requires a free subsystem | Command scheduled immediately, initialize() called |
| New command requires a subsystem with an interruptible command running | Running command's end(true) called, new command starts |
| New command requires a subsystem with a non-interruptible command running | New 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 true | end(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.
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();
}
}
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.
| Method | Where Used | What It Does |
|---|---|---|
| command.schedule() | autonomousInit(), test code | Submits the command to the scheduler. If the required subsystem is free, it starts immediately on the next run(). |
| command.cancel() | Safety handlers, mode switches | Calls end(true) on the command and frees its subsystems. Safe to call even if the command isn't running. |
| command.isScheduled() | Diagnostic code, conditionals | Returns true if the command is currently registered with the scheduler. |
| CommandScheduler.getInstance().cancelAll() | teleopInit(), disabledInit() | Cancels every running command. Useful for clean mode transitions. |
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.
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())inrobotInit(). 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 returningtrue, 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?
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?
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?
Observe the Scheduler in Action
- In your Command-Based project from Lesson 1, add
SmartDashboard.putData("Scheduler", CommandScheduler.getInstance())torobotInit(). 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. - Add
CommandScheduler.getInstance().cancelAll()to bothteleopInit()andautonomousInit()inRobot.java. This is a 2910 standard practice. Add a comment explaining why. - 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. - 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.