Stop writing tangled OpMode loops.
Defined gives your FTC robot a real action architecture —
composable, cancellable, and conflict-safe by design.
A getting-started template and action engine for FTC teams. Defined gives your robot code structure, safety, and composability — so you spend less time debugging loops and more time winning matches.
An action is one job your robot is doing — driving to a target, spinning up the flywheel, pulling in a ball. The important part: an action doesn't stop everything else while it runs. It moves forward a little bit on every cycle of the robot loop, so many actions make progress at the same time.
Each square is one cycle of the robot loop. The flywheel monitor never stops. Driving finishes after six cycles and lets go of the wheels. Intake starts partway through and keeps going — and none of them wait for the others.
There's only one type in the library — Action. What changes is how you hand it to the Runner.
Hand an action to addMonitor() and it becomes a monitor: it runs every single cycle, for the whole match, and never finishes on its own. Keep the flywheel spinning. Rumble the gamepad when a ball drops in. Register it once at startup and forget about it.
Monitors run first each cycle, before everything else, so they can jump in and override what the robot is doing.
Everything else goes through startGroup(): it does a job and ends. Drive to the corner. Intake three balls. These claim the subsystems they need, can be chained one after another, run side by side, or given a timeout and a backup plan for when something goes wrong.
Because they claim subsystems, these are the ones the Runner can cancel and replace.
Two actions must never grab the same motor at the same moment. So every action says what it needs up front — .requires(ActionSlot.INTAKE).
When a new action asks for a subsystem that's already busy, the Runner cancels the old one, lets it clean up after itself, and hands the subsystem over. Newest request wins. You never write that bookkeeping yourself — you just declare what you need.
You build one long chain of actions ahead of time and let it run — drive, shoot, fetch, park. Monitors run alongside it the entire time, keeping the flywheel ready and watching for trouble.
The same (reused!) actions bind to gamepad buttons. One press can fire a whole chain — go to the goal, line up, shoot — while the driver keeps steering.
And you can bind a panic button: one press cancels every running action at once so the driver takes back full manual control. Monitors keep running on purpose — safety behaviour should never switch itself off.
Chain one-shot, sequential, parallel, conditional, toggle, and while-pressed actions into any behavior your robot needs. Each action declares which subsystem slots it owns.
Override clean hooks — init(), initUpdate(), start(), preUpdate(), update(), stop() — and let the base class call them at exactly the right moment.
The ActionRunner automatically cancels any running action when a new action requests the same subsystem slot. No manual tracking — declare .requires(Subsystem.INTAKE) and the runner handles the rest.
SectionProfiler tracks per-cycle timings. SystemMonitor watches CPU and loop time. HardwareScheduler staggers I2C reads. TelemetrySnapshot formats telemetry on a background thread.
Every tunable parameter lives in one Config.java. The @Configurable annotation exposes fields to the Panels dashboard for live tuning — change values without uploading new code.
An interactive menu on the Driver Station during INIT — select alliance color, toggle debug flags, and configure match settings without touching code. Changes take effect when you press START.
Mix and match these primitives to build any robot behavior — from a simple button press to a full multi-subsystem autonomous sequence.
Run a single lambda once and complete immediately. Perfect for setting servo positions, toggling motor power, or any instantaneous state change.
public static Action startIntake(Robot r, boolean reversed) {
return Action.oneShot("intake_start", now -> r.intake.start(reversed));
}
public static Action stopIntake(Robot r) {
return Action.oneShot("intake_stop", now -> r.intake.stop());
}
Run every cycle until a condition is met. Attach .withOnCancel() for cleanup if interrupted before the condition fires.
Action.until("manual_intake_on",
now -> r.intake.start(false),
() -> false // runs until cancelled
)
.requires(Subsystem.INTAKE)
.withOnCancel(now -> r.intake.stop());
Alternate between two actions each time a button is pressed. Register as a monitor — the runner checks it every cycle automatically.
runner.addMonitor(
ToggleAction.onPress(
"intake_toggle",
() -> gamepad1.squareWasReleased(), // button supplier
startIntake(robot, false), // first press
stopIntake(robot) // second press
)
);
Run an action while a button is held, then automatically run a cleanup action on release. Suppliers return fresh instances each press cycle.
runner.addMonitor(new WhilePressedAction(
"manual_intake",
() -> gamepad1.right_trigger_pressed,
runner,
() -> startIntake(robot, false).requires(Subsystem.INTAKE),
() -> stopIntake(robot).requires(Subsystem.INTAKE)
));
Run a list of actions one after another. Each child action must complete before the next one starts.
return new SequentialAction("dump_and_retract", List.of(
Action.oneShot("dump", now -> r.outtake.dump()),
WaitAction.ms("wait", 500),
Action.oneShot("home", now -> r.outtake.home()),
Action.oneShot("retract", now -> r.outtake.retract())
)).requires(Subsystem.OUTTAKE);
ParallelAction.all() runs multiple actions simultaneously and completes when all children are done — great for multi-mechanism moves.
ParallelAction.all("parallel_start",
IntakeActions.startIntake(r, false),
TransferActions.startTransfer(r),
TransferActions.unlockTransfer(r)
);
Real match behaviors combine all types. Here's the full shooting sequence — sequential steps wrapping parallel groups, with a timeout and slot requirements on the whole thing.
public static Action shootingSequence(Robot r) {
List<Action> steps = new ArrayList<>();
steps.add(FlywheelActions.flywheelSpinUp(r));
steps.add(ParallelAction.all("start",
IntakeActions.startIntake(r, false),
TransferActions.startTransfer(r),
TransferActions.unlockTransfer(r)));
steps.add(WaitUntilAction.until("wait_exit", r::noBallsDetected));
steps.add(WaitAction.ms("delay", Config.Time.BALLS_EXIT_DELAY));
steps.add(ParallelAction.all("stop",
IntakeActions.stopIntake(r),
TransferActions.stopTransfer(r),
TransferActions.lockTransfer(r)));
return new SequentialAction("shooting_sequence", steps)
.withTimeout(Config.Time.SHOOTING_TIMEOUT)
.requires(Subsystem.DRIVE, Subsystem.INTAKE);
}
Measures how long specific code sections take per cycle. Wrap any block to find performance bottlenecks with formatted per-section stats in telemetry.
profiler.start(Section.PRE_UPDATE);
// ... code to profile ...
profiler.start(Section.SUBSYSTEMS);Tracks cycle time, CPU usage, and other system-level metrics. Supports a smoothing factor and can be toggled via a Config flag without code re-upload.
new SystemMonitor(0.8);
addCpu(systemMonitor);
systemMonitor.enabledWhen(() -> Config.Debug.ACTIVE);Staggers I2C reads across multiple loops to prevent bus congestion. Register sensors with a per-sensor update interval — reads one at a time, on a configurable schedule.
hardware.register(
Read.INTAKE_CURRENT,
Config.Intervals.CURRENT_MS,
() -> intake.getCurrent()
);Telemetry data is formatted on a background thread so it never slows your main loop. Override fillSnapshot() to add values — refresh rate is configurable.
snapshot.put("Intake", intake.isPowered());
snapshot.putDouble("Amps", amps, 2);
snapshot.put("CPU", monitor.getStats());In your TeamCode/build.gradle, add the Defined repository and the packages you need.
repositories {
maven { url 'https://cstahie.github.io/defined' }
}
dependencies {
implementation "com.teamundefined:defined-core:0.2.1"
implementation "com.teamundefined:defined-ftc:0.2.1" // FTC glue
implementation "com.teamundefined:defined-pedro:0.2.1" // Pedro actions
}
Sync your project in Android Studio, then clone or copy the files from the quickstart repository into your TeamCode module.
Follow the six-step pattern for every new mechanism: add a slot → add config → create the subsystem class → register in Robot → create actions → wire in OpMode. The pattern stays the same whether you have two mechanisms or twelve.
Defined is open-source and free to use. Grab the quickstart, read the code, and adapt it to your team's hardware.
Every snippet below is running (simplified :P) on Team Undefined's award winning competition robot — not a toy demo.
One line hands a behaviour to the runner. It now runs every loop, forever, without you touching it again.
public class MainTeleOp extends BaseOpMode {
public void addMonitors() {
// Keep flywheel on.
if (Config.Flywheel.ALWAYS_ON) {
runner.addMonitor(FlywheelActions.flywheelAlwaysOn(robot));
}
}
}
Continuous.forever runs every cycle for the life of the OpMode. The guard clause is ordinary Java — no state machine required.
public class FlywheelActions {
public static Action flywheelAlwaysOn(Robot r) {
return Continuous.forever("flywheel_monitor", now -> {
// Just keep the wheel enabled; updateFlywheelPolicy() picks the
// target each loop. Don't run while the climbing jack is extended.
if ((!r.flywheel.enabled || r.flywheel.currentState == IDLE)
&& !r.jack.isExtended) {
r.flywheel.startFlywheelSync();
}
});
}
}
ParallelAction.any races the script against the flywheel monitor. Whichever finishes first ends the block.
public static Action autonomousRoutine(Robot r) {
List<Action> sequence = new ArrayList<>();
Config.StartPosition startPos = Config.startPosition;
sequence.add(
ParallelAction.any("autonomy",
// start the script
startPos == Config.StartPosition.FAR ? farSideScript(r) : nearSideScript(r),
// keep the flywheel always on
FlywheelActions.flywheelAlwaysOn(r)
)
);
sequence.add(Action.oneShot("stop_flywheel", now -> r.flywheel.stopFlywheelSync()));
return new SequentialAction("autonomous_routine", sequence.toArray(new Action[0]));
}
tryOnce contains the damage: if intake times out, the robot still drives off and shoots.
public static Action fetchAndShootHP2(Robot r) {
List<Action> actions = new ArrayList<>();
actions.add(
ParallelAction.all("hp2_corner_and_prep_harvest",
DriveActions.auto_navigateToHP2_Start(r), // go to corner
prepareHarvest(r, "harvest_in_front")
)
);
// Wrapped in tryOnce so an intake timeout doesn't block goShootEm
actions.add(TryAction.tryOnce("try_hp2_slide_intake",
ParallelAction.all("HP2_slide_and_intake",
Nav.forAuto("slide_hp2", r, Poses.Paths.getHP2SlidePath(r.drive)),
IntakeActions.smartIntakeStartFresh(r, true)
.withTimeout(Poses.Constraints.TIMEOUT_INTAKE_FAILED)
.withOnStart(now -> ULog.i(TAG, () -> "Intaking balls"))
.withOnTimeout(now -> IntakeActions.cleanup_after_intake(r))
)
));
actions.add(goShootEm(r, null, DriveActions.auto_navigateToShootingFAR(r)));
return new SequentialAction("fetch_shoot_HP2", actions.toArray(new Action[0]));
}
The payoff feature: .requires() claims INTAKE, TRANSFER and INDEXER. If any other action asks for one of them, the runner cancels this one and .withOnCancel() leaves the hardware in a safe state — no manual bookkeeping.
public static Action smartIntakeStartFresh(Robot r, boolean leaveIntakeON) {
SequentialAction seq = new SequentialAction("smart_intake");
seq.then(Action.oneShot("trigger_distance_detectors", now -> r.indexer.scanForBalls(true)));
seq.then(Action.oneShot("close_gates_no_wait", now -> r.indexer.setServoPositionAll(GATE_INTAKE_POSITION)));
seq.then(Action.oneShot("lock_transfer", now -> r.transfer.lock()));
seq.then(new ParallelAction("intake_and_monitor",
ParallelAction.CompletionMode.ALL,
Action.oneShot("start_intake", now -> r.intake.turnOnSync(false)),
new SequentialAction("intake_and_wait",
new WaitUntilAction("monitor_balls", () -> r.indexer.hasAtLeastOneBall())
.withTimeout(2000),
TryAction.tryOnce("try_wait_for_last_2_balls",
new WaitUntilAction("wait_for_last_2_balls", () -> r.indexer.areAllBallsDetected())
.withTimeout(300))
)
));
seq.then(Action.oneShot("gulp_gates", now -> r.indexer.setServoPositionAll(GATE_GULP_POSITION)));
seq.then(Action.oneShot("stop_intake", now -> cleanup_after_intake(r, leaveIntakeON)));
// Claim the three subsystems this action drives. If anything else asks for
// them, the runner cancels this action and withOnCancel puts hardware back.
return seq
.requires(ActionSlot.INTAKE)
.requires(ActionSlot.TRANSFER)
.requires(ActionSlot.INDEXER)
.withOnCancel(now -> cleanup_after_intake(r, leaveIntakeON));
}