Actions 101 Features Actions Utilities Examples Setup View on GitHub →
Team Undefined  ·  #19112  ·  v0.2.1

Defined.

Stop writing tangled OpMode loops.
Defined gives your FTC robot a real action architecture —
composable, cancellable, and conflict-safe by design.

implementation "com.teamundefined:defined-core:0.2.1"

What is Defined?

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.

01
Action System Composable units of work — one-shot, sequential, parallel, or conditional — that can require exclusive access to subsystems.
02
Robot Lifecycle Structured hooks for init, start, loop, and stop phases so your code runs at the right time, every time.
03
Action Runner Manages scheduling, cancellation, and slot-based conflict resolution so two actions never fight over the same motor.
04
Performance Tools Section profiling, system monitoring, I2C scheduling, and background-threaded telemetry out of the box.

What is an Action?

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.

One loop. Many actions moving. the robot loops ~50× per second
flywheel_monitor Monitor
never stops
drive_to_goal Action
finishes, then frees the wheels
smart_intake Action
starts partway through

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.

runner.addMonitor()

Monitors

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.

runner.startGroup()

Triggered actions

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.

The Runner stops actions from fighting

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.

In Autonomous

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.

In TeleOp

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.

Everything you need,
nothing you don't

Composable Actions

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.

one-shot sequential parallel toggle conditional

Robot Lifecycle

Override clean hooks — init(), initUpdate(), start(), preUpdate(), update(), stop() — and let the base class call them at exactly the right moment.

init start loop stop

Slot Conflict Resolution

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.

scheduling cancellation slot locks

Performance Tools

SectionProfiler tracks per-cycle timings. SystemMonitor watches CPU and loop time. HardwareScheduler staggers I2C reads. TelemetrySnapshot formats telemetry on a background thread.

profiling I2C telemetry monitoring

Central Config

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.

live tuning @Configurable one place

Pre-Start Menu

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.

driver station alliance color no re-upload

Seven action types.
Infinite possibilities.

Mix and match these primitives to build any robot behavior — from a simple button press to a full multi-subsystem autonomous sequence.

One-Shot Actions

Run a single lambda once and complete immediately. Perfect for setting servo positions, toggling motor power, or any instantaneous state change.

IntakeActions.java
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());
}

Conditional Actions

Run every cycle until a condition is met. Attach .withOnCancel() for cleanup if interrupted before the condition fires.

IntakeActions.java
Action.until("manual_intake_on",
    now -> r.intake.start(false),
    () -> false   // runs until cancelled
)
.requires(Subsystem.INTAKE)
.withOnCancel(now -> r.intake.stop());

Toggle Actions

Alternate between two actions each time a button is pressed. Register as a monitor — the runner checks it every cycle automatically.

MainTeleOp.java
runner.addMonitor(
    ToggleAction.onPress(
        "intake_toggle",
        () -> gamepad1.squareWasReleased(),  // button supplier
        startIntake(robot, false),           // first press
        stopIntake(robot)                   // second press
    )
);

While-Pressed Actions

Run an action while a button is held, then automatically run a cleanup action on release. Suppliers return fresh instances each press cycle.

MainTeleOp.java
runner.addMonitor(new WhilePressedAction(
    "manual_intake",
    () -> gamepad1.right_trigger_pressed,
    runner,
    () -> startIntake(robot, false).requires(Subsystem.INTAKE),
    () -> stopIntake(robot).requires(Subsystem.INTAKE)
));

Sequential Actions

Run a list of actions one after another. Each child action must complete before the next one starts.

OuttakeActions.java
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);

Parallel Actions

ParallelAction.all() runs multiple actions simultaneously and completes when all children are done — great for multi-mechanism moves.

ShootingActions.java
ParallelAction.all("parallel_start",
    IntakeActions.startIntake(r, false),
    TransferActions.startTransfer(r),
    TransferActions.unlockTransfer(r)
);

Composing Complex Sequences

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.

ShootingActions.java
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);
}

Performance tools,
out of the box

SectionProfiler

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);

SystemMonitor

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);

HardwareScheduler

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()
);

TelemetrySnapshot

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());

Up and running
in three steps

1

Add the Maven dependency

In your TeamCode/build.gradle, add the Defined repository and the packages you need.

build.gradle
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
}
2

Sync Gradle & copy the quickstart

Sync your project in Android Studio, then clone or copy the files from the quickstart repository into your TeamCode module.

3

Replace example subsystems with your hardware

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.

1. Subsystem slot 2. Config values 3. Subsystem class 4. Register in Robot 5. Create actions 6. Wire in OpMode

Ready to get started?

Defined is open-source and free to use. Grab the quickstart, read the code, and adapt it to your team's hardware.

Real code from
a real robot

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.

MainTeleOp.java
public class MainTeleOp extends BaseOpMode {
    public void addMonitors() {
        // Keep flywheel on.
        if (Config.Flywheel.ALWAYS_ON) {
            runner.addMonitor(FlywheelActions.flywheelAlwaysOn(robot));
        }
    }
}