Gray Matter LogoGray Matter Workshop

Commands

KEY CONCEPT

Commands with WPILib Commands V3

A coroutine Command is a single method body that the scheduler runs once. Inside the body you write plain Java; whenever the command needs to wait — for time to pass, for a condition to become true, for a sub-command to finish — you explicitly yield to the scheduler through the Coroutine parameter. There's no separate init / execute / isFinished / end to fill in, just the one body.

Most commands you write will be a single line of motor setup wrapped in mech.run(...). The coroutine parameter is there when you need it (.wait, .waitUntil, .await, .park) and ignored when you don't. Roughly 80% of teaching examples never touch it.

↳ TAKEAWAY

A command is a body. You yield through the coroutine wherever you need to wait. Most bodies don't need to.

The default command shape

Most commands live on a mechanism as a thin factory: take some arguments, configure the hardware, hand the resulting Commandback. The body runs once. If it doesn't yield, the command finishes immediately. If it does, it stays scheduled until the yield resolves and the body falls off the end.

Two ways to write a command

The inline run(coroutine -> …) style above is one way to write a command. The other is the explicit initialize / execute / isFinished / end lifecycle: the workshop template ships utils/ClassicCommand, a small base class that gives you those four methods. Extend it, override only the methods you need, and the instance is a Command you can schedule or bind to a trigger. Reach for ClassicCommand when a command has explicit, stateful steps; reach for the inline run(coroutine -> …) body when a single body reads more clearly.

HOW IT MAPS

How the lifecycle runs on the scheduler

Under the hood ClassicCommand runs initialize() once, then loops execute() + isFinished() with a yield each tick. end(false) runs on a natural finish; end(true)runs on cancellation. The base class wires the cancel hook for you, because (as with any v3 command) a cancelled coroutine is dropped and wouldn't otherwise reach your cleanup.

Four shapes cover almost everything

When you're writing a new command, the question to answer first is what does it have to wait for? That answer picks the shape.

SHAPE 1 · SET ONCE

Body sets a value and falls off

No yield, no wait. The command finishes the same tick it's scheduled. Good for fire-and-forget actions where the next command in a chain is responsible for whatever comes next.

run(coroutine -> setVoltage(6)).named(...)
SHAPE 2 · SET AND HOLD

Body sets a value then parks

Yields forever after the setup. The command stays scheduled until something cancels it (a default-command return, a higher-priority command, a button release). Good for "run while held" bindings.

run(coroutine -> { setVoltage(6); coroutine.park(); })
SHAPE 3 · SET THEN WAIT

Body sets a value then waits on a condition

Yields until the predicate goes true. Falls off on its own when the condition is met. The bread-and-butter shape for "move there" commands that need to report when they've arrived.

run(coroutine -> { setPosition(t); coroutine.waitUntil(atTarget); })
SHAPE 4 · MULTI-PHASE

Body awaits child commands in order

Yields for the full duration of each child. The body reads top to bottom like a script. Good when the sequence is fixed and each phase's setup depends only on what came before it.

coroutine.await(a); coroutine.await(b);

For shapes 1, 2, and 3 the coroutine parameter is the only thing that lets the scheduler know when to yield. For shape 4 you chain commands together by awaiting them on the same coroutine, inheriting all of their requirements.

Composing across mechanisms

A routine that touches more than one mechanism is hosted on the mechanism it primarily drives. From inside that mechanism's run(coroutine -> { ... }) body it controls its own hardware directly and awaits or forks the other mechanisms' commands. Each awaited command carries its own requirement, so the scheduler still tracks who owns what.

NOTE

One body coordinates the whole sequence

The body reads top to bottom: setVelocity starts the flywheel and Phoenix holds it while the arm moves, and the await / waitcalls sequence the rest. When you genuinely need a background command that the routine can later cancel, that's what coroutine.fork(...) is for: a forked command is cancelled automatically when the parent body exits.

Beyond static sequences: runtime decisions in command bodies

The four shapes above cover routines that are static: the sequence is known when the command is built. The interesting case is when one phase's outcome decides what the next phase should be. v3 lets the body read a sensor, store the result in a local variable, and branch with plain Java.

The key line is int weight = sensors.readPieceWeight(); followed by a three-way if/else picking between different next phases. Because the whole routine is one method body, a value read in one phase is just a local variable the later phases can branch on.

WHY THIS MATTERS

Write control flow as control flow

Because the routine is a single method body, multi-phase logic reads top to bottom in plain Java: read a sensor into a local, branch with if/else, and await the phase you chose. The coroutine parameter suspends and resumes the body at each await / waitUntil, so there's no phase bookkeeping to maintain.

Cancellation

A command's normal completion is just the body falling off the end. Cancellation cleanup goes in a .whenCanceled(...) hook on the command builder. This matters because a cancelled coroutine is simply dropped: any code after a park() or an unfinished waitUntilnever runs, so a trailing "cleanup" line at the end of the body won't fire on cancellation.

The whenCanceled callback only fires when the command is interrupted. Code that should run only on normal completion goes at the bottom of the body (it runs when the body returns on its own). Because cancellation drops the coroutine before it reaches that point, interrupt cleanup has to live in whenCanceled. Two clear places, one for each way a command can end.

NOTE · API STATUS

This is the WPILib 2027 alpha

The Coroutine API, the staged builder, and the compile-time enforcement of .named(...) run on Java 25 and deploy to SystemCore. The stack is the WPILib 2027 alpha (GradleRIO 2027.0.0-alpha-6), so the exact APIs are still moving between alpha builds.
CHECKPOINT · 4 ITEMS