Structuring a Command-Based Robot Project

The Standard Project Shape

WPILib 2027's official Commands v3 project template organizes a robot around a thin Robot class, one class per opmode, and two packages:

Robot.java          the entry point: mechanisms, controllers, and the scheduler call
ExampleAuto.java    an autonomous opmode, marked @Autonomous
ExampleTeleop.java  a teleop opmode, marked @Teleop
constants/          one small constants class per topic, like DriverConstants
mechanisms/         one file per mechanism, like ExampleMechanism

An opmode is one selectable mode of robot behavior — "Score and Back Up" or "Two-Piece Auto" for autonomous, "Driver Teleop" for teleop. The drive team picks one on the Driver Station before enabling. Package names are lowercase (constants, mechanisms), like every Java package.

Alpha note: this OpModeRobot framework is new for 2027 (it's been in the template since June 2026), so details may still shift before the season.

Robot: Deliberately Thin

Robot extends WPILib's OpModeRobot. It creates the mechanisms and controllers, sets the default commands that apply in every mode, and runs the scheduler — and almost nothing else:

public class Robot extends OpModeRobot
{
    final CommandXboxController driverController =
        new CommandXboxController(DriverConstants.DRIVER_CONTROLLER_PORT);
    final Intake intake = new Intake(driverController.a(), driverController.b());
    final Drive drive = new Drive();
    final Shooter shooter = new Shooter();

    public Robot()
    {
        intake.setDefaultCommand(intake.stateMachineCommand()); // Lesson 25.5's machine
    }

    @Override
    public void robotPeriodic()
    {
        Scheduler.getDefault().run(); // the most important line in the project
    }
}

The fields are final, so nothing can ever swap in a different intake, and they deliberately have no access modifier: package-private access (Ch.23) lets the opmode classes in the same package read robot.intake, while code outside the package can't. The controller is declared first because the intake's constructor uses its buttons — field initializers run top to bottom.

Piling large amounts of imperative logic directly into Robot.java fights the entire declarative philosophy (Lesson 25.1) — mode-specific setup belongs in the opmodes.

OpModes: One Class per Mode

An opmode is a class that implements WPILib's OpMode interface and is marked with an annotation — @Autonomous or @Teleop — so the framework finds it automatically. Its constructor receives the Robot, which is how it reaches the mechanisms:

@Teleop
public class DriverTeleop implements OpMode
{
    public DriverTeleop(Robot robot)
    {
        robot.driverController.rightBumper().whileTrue(robot.intake.ejectCommand());
    }
}

The framework constructs an opmode when it's selected on the Driver Station. Bindings made in its constructor belong to that opmode's scope (Lesson 25.3): they only work while it's selected, so each teleop opmode can map the buttons its own way. An autonomous opmode usually schedules its routine in start(), which runs once, when the robot is enabled:

@Autonomous(name = "Drive Then Shoot")
public class DriveThenShoot implements OpMode
{
    private final Command routine;

    public DriveThenShoot(Robot robot)
    {
        routine = Autos.driveThenShoot(robot.drive, robot.shooter);
    }

    @Override
    public void start()
    {
        Scheduler.getDefault().schedule(routine);
    }
}

Because the routine is scheduled inside the autonomous opmode, the scheduler cancels it automatically when autonomous ends — there's no separate "cancel autonomous when teleop starts" step. The name shown on the Driver Station defaults to the class name if you leave it out.

Passing the Robot into each opmode's constructor is dependency injection (first seen in Lesson 25.2's factories): each opmode is handed exactly what it works with, instead of reaching for global variables.

Constants: One Place for Every Tunable Number

public final class IntakeConstants
{
    public static final int MOTOR_CHANNEL = 5; // PWM channel
    public static final double INTAKE_SPEED = 0.8;
}

Every constant is public static final and named in ALL_CAPS — the constants convention from Lesson 7.4, which WPILib 2027's own template uses too (DRIVER_CONTROLLER_PORT). The template gives each topic its own small class in the constants package; marking the class final means nothing can extend it. Changing one tunable value, like the intake speed, means editing exactly one line.

Choosing How to Define a Reusable Command

For a command that's used in more than one place (a button binding, an autonomous routine, a self-test), a few options exist, each suited to a different case:

  • Instance factory method on a single mechanism (the run(...) factories from Lesson 25.2) — best for a command tied to exactly one mechanism: shooter.spinUpCommand().
  • Static factory method (Ch.21) — best when a command needs to coordinate multiple mechanisms at once, so it doesn't naturally belong to any single one of them.
  • A class that implements Command — best when a command needs its own genuine internal state (not mechanism state) or unusually complex logic.
public class Shooter implements Mechanism
{
    // built with run(...), so it implicitly requires `this` shooter
    public Command spinUpCommand()
    {
        return run(coroutine -> {
            setSpeed(ShooterConstants.SPIN_SPEED);
            coroutine.park();
        }).whenExited(() -> setSpeed(0.0)).named("Spin Up");
    }
}
public final class Autos
{
    public static Command driveThenShoot(Drive drive, Shooter shooter)
    {
        return Command.sequence(
            drive.driveForwardCommand().withTimeout(Seconds.of(2)),
            shooter.shootCommand()
        ).named("Drive Then Shoot");
    }
}

A multi-mechanism routine can also be written in the coroutine.await(...) style from Lesson 25.2, so each mechanism is claimed only while its step runs.

Reading Commands v2 Code (2026 and Earlier)

Commands v2 is what most existing team code — including older code of our own — still uses, and it's what most examples online show. This section is for recognizing v2 code when you read it, not for writing new code: new code in this course is v3. (In 2026 and earlier, v2 lives in the package edu.wpi.first.wpilibj2.command; WPILib 2027 still ships it as org.wpilib.command2 for teams that haven't moved over yet.)

Here is one v2 mechanism and one v2 command class, with notes on what each piece became:

// Commands v2 (2026 and earlier): for recognizing, not for writing new code
public class Intake extends SubsystemBase    // v3: implements Mechanism
{
    public Command runCommand()
    {
        // v3: run(...) with park(), plus whenExited(...), plus .named(...)
        return startEnd(() -> set(0.8), () -> set(0.0));
    }
}

public class FeedUntilLoaded extends Command  // v3: one factory method with one body
{
    private final Intake intake;

    public FeedUntilLoaded(Intake intake)
    {
        this.intake = intake;
        addRequirements(intake);              // v3: requirements come from the builder
    }

    @Override
    public void initialize() { intake.set(0.6); }            // v3: code before the loop

    @Override
    public boolean isFinished() { return intake.hasPiece(); } // v3: the loop condition

    @Override
    public void end(boolean interrupted) { intake.set(0.0); } // v3: whenExited(...)
}
In v2 code you'll seeIn v3 you write
extends SubsystemBaseimplements Mechanism (an interface)
initialize(), execute(), isFinished(), end(interrupted)one coroutine body: code before the loop, a loop ending in coroutine.yield(), code after, plus whenExited(...)/whenCanceled(...)
CommandScheduler.getInstance()Scheduler.getDefault()
RobotContainerRobot extends OpModeRobot, plus @Autonomous/@Teleop opmode classes
addRequirements(...)requirements from the builder: mechanism.run(...), Command.requiring(...)
InterruptionBehavior (CANCEL_SELF, CANCEL_INCOMING; kCancelSelf in older code)priorities: .withPriority(...)
Commands.runOnce(() -> ..., mechanism)mechanism.run(coroutine -> ...).named("...")
withTimeout(1.5)withTimeout(Seconds.of(1.5))
a subsystem's periodic() methoda default command (or addPeriodic(...) for data only)
cancel the auto command in teleopInit()nothing to write: scopes cancel it
constants named like kMotorIdconstants named like MOTOR_CHANNEL

Two more v2 habits you'll spot: v2 commands don't have to be named, and v2 compositions like Commands.sequence(...) return a finished command, with no .named(...) at the end.

Common Pitfalls

  • Declaring mechanisms as static fields for easy access. Global mechanisms let any code anywhere call their methods directly, bypassing the scheduler's resource management — opmodes reach them through the Robot passed to their constructor instead.
  • Scheduling an autonomous routine in the opmode's constructor. The constructor runs when the opmode is selected, while the robot is still disabled; schedule the routine in start(), which runs when the robot is enabled.
  • Scattering constants across many files instead of the constants package. Changing one tunable value (like an intake speed) should mean editing exactly one line, not hunting across the codebase.
  • Pasting v2 code into a v3 project. Names like SubsystemBase, Commands.runOnce, and CommandScheduler don't exist in v3; translate the code with the table above instead of mixing the two frameworks.

Key Takeaways

  • WPILib 2027's Commands v3 template has a thin Robot (extends OpModeRobot), one class per opmode (@Autonomous/@Teleop, implementing OpMode), a constants package, and a mechanisms package.
  • Robot.robotPeriodic() must call Scheduler.getDefault().run() — nothing works without this line.
  • Each opmode receives the Robot in its constructor (dependency injection); bindings made there only work while that opmode is selected, and autonomous routines are scheduled in start() and canceled automatically when the mode ends.
  • Constants are public static final, ALL_CAPS, grouped by topic.
  • Choose how to define a reusable command based on its shape: instance factory (one mechanism), static factory (several mechanisms), or a class that implements Command (real internal state or complex logic).
  • Most existing team code is Commands v2: recognize SubsystemBase, the four lifecycle methods, addRequirements, CommandScheduler, and RobotContainer, and translate them to v3 rather than mixing frameworks.

Derived from other-reference-repo: wpilib/command-based/04-subsystems.md (WPILIB 20.4), 06-structuring-command-based-project.md (WPILIB 20.6), 07-organizing-command-based.md (WPILIB 20.7); updated for Commands v3 from allwpilib main (wpilibjExamples/…/templates/commandv3/, hatchbotcmdv3 example, wpilibj/…/framework/OpModeRobot.java, wpilibj/…/opmode/OpMode.java)