Unit 9 · Lesson 4

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(), and Commands.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.

Commands.sequence()
One after another Each command runs to completion before the next one starts. The group finishes when the last command finishes.
Use when: steps depend on previous steps. "Drive, then turn, then score."
Commands.parallel()
All at once, wait for all All commands run simultaneously. The group finishes when every command has finished.
Use when: all parallel actions must complete. "Raise arm AND spin up shooter, then continue."
Commands.parallelRace()
All at once, first one wins All commands run simultaneously. The group finishes when any one command finishes. All others are interrupted.
Use when: you have competing end conditions. "Run intake OR time out — whichever happens first."
Commands.deadline()
Deadline drives the clock All commands run simultaneously. The group finishes when the first argument (the deadline) finishes. All others are interrupted.
Use when: one action defines the duration. "Drive forward while running intake — stop intake when drive ends."
💡 parallelRace vs. deadline — the key distinction

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.

Command group timeline — illustrative A = 2s, B = 3.5s, C = 1.5s
sequence()
parallel()
parallelRace()
deadline()

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.

RobotContainer.java — factory method style (preferred)
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)
    )
);
Fluent decorator style — equivalent result, different syntax
// 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;
💡 Decorator reference: the four fluent methods

.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.

Requirement conflict explorer — click a scenario
No conflict
Parallel conflict
Sequential reuse
Deadline pattern
✓ Valid

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.

🔍 The silent failure you won't see in the logs

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.

RobotContainer.java — nested groups for a 2-piece auto
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)
        )
    );
}
🔍 Why 2910's auto routines read like outlines

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.

Correct pattern: fresh instances, not reused objects
// ❌ 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.

Inline commands in a sequence — keeping simple things simple
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"))
);
💡 When to use a named class vs. an inline command

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

⚙️ Before Running a Multi-Step Command Group

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 using deadline() 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 a parallel() group indefinitely. Either give it a .withTimeout(), or use deadline() or parallelRace() instead of parallel().
  • 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?

  • A DriveDistanceCommand canceled IntakeUntilSensorCommand because they share a subsystem requirement
  • B IntakeUntilSensorCommand never returned true from isFinished() — no game piece was acquired — and parallel() waits for all children to finish, so the group is still running
  • C The sequence was interrupted by the default drive command taking over the drivetrain
  • D Commands.parallel() requires both commands to start at the same time, which isn't possible if one of them has an initialize() delay

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."

  • A Commands.parallel(DriveCommand, ArmCommand) — both run together, group ends when both finish
  • B Commands.parallelRace(DriveCommand, ArmCommand) — ends when either finishes first
  • C Commands.deadline(DriveCommand, ArmCommand) — drive is the deadline; arm is interrupted when drive ends
  • D Commands.sequence(DriveCommand, ArmCommand) — drive first, then arm

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?

  • A Command objects reset themselves after running, so the second use should work — this must be a hardware issue
  • B The same command object instance was used in both positions — when a command finishes once, the scheduler marks it as completed and won't schedule the same object again; each position in the group needs a new instance
  • C Commands.sequence() only allows each command class to appear once by design
  • D The subsystem requirement from the first use carries over and blocks the second use
💪 Practice Prompt

Build a Multi-Phase Autonomous Using All Four Group Types

  1. In your robot project, write a method getCompetitionAuto() in RobotContainer. This method should return a Commands.sequence() with at least three phases. Each phase must use a different group type: one phase as a plain Commands.parallel(), one as Commands.deadline(), and one as Commands.parallelRace(). Add at least one Commands.runOnce() inline command for a SmartDashboard status update.
  2. 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.
  3. 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.
  4. 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.
  5. Bonus: Create a private helper method buildScoringPhase() that returns a reusable Command object 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.