Commands
Hook
You’ve wired up a subsystem with motors and sensors. Now you need the arm to move to 90 degrees when a button is pressed, hold there while the button is held, and return to zero when it’s released. How do you express that in code?
Not with a flag variable and a switch statement hidden in robotPeriodic(). That approach works for one button — it becomes unmanageable for five. Commands give you a clean, composable way to describe actions with explicit start, run, and stop behavior. They’re the reason teams can run 15-step autonomous routines written by three different people without catastrophic conflicts.
Core concept
A command is a Java object that encapsulates one robot action. It has four lifecycle methods: initialize() runs once when the command starts, execute() runs every loop iteration while it’s active, isFinished() tells the scheduler when to stop it, and end(boolean interrupted) runs once for cleanup. The CommandScheduler drives this lifecycle automatically.
The command lifecycle
schedule()
│
▼
┌─────────────┐
│ initialize() │ ← runs once
└──────┬──────┘
│
▼
┌─────────────┐
│ execute() │ ← runs every 20 ms
└──────┬──────┘
│
isFinished()?
No │ Yes
│ │
│ ▼
│ ┌─────────────────────┐
│ │ end(interrupted=false)│ ← normal completion
│ └─────────────────────┘
│
└── (loops back to execute())
If another command interrupts:
┌─────────────────────┐
│ end(interrupted=true) │ ← cleanup, don't assume finished
└─────────────────────┘
| Method | When it runs | Typical use |
|---|---|---|
initialize() | Once, at command start | Reset encoder, set initial setpoint, start timer |
execute() | Every loop, while active | Apply motor output, calculate PID, update state |
isFinished() | Every loop, after execute() | Check if goal reached, timer expired, etc. |
end(boolean interrupted) | Once, when done | Stop motors, retract cylinder, log completion |
The interrupted parameter in end() is true if another command cancelled this one, false if isFinished() returned true. This lets you clean up differently depending on whether you completed successfully.
Extending CommandBase
import edu.wpi.first.wpilibj2.command.CommandBase;
import edu.wpi.first.wpilibj.Timer;
public class DriveForDistance extends CommandBase {
private final DriveSubsystem drive;
private final double targetInches;
private static final double TOLERANCE_INCHES = 1.0;
public DriveForDistance(DriveSubsystem drive, double targetInches) {
this.drive = drive;
this.targetInches = targetInches;
addRequirements(drive); // MUST include this
}
@Override
public void initialize() {
drive.resetEncoders(); // start measuring from zero
}
@Override
public void execute() {
double remaining = targetInches - drive.getAverageDistanceInches();
double speed = remaining > 0 ? 0.5 : -0.5; // simple bang-bang
drive.tankDrive(speed, speed);
}
@Override
public boolean isFinished() {
return Math.abs(targetInches - drive.getAverageDistanceInches()) < TOLERANCE_INCHES;
}
@Override
public void end(boolean interrupted) {
drive.tankDrive(0, 0); // always stop motors on exit
if (interrupted) {
System.out.println("DriveForDistance interrupted at "
+ drive.getAverageDistanceInches() + " inches");
}
}
}
The most common beginner mistake: forgetting addRequirements(). Without it, two commands can both call drive.tankDrive() simultaneously, and neither the scheduler nor you will know who “won.”
If you forget addRequirements(), the scheduler cannot enforce exclusive ownership. Two commands will happily run at the same time against the same subsystem. The hardware output will be the result of whichever command ran last in the scheduler loop — unpredictable and impossible to debug under pressure.
Built-in command factories
Writing a full class for every action is verbose for simple cases. WPILib provides factory classes:
InstantCommand
Runs initialize() once, then immediately finishes. Perfect for toggling state.
// As an anonymous class (old style):
new InstantCommand(() -> claw.closeClaw(), claw);
// In a button binding:
new JoystickButton(controller, XboxController.Button.kA.value)
.onTrue(new InstantCommand(() -> intake.deployArm(), intake));
RunCommand
Runs execute() every loop and never finishes on its own. Requires manual cancellation (e.g., the button is released).
// Drive while button is held:
new JoystickButton(controller, XboxController.Button.kRightBumper.value)
.whileTrue(new RunCommand(
() -> driveSubsystem.arcadeDrive(
controller.getLeftY(),
controller.getRightX()
),
driveSubsystem
));
FunctionalCommand
The full lifecycle in a single call — useful when you need initialize/execute/end/isFinished but don’t want a separate file:
new FunctionalCommand(
() -> drive.resetEncoders(), // initialize
() -> drive.tankDrive(0.5, 0.5), // execute
interrupted -> drive.tankDrive(0, 0), // end
() -> drive.getAverageDistanceInches() >= 60.0, // isFinished
drive // requirements
);
Use FunctionalCommand sparingly — once the logic gets complex, a named class is more readable.
Command composition
The power of commands comes from composing them. WPILib provides two primary group types:
SequentialCommandGroup
Runs commands one after another. The next command starts only after the previous one finishes.
import edu.wpi.first.wpilibj2.command.SequentialCommandGroup;
public class ScoreAuto extends SequentialCommandGroup {
public ScoreAuto(DriveSubsystem drive, ArmSubsystem arm, ClawSubsystem claw) {
addCommands(
new DriveForDistance(drive, 72), // drive 72 inches forward
new ArmToAngle(arm, 90), // raise arm to 90 degrees
new InstantCommand(claw::closeClaw, claw), // grab game piece
new ArmToAngle(arm, 0), // lower arm
new DriveForDistance(drive, -72) // back up
);
}
}
ParallelCommandGroup
Runs commands simultaneously. The group finishes when all commands finish.
import edu.wpi.first.wpilibj2.command.ParallelCommandGroup;
// Raise arm AND spin up shooter at the same time
new ParallelCommandGroup(
new ArmToAngle(arm, 75),
new SpinUpShooter(shooter, 4500) // RPM
)
ParallelRaceGroup
Runs commands simultaneously. Finishes when any one command finishes, then cancels the rest.
import edu.wpi.first.wpilibj2.command.ParallelRaceGroup;
// Intake until a ball is detected, then stop — timeout after 3 seconds
new ParallelRaceGroup(
new RunIntake(intake), // never finishes on its own
new WaitUntilCommand(intake::hasBall), // finishes when sensor triggers
new WaitCommand(3.0) // safety timeout
)
ParallelDeadlineGroup
Like ParallelRaceGroup but one specific command is the “deadline” — the group ends when that one finishes.
import edu.wpi.first.wpilibj2.command.ParallelDeadlineGroup;
// Drive while also tracking with the limelight — stop when drive finishes
new ParallelDeadlineGroup(
new DriveForDistance(drive, 48), // deadline
new TrackTarget(vision) // runs alongside, cancelled when drive ends
)
Inline composition with decorators
Commands also expose chainable decorator methods:
new ArmToAngle(arm, 90)
.withTimeout(3.0) // cancel after 3 seconds if not finished
.andThen(new InstantCommand(claw::closeClaw, claw)) // run next in sequence
.beforeStarting(() -> System.out.println("Starting arm sequence"))
.finallyDo(interrupted -> arm.setSpeed(0));
Decorator methods return a new command wrapping the original — they don’t modify in place. Chain them like you would Java stream operations: each call returns the decorated version.
Complete example: coordinated autonomous sequence
import edu.wpi.first.wpilibj2.command.SequentialCommandGroup;
import edu.wpi.first.wpilibj2.command.ParallelCommandGroup;
import edu.wpi.first.wpilibj2.command.InstantCommand;
import edu.wpi.first.wpilibj2.command.WaitCommand;
public class TwoGamePieceAuto extends SequentialCommandGroup {
public TwoGamePieceAuto(
DriveSubsystem drive,
ArmSubsystem arm,
ClawSubsystem claw
) {
addCommands(
// 1. Start: arm up + drive forward simultaneously
new ParallelCommandGroup(
new ArmToAngle(arm, 90),
new DriveForDistance(drive, 24)
),
// 2. Grab first game piece
new InstantCommand(claw::closeClaw, claw),
new WaitCommand(0.3), // let cylinder fully extend
// 3. Back up and lower arm
new ParallelCommandGroup(
new DriveForDistance(drive, -24),
new ArmToAngle(arm, 0)
),
// 4. Score position
new DriveForDistance(drive, 60),
new ArmToAngle(arm, 45),
new InstantCommand(claw::openClaw, claw),
new WaitCommand(0.2),
// 5. Return to start
new ParallelCommandGroup(
new DriveForDistance(drive, -60),
new ArmToAngle(arm, 0)
)
);
}
}
Full lifecycle trace
new ArmToAngle(arm, 90).schedule()// placed in scheduler queueinitialize()// runs once — set up PID setpointexecute() — loop 1// PID output applied, arm movingexecute() — loop 2// still movingexecute() — loop 15// approaching targetisFinished() returns true (|90 - 89.1| < 2.0)// within toleranceend(interrupted=false)// normal completion — stop motorcommand removed from scheduler// subsystem free — default command resumesStep through to see values update.
Interrupted lifecycle trace
DriveForDistance(60) running// driving forward normallyNew command EmergencyStop(drive) scheduled// new command requires Drive — conflict detectedend(interrupted=true) on DriveForDistance// old command cleaned up, new one startsEmergencyStop.execute() — sets all motors to 0// robot stopped safelyStep through to see values update.
Key takeaways
- Commands have four lifecycle methods:
initialize(),execute(),isFinished(),end(interrupted). initialize()andend()each run exactly once.execute()andisFinished()run every loop while active.- Always call
addRequirements()— it’s what gives the scheduler the information it needs to prevent conflicts. InstantCommand,RunCommand, andFunctionalCommandcover simple cases without full class boilerplate.- Sequential and parallel groups compose simple commands into complex behaviors declaratively.
end(true)signals interruption — clean up safely but don’t assume the goal was reached.
Common confusions
“My command runs once and stops immediately.” isFinished() is probably returning true before execute() has done anything meaningful. This often happens when a state variable isn’t initialized in initialize() (e.g., forgetting drive.resetEncoders() before checking distance traveled).
“My sequential group skips commands.” A command in the group’s isFinished() may be returning true immediately (see above). Add a print statement in initialize() and end() of each command to trace which ones actually ran.
“My InstantCommand isn’t doing anything.” Lambda captures in Java don’t evaluate at call time — they evaluate at execution time. But if the object you’re calling (e.g., intake) was null when the lambda was created, you’ll get a NullPointerException silently if the scheduler swallows exceptions. Always check that your subsystem references are non-null.
“I added withTimeout() but the command still runs forever.” withTimeout(3.0) wraps the command in a ParallelRaceGroup with a WaitCommand(3.0). It works correctly for commands that are scheduled directly. If the command is inside a SequentialCommandGroup, the group’s command is the wrapper — confirm you’re calling withTimeout() on the command object you’re adding to the group.
Challenge
Trace through a simplified command lifecycle in pure Java. Implement a SimulatedCommand class with initialize(), execute(), isFinished(), and end(boolean) methods, then write a simple scheduler loop that drives the lifecycle correctly. The command should run for exactly 5 execute() calls before finishing.
Stuck? Show hint
In runCommand(): call command.initialize(). Then use a while loop: call command.execute(), then check command.isFinished(). If true, call command.end(false) and break. Add a counter for the safety limit — if it exceeds 20, call command.end(true) and break.
A command's isFinished() returns true on the very first call (before execute() runs). What happens?
You build a SequentialCommandGroup with three commands. The second command requires the same subsystem as the third. What happens when the second command finishes and the third starts?
What’s next
You now have the complete command-based framework: subsystems own hardware, commands describe actions with a clean lifecycle, and the scheduler orchestrates everything. The next lessons will put these together with feedback control — you’ll implement a PID controller that uses encoder feedback to drive a mechanism to a precise position, and learn how WPILib’s PIDController and ProfiledPIDController make this composable with the command system.