Inline Commands vs. Named Command Classes
Not every command needs its own file. WPILib's factory methods let you express simple, one-shot actions in a single line directly in RobotContainer. Knowing when to write a named class and when to write an inline command is what separates clean codebases from cluttered ones.
By the end of this lesson, you will:
- Use
Commands.runOnce(),Commands.run(), andCommands.print()to express simple behaviors inline - Explain what a Java lambda is and how it captures subsystem references
- Apply command decorators:
.withTimeout(),.unless(),.onlyIf(), and.andThen() - State the decision rule: when to use inline vs. a named
CommandBasesubclass - Recognize code smell: an inline command that is too long and should become a named class
The Problem with a File for Everything
In Lesson 4 you created IntakeCommand.java — a full class file with a constructor, four lifecycle methods, and 35+ lines of code. That was the right choice for a sensor-terminated, multi-step behavior.
But consider this: you also need a command that zeros the gyro heading. It's one line: m_gyro.reset(). Creating ZeroGyroCommand.java — 35 lines of boilerplate wrapping one line of logic — wastes time, clutters the commands/ folder, and makes the codebase harder to navigate. WPILib anticipated this. The Commands factory class exists precisely for this case.
The Commands Factory: Your Inline Toolkit
The Commands class (in edu.wpi.first.wpilibj2.command.Commands) provides static factory methods that create fully functional Command objects from lambdas — no class declaration required. These are sometimes called functional commands or inline commands.
| Factory Method | What It Creates | isFinished()? |
|---|---|---|
| Commands.runOnce(action, subsystems...) | Runs the lambda once, then ends immediately | Always true after first execute |
| Commands.run(action, subsystems...) | Runs the lambda repeatedly — never ends on its own | Always false |
| Commands.print(message) | Prints a string to the console and ends | Always true |
| Commands.waitSeconds(seconds) | Does nothing for the given duration, then ends | True after elapsed time |
| Commands.waitUntil(condition) | Does nothing until the boolean supplier returns true | True when condition is true |
| Commands.none() | Ends instantly — a no-op placeholder | Always true immediately |
// Zero the gyro heading on Start button press
new JoystickButton(m_driver, XboxController.Button.kStart.value)
.onTrue(Commands.runOnce(
() -> m_gyro.reset(),
m_gyro // subsystem requirement
));
// Hold B to run intake — stop when released (whileTrue cancels)
new JoystickButton(m_operator, XboxController.Button.kB.value)
.whileTrue(Commands.run(
() -> m_intake.setSpeed(0.7), m_intake
));
// Simplified default drive using Commands.run()
m_drive.setDefaultCommand(Commands.run(
() -> m_drive.arcadeDrive(
-m_driver.getLeftY(), m_driver.getRightX()),
m_drive
));
}
Every Commands.run() and Commands.runOnce() call that touches a subsystem must declare that subsystem as a requirement — just like addRequirements() in a named command. Omitting it means the scheduler doesn't know this inline command uses the subsystem, and two commands can fight over it silently. The lambda captures the subsystem reference; the second argument to the factory method registers the requirement. Both are required.
Command Decorators: Modifying Behavior Without Subclassing
WPILib's Command interface supports a decorator pattern — you can chain modifiers onto any command (inline or named) to change how it behaves, without editing the command itself.
new IntakeCommand(m_intake).withTimeout(3.0);
// onlyIf: only schedule if the intake doesn't already have a piece
new IntakeCommand(m_intake).onlyIf(
() -> !m_intake.hasGamePiece());
// unless: run the intake unless currently ejecting
new IntakeCommand(m_intake).unless(
() -> m_intake.isEjecting());
// andThen: after zeroing gyro, print confirmation
Commands.runOnce(() -> m_gyro.reset(), m_gyro)
.andThen(Commands.print("Gyro zeroed"));
// withName: labels the command in the Scheduler widget
new IntakeCommand(m_intake).withName("IntakeWithTimeout").withTimeout(3.0);
The Decision Rule: Inline or Named?
Use this tool to think through the decision for a specific behavior.
When I look at a team's RobotContainer.java, I want to see it read like a control map. Short inline commands for simple one-shot actions, named commands for complex behaviors, decorators for safe fallbacks. When I see 200-line lambdas inside configureButtonBindings(), I know the programmer learned inline commands and over-applied them. The rule is honest: if you're reading a lambda and thinking "this needs a comment to explain what it does," it belongs in a named class.
When using Commands.run() as a default drive command, verify that the lambda correctly applies a deadband. A bare () -> m_drive.arcadeDrive(m_driver.getLeftY(), ...) without MathUtil.applyDeadband() will drift at rest — the same bug from Unit 5, Lesson 9. Inline commands are just as susceptible to this as named ones. The lambda captures whatever logic you give it, including bad logic.
Knowledge Check
1. You use Commands.run(() -> m_intake.setSpeed(0.7), m_intake) bound with .whileTrue(). When the button is released and the command is cancelled, what happens to the intake motor?
2. A teammate writes: Commands.runOnce(() -> m_gyro.reset()) with no second argument. What is the risk?
3. You have an inline Commands.run() lambda that has grown to include a state variable, a timer, and a sensor check across 20 lines inside configureButtonBindings(). What should you do?
Refactor to Inline Where Appropriate
- Convert the gyro zero binding from Lesson 4 (if you made it a named command) into a
Commands.runOnce()inline. Verify it still appears in the Scheduler widget with the correct name — add.withName("ZeroGyro")if it doesn't. - Replace your
DriveWithJoysticksCommanddefault command with aCommands.run()inline equivalent including deadband. Run both versions and confirm behavior is identical. Then decide which you prefer for your codebase and add a comment explaining why. - Add a
.withTimeout(3.0)decorator to yourIntakeCommandbinding. Simulate a broken sensor (hardcodehasGamePiece()to returnfalse). Confirm the command ends after 3 seconds. Remove the hardcode and restore normal behavior. - Add an
.onlyIf(() -> !m_intake.hasGamePiece())decorator to the intake binding. Confirm that pressing the button when a game piece is already loaded does nothing. This is competition-level safety behavior in two lines. - Stretch goal: Add a
.finallyDo(() -> m_intake.stop())decorator to aCommands.run()intake binding so the motor always stops when the button is released — solving the Q1 problem. Compare this pattern to writing a named command withstop()inend()and add a comment explaining which you'd use in production and why.