A beginner-friendly guide to WPILib command-based programming in Java: subsystems, commands, the CommandScheduler, triggers, and composing commands.
to read
words
sections
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.
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.
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.
You rarely need to write a full command class. WPILib's Commands utility class provides factory methods for the common cases:
| Factory | What 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.
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).
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.)
The real power of command-based shows up when you stitch commands together. Compositions are themselves commands, so you can nest them freely.
| Composition | Factory | Finishes when… |
|---|---|---|
| Sequential | Commands.sequence(a, b, c) | the last command finishes |
| Parallel | Commands.parallel(a, b) | all commands finish |
| Race | Commands.race(a, b) | any command finishes (others cancelled) |
| Deadline | Commands.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.
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().
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.
This article is AI-assisted: drafted from primary sources, then reviewed and edited by hand. Errors still get through. When one is reported we fix it and write down what changed — publicly, in the corrections log.
Keep going
You came here for one answer. These lessons teach the same subject properly, in the order a team actually learns it. All 51 are free and none of them need an account to read.
Go deeper
LearnFRC has 394 free FRC lessons across every department. Create a free account to save your place, track your progress, and earn a certificate.
Create a free accountKeep reading
Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.