Commands & Command Compositions

One Body, Written Like a Normal Method

In Commands v3, a command's entire behavior is one block of code — a lambda that receives a Coroutine object. The body reads top to bottom like any method you've written: code before a loop runs once when the command starts, the loop runs once per scheduler loop, and code after the loop runs once when the goal is reached.

public Command raiseCommand()
{
    return run(coroutine -> {
        motor.setThrottle(0.5);       // once, when the command starts
        while (!topLimit.get())
        {
            coroutine.yield();        // pause here until the next loop
        }
        motor.setThrottle(0.0);       // once, when the top limit switch trips
    }).named("Raise Elevator");
}

This method lives in an Elevator class that implements Mechanism, where motor is a DriveMotor (Ch.4) and topLimit is a DigitalInput (Ch.3). The coroutine is the object that lets a command pause partway through and pick up again later: coroutine.yield() means "I'm done for this loop — let the rest of the robot run, and resume me right here next loop."

Part of the bodyWhen it runsThe v2 method you'll see in older code
Code before the loopOnce, when the command startsinitialize()
The loop's bodyOnce per scheduler loopexecute()
The loop's conditionChecked every loop to decide when to stopisFinished()
Code after the loopOnce, when the command finishes on its ownend(false)

In v2 code you'll see a class that extends Command and overrides those four methods separately, plus addRequirements(...) in its constructor. v3 folds all four into the one body above; Lesson 25.7 has a v2 reading guide.

A body with no loop at all simply runs once and ends — that's how a one-shot action like "extend the piston" is written.

Every Command Has a Name — Enforced by a Staged Builder

run(...) doesn't return a Command yet. It returns a builder stage, and only calling .named("...") on it produces the finished Command. This is the Builder Pattern from Ch.22 in a strict form: each stage only offers the methods that are legal next, so forgetting the name is a compile error, not a surprise at a match. Before naming, a stage can take optional settings — .whenExited(...), .whenCanceled(...), .withPriority(...), and .until(...) — covered below and in Lesson 25.3.

Requirements Come From How You Build the Command

A command requires the mechanisms it controls, and the way you start building it decides which ones:

  • elevator.run(...) or elevator.runRepeatedly(...) — requires elevator, automatically.
  • Command.requiring(drive, arm).executing(coroutine -> ...) — requires several mechanisms at once.
  • Command.noRequirements(coroutine -> ...) — requires nothing; for commands that don't touch hardware, like resetting a software value or logging.

There's no separate "declare my requirements" step to forget — which is exactly the mistake v2's addRequirements(...) made easy.

Built-In Factories: Most Commands Are One Statement

hatch.run(coroutine -> hatch.grab()).named("Grab Hatch");     // runs once, then ends
drive.runRepeatedly(() -> drive.arcade(forward.getAsDouble(), turn.getAsDouble()))
    .named("Arcade Drive");                                   // repeats until interrupted
intake.idle();                                                // owns the intake, idles
Command.waitFor(Seconds.of(5)).named("Wait 5 s");             // ends after 5 seconds
Command.waitUntil(limitSwitch::get).named("Wait for Switch"); // ends once this is true

runRepeatedly takes a plain Runnable (Lesson 19.1) and calls it once every loop, yielding for you — a natural fit for a default command (Lesson 25.3). (Here forward and turn are DoubleSuppliers reading the joystick axes.) idle() is already named and has the lowest possible priority, so anything else can take the mechanism from it.

Inside a body, the coroutine has helpers for the most common kinds of pausing:

  • coroutine.yield() — pause until the next loop.
  • coroutine.wait(Seconds.of(1)) — pause for a length of time.
  • coroutine.waitUntil(limitSwitch::get) — pause until a condition is true.
  • coroutine.park() — pause forever, until something cancels the command.

Cleaning Up When a Command Ends

A command that starts a motor usually has to stop it however the command ends — finished, canceled, or interrupted by another command. whenExited(...) runs a Runnable on every exit; whenCanceled(...) runs only when the command is stopped early.

public Command spinCommand()
{
    return run(coroutine -> {
        motor.setThrottle(0.8);
        coroutine.park();             // keep spinning until something stops this command
    }).whenExited(() -> motor.setThrottle(0.0)).named("Spin Shooter");
}

Alpha note: whenExited (and the onExit() hook behind it) was added to v3 in September 2026 — re-check it against the current WPILib docs before the season.

Units: Durations Are Time Values, Not Plain Numbers

Every v3 method that takes a duration takes a Time (from org.wpilib.units.measure), not a double. You make one from a unit constant in WPILib's Units class, usually brought in with a static import (Lesson 3.3):

import static org.wpilib.units.Units.Milliseconds;
import static org.wpilib.units.Units.Seconds;

Time autoDelay = Seconds.of(2);        // 2 seconds
Time nudge = Milliseconds.of(100);     // 0.1 seconds

The payoff: a bare withTimeout(5) can't compile, so nobody can ever wonder whether that 5 meant seconds or milliseconds.

Command Compositions: Building Bigger Actions From Smaller Ones

A command composition combines several commands into one — and since a composition is itself a Command, compositions can be nested arbitrarily deep. Like single commands, compositions are built with builders, so they also end in .named("...") — or .withAutomaticName(), which names the group after its members. Three shapes matter most:

  • Sequential — runs each command in order, one after another, finishing when the last one finishes:
    fooCommand.andThen(barCommand).withAutomaticName(); // runs fooCommand, then barCommand
    
  • Parallel — runs several commands at once, finishing once all of them finish:
    Command.parallel(twoSec, oneSec, threeSec).named("All Three"); // ends after 3 seconds
    
  • Race — runs several commands at once, finishing as soon as any one of them finishes (canceling the rest):
    Command.race(twoSec, oneSec, threeSec).named("First Done"); // ends after 1 second
    

Command.sequence(a, b, c) builds a sequence from a list, and a.alongWith(b) and a.raceWith(b) are the chained forms of parallel and race. Two decorators add extra end conditions to any command:

raise.until(limitSwitch::get).named("Raise to Switch"); // ends early if the switch trips
raise.withTimeout(Seconds.of(5));                       // ends after 5 s, no matter what

(withTimeout returns a finished, automatically named Command, so it needs no .named(...).)

Requirements of a Composition, and the await Alternative

A built-in composition requires every mechanism any of its members requires, for its entire duration — a sequence that uses the intake, then the indexer, then the shooter holds all three the whole time, even while two of them are sitting unused. (Two members of one parallel group can't require the same mechanism at all; building such a group throws an error.)

v3 offers a finer-grained alternative: a command with no requirements of its own that awaits other commands from inside its body. Each awaited command claims its mechanism only while it's actually running:

Command.noRequirements(coroutine -> {
    coroutine.await(intake.intakeCommand());   // intake is claimed only during this step
    coroutine.await(shooter.fireCommand());    // then the shooter, only during this step
}).named("Intake Then Fire");

coroutine.awaitAll(...) and coroutine.awaitAny(...) wait for several commands at once, and coroutine.fork(...) starts commands without waiting for them. A command started this way can never outlive the command that started it.

Common Pitfalls

  • A loop that never calls coroutine.yield(). The scheduler runs one command at a time, and a command only gives control back when it yields. A while loop without coroutine.yield() never lets anything else run — not other commands, not trigger checks, not the rest of your robot code — so the whole robot hangs.
  • Writing yield() without coroutine. in front. yield is a restricted word in Java (switch expressions use it, Lesson 5.12), so a bare yield(); is a compile error; the call must be written coroutine.yield().
  • Using a coroutine that isn't yours. Each command's body gets its own coroutine; calling methods on a different command's coroutine (for example, one captured by a lambda from an outer command) throws an IllegalStateException.
  • Forgetting that a sequential composition needs every step to actually finish. If one command in a sequence never finishes — a loop that never ends, or a park() — every command after it in the sequence never runs.

Key Takeaways

  • A v3 command is one body: code before the loop runs once at the start, the loop body runs once per scheduler loop (ending with coroutine.yield()), and code after the loop runs once at the end.
  • Commands are built with staged builders, and every command must be named with .named(...) — a missing name is a compile error.
  • Requirements come from how a command is built: mechanism.run(...), Command.requiring(...), or Command.noRequirements(...).
  • Built-in factories (run, runRepeatedly, idle, Command.waitFor, Command.waitUntil) and coroutine helpers (yield, wait, waitUntil, park) cover most commands in one statement; whenExited(...) handles cleanup.
  • Durations are Time values, like Seconds.of(2) — never plain numbers.
  • Compositions (sequence, parallel, race) combine commands into bigger ones and are themselves ordinary commands; coroutine.await(...) lets a command use each mechanism only while it needs it.

Derived from other-reference-repo: wpilib/command-based/02-commands.md, 03-command-compositions.md (WPILIB 20.2-3); updated for Commands v3 from allwpilib main (commandsv3/…/command3/Command.java, Coroutine.java, Mechanism.java, NeedsNameBuilderStage.java, SequentialGroupBuilder.java, ParallelGroupBuilder.java)