Complex Movements with Command Groups
Individual commands are the vocabulary. Command groups are the grammar. This lesson teaches you to compose reliable autonomous routines from simple building blocks — and explains exactly why some combinations will break if you don't understand subsystem requirements.
By the end of this lesson, you will:
- Distinguish between the four WPILib command group types and identify which to use for a given situation
- Explain what
Commands.sequence(),Commands.parallel(),Commands.parallelRace(), andCommands.deadline()each do when their commands finish at different times - Recognize and resolve subsystem requirement conflicts in parallel groups
- Use nested command groups to build multi-layer autonomous routines
- Apply the fluent decorator API (
.andThen(),.alongWith(),.raceWith(),.deadlineFor()) as alternatives to factory methods - Implement a complete multi-step autonomous sequence using all four group types where appropriate
The Four Command Group Types
Every autonomous routine beyond a single command uses one or more of these four group types. They all accept any number of commands as children, and they all terminate when their completion condition is met — but what that condition is differs critically between them.
parallelRace(A, B, C) ends when any of A, B, or C finishes — you don't control which one wins. deadline(A, B, C) always ends when A finishes, regardless of whether B or C has finished. Use deadline when one specific command defines the natural endpoint of the group. Use parallelRace when you genuinely don't know which condition will trigger first and any of them are valid exit points.
Timeline Visualizer: How Each Group Type Executes
Select a group type below to see how three commands with different durations execute, and when the group itself ends. The timing shown is illustrative — real command durations are determined at runtime by sensor conditions, not a fixed schedule.
Two Ways to Write Command Groups
WPILib provides two equivalent syntaxes for building command groups. The factory method style is usually preferred inside getAutonomousCommand(). The fluent decorator style is useful when chaining groups onto an existing command reference.
return Commands.sequence( // Step 1: Drive 2 m while simultaneously running intake (deadline = drive) Commands.deadline( new DriveDistanceCommand(m_drive, 2.0, 0.5), // deadline new IntakeUntilSensorCommand(m_intake) // canceled when drive ends ).withTimeout(4.0), // Step 2: Turn AND prep arm at the same time — both must finish Commands.parallel( new TurnToAngleCommand(m_drive, 90.0).withTimeout(2.5), new MoveArmToAngleCommand(m_arm, 45.0).withTimeout(2.5) ), // Step 3: Score (race: sensor confirms OR 0.8s timeout) Commands.parallelRace( new EjectGamePieceCommand(m_intake), Commands.waitSeconds(0.8) ) );
// Each command object has decorator methods that create group wrappers. // These are equivalent to the factory methods above. Command driveWithIntake = new DriveDistanceCommand(m_drive, 2.0, 0.5) .deadlineFor(new IntakeUntilSensorCommand(m_intake)); // ↑ drive is the deadline; intake runs alongside, canceled when drive ends Command turnAndPrepArm = new TurnToAngleCommand(m_drive, 90.0) .alongWith(new MoveArmToAngleCommand(m_arm, 45.0)); // ↑ parallel: both must finish Command score = new EjectGamePieceCommand(m_intake) .raceWith(Commands.waitSeconds(0.8)); // ↑ parallelRace: first to finish wins Command fullAuto = driveWithIntake .andThen(turnAndPrepArm) .andThen(score); // ↑ sequence: run in order return fullAuto;
.andThen(other) — run this command, then run other (sequential). .alongWith(other) — run both simultaneously, finish when all finish (parallel). .raceWith(other) — run both simultaneously, finish when any finishes (parallelRace). .deadlineFor(other) — run both simultaneously, this command is the deadline (deadline). These are member methods on every Command object — you don't need to import anything extra to use them.
Subsystem Requirements in Parallel Groups
This is the one thing that will silently break your autonomous if you don't understand it. When two commands in a parallel group both require the same subsystem, the scheduler has a conflict — and it resolves it in a way that may surprise you.
Use the scenario explorer below to see how the scheduler handles different requirement situations.
Commands.parallel(DriveDistanceCommand, MoveArmToAngleCommand)
The drive command requires DriveSubsystem. The arm command requires ArmSubsystem. These are different subsystems — no conflict. Both commands run simultaneously without issue. This is the intended use of parallel groups: concurrent actions on different mechanisms.
Rule of thumb: parallel groups work cleanly when each child command requires a different subsystem from every other child command.
When two commands in a parallel group conflict over a subsystem, WPILib doesn't throw an exception — it cancels the command that was scheduled later. You'll see one command run and one silently disappear. If you don't have telemetry logging which commands are active, you'll spend a practice match trying to figure out why your arm never moved during autonomous. Always log CommandScheduler.getInstance().getActiveCommandCount() and consider logging active command names to SmartDashboard when debugging parallel routines.
Nesting Groups: The Real Power
The true capability of command groups emerges when you nest them. A sequence can contain a parallel group, which can contain a deadline group. Each level describes a different layer of the routine's choreography. Reading a well-nested group structure is like reading an outline — the indentation tells you what happens together and what happens in order.
public Command getTwoPieceAuto() { double startHeading = m_drive.getHeadingDegrees(); return Commands.sequence( // ── Phase 1: Score preloaded piece ────────────────────────── Commands.parallel( new MoveArmToAngleCommand(m_arm, 90).withTimeout(2.0), new SpinShooterCommand(m_shooter, 4000).withTimeout(2.0) ), // Both arm and shooter must be ready before we shoot. Commands.parallelRace( new EjectCommand(m_shooter), Commands.waitSeconds(0.6) ), // ── Phase 2: Drive to second piece ────────────────────────── // Deadline: the drive defines how long intake runs. Commands.deadline( Commands.sequence( new TurnToAngleCommand(m_drive, startHeading + 180).withTimeout(2.0), new DriveDistanceCommand(m_drive, 3.0, 0.5).withTimeout(4.0) ), new IntakeUntilSensorCommand(m_intake).withTimeout(5.0), new MoveArmToAngleCommand(m_arm, 0).withTimeout(1.5) ), // ── Phase 3: Return and score second piece ────────────────── Commands.parallel( new TurnToAngleCommand(m_drive, startHeading).withTimeout(2.0), new MoveArmToAngleCommand(m_arm, 90).withTimeout(2.0), new SpinShooterCommand(m_shooter, 4000).withTimeout(2.0) ), Commands.parallelRace( new EjectCommand(m_shooter), Commands.waitSeconds(0.6) ) ); }
Looking at Team 2910's competition code, autonomous routines are almost always structured as nested command groups rather than flat sequences of many steps. The nesting makes the intent visible at each level: the outer sequence describes phases, the parallel groups inside each phase describe what happens simultaneously within that phase. A new programmer reading the code can understand the robot's behavior without reading individual command implementations — the structure alone communicates the choreography. That's not an accident. It's the result of deliberate architectural discipline that starts exactly with the patterns in this lesson.
Common Command Group Mistakes
Mistake 1: Using parallel() when you need deadline()
A team writes Commands.parallel(DriveDistanceCommand, IntakeCommand). Their intent: run intake during the drive. The problem: parallel() waits for all commands to finish. If the intake has no sensor and never calls isFinished() returning true, the group hangs forever. The robot drives to the target and stops driving — but the group doesn't end, so the sequence never advances to the next step.
The fix: use Commands.deadline(DriveDistanceCommand, IntakeCommand). The drive is the natural endpoint. When the drive finishes, the group ends and the intake is interrupted — which is exactly the intended behavior.
Mistake 2: Forgetting timeouts on parallel branches
A team writes Commands.parallel(TurnToAngleCommand, MoveArmToAngleCommand) without timeouts. In testing the arm PID is slightly mistuned and atGoal() never returns true. The group runs forever. The robot turns and raises its arm but the sequence never advances. Adding .withTimeout() to each child gives each branch an independent safety ceiling — if any branch gets stuck, it times out and the overall group can proceed.
Mistake 3: Reusing the same command object instance in two places
A command group is an immutable structure — each command object can only be scheduled once at a time. If you write Command spin = new SpinShooterCommand(...) and then include spin in two places in the same group tree, only one instance will run correctly. Always create a new instance for each use, or use a method that creates a new command object each time it's called.
// ❌ WRONG: same Command object used in two places Command spin = new SpinShooterCommand(m_shooter, 4000); Commands.sequence( spin.withTimeout(2.0), // first use somethingElse, spin.withTimeout(2.0) // second use — same object, will misbehave ); // ✅ CORRECT: create a new instance for each use Commands.sequence( new SpinShooterCommand(m_shooter, 4000).withTimeout(2.0), somethingElse, new SpinShooterCommand(m_shooter, 4000).withTimeout(2.0) ); // ✅ ALSO CORRECT: use a factory method that builds a fresh command each call private Command spinShooter() { return new SpinShooterCommand(m_shooter, 4000).withTimeout(2.0); } Commands.sequence(spinShooter(), somethingElse, spinShooter());
Using Inline Commands in Groups
Not every step in an autonomous sequence deserves a named command class. For simple one-off actions, WPILib's inline command factories let you express the behavior directly inside the group definition without creating a separate file.
return Commands.sequence( // Reset odometry to known starting pose — runOnce() for one-shot actions Commands.runOnce(() -> m_drive.resetOdometry(getStartingPose())), // Print to dashboard to mark the start of auto (debugging aid) Commands.runOnce(() -> SmartDashboard.putString("Auto/Status", "Started")), // Named command for complex behavior new DriveDistanceCommand(m_drive, 2.0, 0.5).withTimeout(4.0), // Wait for mechanism to settle before next step Commands.waitSeconds(0.15), // Constant-speed intake for exactly 1 second — inline is fine here Commands.run(() -> m_intake.runIntake(0.6), m_intake).withTimeout(1.0), // Print completion marker Commands.runOnce(() -> SmartDashboard.putString("Auto/Status", "Complete")) );
A named command class is worth creating when: the behavior is complex enough to need its own initialize(), execute(), and isFinished(); the command is reused in multiple places; or the behavior needs parameters. An inline Commands.runOnce() or Commands.run().withTimeout() is appropriate when: the action is a single method call, it runs exactly once, or the behavior is obvious from reading the lambda. Don't create a named class for resetEncoders() — write Commands.runOnce(() -> m_drive.resetEncoders()) directly in the group.
🔌 System Check
Command groups fail in ways that are hard to debug unless you verify each layer independently:
- Test each child command in isolation first. Every command in a group should work correctly when run alone before it's placed in the group. A command that hangs or finishes immediately in isolation will cause the same problem inside a group, but it will be harder to diagnose which command is responsible when they're all running together.
- Add SmartDashboard markers at the start and end of each phase. Use
Commands.runOnce(() -> SmartDashboard.putString("Auto/Phase", "Phase1Start"))at the beginning of each major sequence step. When a group gets stuck, the last-reported phase tells you exactly where execution stopped. - Verify that parallel groups have no subsystem conflicts. Read every
addRequirements()call in every child command of a parallel group. No two children should require the same subsystem. If they do, restructure usingdeadline()or split into sequential steps. - Confirm that infinite-running commands in parallel groups have timeouts. Any command whose
isFinished()always returns false (default commands, constantly running mechanisms) will hang aparallel()group indefinitely. Either give it a.withTimeout(), or usedeadline()orparallelRace()instead ofparallel(). - Run the full group at reduced speed with elevated wheels first. Before running a complex group on the ground, run it with the drivetrain elevated and all speeds reduced to 20% to verify sequencing and timing without risk of driving into a wall. Confirm the full sequence completes, all phases log correctly, and no command is silently canceled.
Knowledge Check
1. A team writes Commands.parallel(DriveDistanceCommand, IntakeUntilSensorCommand). The robot drives 2 meters and stops, but the sequence never advances to the next step. They check the logs and see DriveDistanceCommand ended normally. What is the most likely cause?
2. What is the correct group type for this scenario: "While the robot drives to the scoring position, raise the arm. The arm should stop — at whatever angle it reached — the moment the drive command finishes, even if the arm hasn't reached its target yet."
3. A team creates a reusable scoring action: Command scoreCommand = new SpinAndEjectCommand(...). They use scoreCommand in two places in a Commands.sequence(). On the second run of the sequence during simulation, the scoring action does nothing. What is the cause?
Build a Multi-Phase Autonomous Using All Four Group Types
- In your robot project, write a method
getCompetitionAuto()inRobotContainer. This method should return aCommands.sequence()with at least three phases. Each phase must use a different group type: one phase as a plainCommands.parallel(), one asCommands.deadline(), and one asCommands.parallelRace(). Add at least oneCommands.runOnce()inline command for a SmartDashboard status update. - Before running the group, read every
addRequirements()call in every command you're using. Draw a table: command name in one column, required subsystems in another. Verify that no two commands in any of your parallel groups share a subsystem. Fix any conflicts before proceeding. - Add
.withTimeout()to every sensor-based child command in your parallel and deadline groups. Choose timeouts conservatively — what's the longest reasonable time each step should take? Record your reasoning in a comment next to each timeout value. - Run the sequence in simulation (if available) or with wheels elevated at 20% speed. Add a SmartDashboard string output that updates at the start of each phase. Confirm every phase executes in order and no phase hangs indefinitely. Fix any issues found.
- Bonus: Create a private helper method
buildScoringPhase()that returns a reusableCommandobject representing your scoring action. Call it twice in your sequence — once near the start and once near the end. Verify that you're calling the method (not reusing the same object reference) and that both calls produce fresh command instances. Explain in a comment why method-based factories are safer than storing command references as fields when the same action appears multiple times in a routine.