Unit 6 · Lesson 5

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(), and Commands.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 CommandBase subclass
  • 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 MethodWhat It CreatesisFinished()?
Commands.runOnce(action, subsystems...)Runs the lambda once, then ends immediatelyAlways true after first execute
Commands.run(action, subsystems...)Runs the lambda repeatedly — never ends on its ownAlways false
Commands.print(message)Prints a string to the console and endsAlways true
Commands.waitSeconds(seconds)Does nothing for the given duration, then endsTrue after elapsed time
Commands.waitUntil(condition)Does nothing until the boolean supplier returns trueTrue when condition is true
Commands.none()Ends instantly — a no-op placeholderAlways true immediately
RobotContainer.java — inline commands in button bindings
private void configureButtonBindings() {

  // 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
  ));
}
← click a highlighted token
⚠️ The subsystem argument is not optional

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.

Decorator examples — chain on any Command
// Timeout: IntakeCommand ends after 3s even if sensor never fires
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);
← click a highlighted token

The Decision Rule: Inline or Named?

Use this tool to think through the decision for a specific behavior.

Named class vs. inline — select your scenario
What does the behavior look like?
← select a scenario
🔍 LRI Perspective: "The RobotContainer tells me your architecture"

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.

🔌 System Check

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?

  • A The motor stops automatically — Commands.run() always stops the motor on cancel.
  • B The motor keeps running — Commands.run() has an empty end() by default. You must chain a stop call or use .finallyDo(() -> m_intake.stop()).
  • C The motor stops because .whileTrue() automatically calls stop() on the subsystem when the button is released.
  • D The scheduler throws an exception because inline commands cannot be used with whileTrue().

2. A teammate writes: Commands.runOnce(() -> m_gyro.reset()) with no second argument. What is the risk?

  • A runOnce requires a second argument or it throws a NullPointerException.
  • B The command has no subsystem requirement. Another command can freely use GyroSubsystem simultaneously, including one that assumes it hasn't been reset mid-run.
  • C No risk — sensor-only subsystems like gyros don't need to be declared as requirements.
  • D The command won't compile without a subsystem argument.

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?

  • A Split it into multiple chained .andThen() calls within the same lambda.
  • B Refactor it into a named CommandBase subclass. If a lambda needs state, a timer, and a sensor check, it has outgrown inline. A named class can use all four lifecycle methods cleanly and be tested in isolation.
  • C Extract the logic into a private method in RobotContainer and call that method from the lambda.
  • D Inline commands have no size limit — keep it in the lambda as long as it works.
💪 Practice Prompt

Refactor to Inline Where Appropriate

  1. 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.
  2. Replace your DriveWithJoysticksCommand default command with a Commands.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.
  3. Add a .withTimeout(3.0) decorator to your IntakeCommand binding. Simulate a broken sensor (hardcode hasGamePiece() to return false). Confirm the command ends after 3 seconds. Remove the hardcode and restore normal behavior.
  4. 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.
  5. Stretch goal: Add a .finallyDo(() -> m_intake.stop()) decorator to a Commands.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 with stop() in end() and add a comment explaining which you'd use in production and why.