Command Groups
Overview
Command groups combine multiple commands to create complex behaviors. Use them for autonomous routines or coordinated mechanism movements.
Group Types
Three main patterns:
- Sequential: Commands run one after another, waiting for each to finish.
- Parallel: Commands run simultaneously, all finishing when the last one completes.
- Mixed: Combine both patterns for complex routines.
The Commands Factories
WPILib offers two equivalent ways to build a group: the SequentialCommandGroup / ParallelCommandGroup constructors, and static factories on Commands. Prefer the factories — they read better when nested and they compose without a new on every line.
import edu.wpi.first.wpilibj2.command.Commands;
Commands.sequence(a, b, c) // one after another
Commands.parallel(a, b) // together, ends when all end
Commands.race(a, b) // together, ends when the first ends
Commands.deadline(d, a, b) // together, ends when d ends
Commands.waitUntil(condition) // waits without blocking
Commands.waitSeconds(2.0) // waits a fixed time
Commands.runOnce(action) // one action, no subsystem
And the decorators, which chain onto any command:
| Decorator | Effect |
|---|---|
.withTimeout(s) | Cancel after s seconds |
.until(cond) | End early when a condition becomes true |
.repeatedly() | Restart as soon as it finishes |
.alongWith(other) | Run other in parallel |
.andThen(other) | Run other afterwards |
.ignoringDisable(true) | Keep running while the robot is disabled |
Compose on the Subsystem
When every step belongs to one subsystem, put the composition on that subsystem, next to the factories it combines:
public Command deployAndIntakeCommand() {
return Commands.sequence(
setIntakePosCommand(INTAKE_PIVOT_DOWN),
Commands.waitUntil(this::withinTolerance).withTimeout(0.5),
setIntakeCommand(1));
}
Read it top to bottom: drop the pivot, wait until it gets there (but no longer than half a second), then start the rollers. Every step is a factory that already exists on the subsystem, so composing them needs no constructor, no imports, and no new file — and everything the intake can do stays in one place.
Waiting Without Blocking
waitUntil is the piece worth dwelling on. Its condition is an ordinary boolean method:
public boolean withinTolerance() {
return Math.abs(pivot.getPosition().getValueAsDouble() - motionMagic.Position) < INTAKE_PIVOT_TOLERANCE;
}
The scheduler polls it once per loop. Between polls, every other subsystem keeps running, the drivetrain keeps responding, and the loop stays at 20 ms.
Never block the scheduler
The tempting version is a loop:
setIntakePosition(INTAKE_PIVOT_DOWN);
while (!withinTolerance()) {
}
setIntakePosition(INTAKE_PIVOT_UP);This does not work. The whole robot runs on one thread, so that while loop stops the scheduler — including the updates to pivot.getPosition(), which is exactly what the condition reads. The drivetrain stops responding and the loop overruns.
Commands.waitUntil(this::withinTolerance) expresses the same intent and yields between checks. Any time you want to wait, that is the tool.
Pair waitUntil with a timeout whenever the condition depends on hardware. If the mechanism jams, the condition never becomes true and the sequence hangs for the rest of the match. .withTimeout(0.5) gives up and moves on.
Repeating
.repeatedly() restarts a sequence as soon as it ends — how a mechanism shakes a stuck game piece loose:
public Command agitateCommand() {
return Commands.sequence(
setIntakeCommand(1),
setIntakePosCommand(28),
Commands.waitUntil(this::withinTolerance),
setIntakePosCommand(32),
Commands.waitUntil(this::withinTolerance)).repeatedly();
}
Rollers on, pivot to 28, wait, pivot to 32, wait, start over — until the button is released.
Compose at the Binding
When an action spans several subsystems but they do not need to react to each other, combine their factories directly in RobotContainer:
controller.L2().and(controller.R1().negate())
.whileTrue(transfer.setTransferCommand(-1)
.alongWith(conveyor.setConveyorCommand(0.6))
.alongWith(intake.deployAndOutakeCommand(-1)));
Three subsystems running together, built from three factories, with no new class anywhere. Each factory requires only its own subsystem, so the composed command requires all three and the scheduler resolves conflicts correctly.
Autonomous Routines
The same tools build an auto:
public Command getAutoCommand() {
return Commands.sequence(
Commands.parallel(
drivetrain.driveForwardCommand().withTimeout(2.0),
intake.deployAndIntakeCommand().withTimeout(2.0)),
intake.setIntakeCommand(0));
}
For anything path-based, PathPlanner generates these for you from event markers — see PathPlanner.
Where a composition belongs
| Situation | Put it |
|---|---|
| Steps all belong to one subsystem | On that subsystem, via Commands.sequence |
| Several subsystems, independent | In the binding, via .alongWith |
| Several subsystems that must react to each other | In its own Command class |
Two timing surprises
The body of a factory method runs when the method is called, not when the command runs.
public Command agitateCommand() {
agitatePos = 38; // runs at binding time, once
return Commands.sequence(...);
}That assignment executes while RobotContainer is building bindings — at startup — not each time the button is pressed. Anything that must happen at schedule time belongs inside a runOnce or run body.
runOnce steps end immediately. A sequence of nothing but runOnce commands finishes in a single loop and commands the last setpoint straight away. The waitUntil between steps is what makes a sequence take time.