Skip to content
All articles

FRC Command-Based Programming: Subsystems, Commands, and the Scheduler

9 min read·

If you've ever tried to control a robot with one giant while loop full of if statements, you already know how fast it turns into spaghetti. Command-based programming is WPILib's answer to that mess: a design pattern that lets you describe what your robot should do as small, reusable building blocks, and lets the framework figure out when to run them. It's the approach the vast majority of competitive FRC teams use, and once the mental model clicks, your code gets dramatically cleaner. This guide walks through that model in Java, grounded in the official WPILib command-based docs.

The mental model: subsystems and commands

Command-based programming rests on two abstractions. Get these right and everything else follows.

A subsystem is an "independently-controlled collection of robot hardware (such as motor controllers, sensors, pneumatic actuators, etc.) that operate together." Think of it as one functional unit of your robot: the drivetrain, the arm, the intake, the shooter. A subsystem owns its hardware and its state.

A command is an action that runs over time. "Commands run when scheduled, until they are interrupted or their end condition is met." A command might drive the robot from joystick input, spin a flywheel up to speed, or run an arm to a setpoint.

The single most important rule that makes this work: only one command can use (require) a given subsystem at the same time. This is the core of command-based resource management. If the arm subsystem is busy running a "go to scoring position" command and you fire a "stow the arm" command, the scheduler resolves the conflict for you instead of letting two commands fight over the same motors.

Writing a subsystem

In Java, you almost always subclass SubsystemBase. It gives you two big conveniences: automatic registration with the CommandScheduler, and a Sendable implementation so the subsystem shows up on dashboards.

public class IntakeSubsystem extends SubsystemBase {
  // Hardware is PRIVATE. The outside world never touches it directly.
  private final SparkMax m_motor = new SparkMax(5, MotorType.kBrushless);

  // Public methods expose ACTIONS, not hardware.
  public void run()  { m_motor.set(0.8); }
  public void stop() { m_motor.set(0.0); }

  @Override
  public void periodic() {
    // Called once per scheduler run (every 20 ms). Good for telemetry.
  }
}

Notice the encapsulation: the motor is private, and the public surface is descriptive methods like run() and stop(). The official docs use the same pattern, hiding a DoubleSolenoid behind grabHatch() and releaseHatch(). There's also a companion simulationPeriodic() that runs only in simulation. Hardware setup like this is exactly where your electrical and mechanical knowledge meets your code: the CAN IDs and motor types must match what's actually wired on the robot.

Writing commands

You rarely need to write a full command class. WPILib's Commands utility class provides factory methods for the common cases:

FactoryWhat it does
Commands.runOnce(action, reqs)Runs a lambda once, then finishes (InstantCommand)
Commands.run(action, reqs)Runs a lambda repeatedly until interrupted (RunCommand)
Commands.startEnd(start, end, reqs)One lambda on start, another when it ends (StartEndCommand)
Commands.waitSeconds(t)Ends after t seconds (WaitCommand)
Commands.waitUntil(condition)Ends when a BooleanSupplier becomes true (WaitUntilCommand)

A clean convention is to put factory methods on the subsystem itself, so the requirement is wired in automatically:

public Command runCommand() {
  // run() repeats the lambda; the trailing "this" adds the requirement.
  return run(this::run).finallyDo(interrupted -> stop());
}

When you need the full lifecycle, extend the abstract Command class. Every command has four lifecycle methods:

  • initialize() — called exactly once when the command is scheduled. One-time setup.
  • execute() — called repeatedly (every 20 ms) while scheduled. Your control loop lives here.
  • isFinished() — checked repeatedly; as soon as it returns true, the command ends.
  • end(boolean interrupted) — called once when the command ends, whether it finished normally (interrupted == false) or was cancelled (interrupted == true). Clean up here, e.g. stop motors.

A command declares which subsystems it needs by calling addRequirements(...) in its constructor. That's the hook into resource management.

Default commands

Every subsystem can have one default command that runs automatically whenever no other command is using that subsystem. The classic use is teleop driving: your drivetrain's default command reads the joysticks continuously, but it gets cleanly interrupted the moment an auto-align command grabs the drivetrain.

m_drivetrain.setDefaultCommand(
    m_drivetrain.run(() -> m_drivetrain.arcadeDrive(
        -driver.getLeftY(), -driver.getRightX())));

One hard requirement straight from the docs: a default command must require its subsystem, and it must not finish on its own (a default command that ends gets immediately re-scheduled).

Binding commands to triggers

You don't poll buttons in command-based — you declare bindings once, during initialization, and the library handles the rest. The foundation is the Trigger class, which represents a boolean condition. The easiest way to get triggers is the command-based HID classes like CommandXboxController:

CommandXboxController driver = new CommandXboxController(0);

// Schedule on the false -> true edge:
driver.a().onTrue(m_intake.runOnceCommand());

// Run while held, cancel when released:
driver.rightBumper().whileTrue(m_intake.runCommand());

The key binding methods:

  • onTrue(cmd) / onFalse(cmd) — schedule once on the rising/falling edge.
  • whileTrue(cmd) — schedule when the trigger goes true, cancel when it goes false.
  • toggleOnTrue(cmd) — schedule on a press, cancel on the next press.

Triggers compose like booleans with .and(), .or(), and .negate(), and you can clean up noisy inputs with .debounce(seconds). You can also wrap any condition in a trigger — a limit switch, a sensor threshold, anything:

new Trigger(m_arm::atUpperLimit).onTrue(m_arm.stopCommand());

(If you read older code that uses whenPressed or whenHeld, those binding methods on the deprecated Button subclass are gone — use onTrue and whileTrue instead.)

Composing commands

The real power of command-based shows up when you stitch commands together. Compositions are themselves commands, so you can nest them freely.

CompositionFactoryFinishes when…
SequentialCommands.sequence(a, b, c)the last command finishes
ParallelCommands.parallel(a, b)all commands finish
RaceCommands.race(a, b)any command finishes (others cancelled)
DeadlineCommands.deadline(deadline, a, b)the deadline command finishes

Most of these also have decorator forms that read like English:

Command scoreThenStow =
    m_arm.toScoringPosition()
        .andThen(m_intake.ejectCommand().withTimeout(1.0))
        .andThen(m_arm.toStowPosition());

Useful decorators: andThen(...) (run after), alongWith(...) (run in parallel), raceWith(...), deadlineFor(...), withTimeout(seconds), until(condition), unless(condition), and repeatedly(). (If you see deadlineWith(...) in older code, it was deprecated in 2025 for removal — deadlineFor(...) is the current name.)

Two rules to internalize. First, a composition inherits the union of its members' requirements — so a parallel group of an arm command and an intake command requires both subsystems. Second, a command instance can only be in one composition (and can't be independently scheduled once it's in one); reusing the same instance throws an exception. When in doubt, build a fresh command from a factory each time.

Where it all comes together: RobotContainer and the scheduler

A standard command-based project (generated from the WPILib template) has a Robot class, a RobotContainer, and a Constants class.

RobotContainer is where the declarative setup lives. Subsystems are declared as private fields, button bindings go in a configureBindings() method, and getAutonomousCommand() returns the command to run in autonomous.

Robot extends TimedRobot and stays tiny. The one line that makes the whole framework work is in robotPeriodic():

@Override
public void robotPeriodic() {
  CommandScheduler.getInstance().run();
}

That call drives everything, running at 50 Hz (once every 20 ms). Per the docs, each run() does four things in order: (1) calls periodic() on every registered subsystem, (2) polls all trigger/button bindings and schedules commands, (3) runs each scheduled command's execute() and checks isFinished(), ending finished commands, and (4) schedules default commands on any subsystem that's now free. You can also schedule and cancel commands manually with CommandScheduler.getInstance().schedule(cmd) and .cancel(cmd), and note that a command's initialize() runs at schedule time, not on the next run().

Putting it together

The whole pattern is: subsystems own hardware and state, commands describe actions over time, triggers decide when commands fire, compositions glue actions into routines, and the CommandScheduler arbitrates who gets which subsystem. Start small — one subsystem with a default command and a couple of button bindings — and grow from there.

Ready to build your first command-based robot? Dive into the LearnFRC Programming track.

Keep reading

Learn every department of FRC — free

393+ structured lessons, quizzes, and team tools. Built by an FRC student, for the community.

Browse the guides