State machine & trial lifecycle
How a LUIDA experiment progresses from start to end — the states, the transitions, the trial loop.
Every LUIDA experiment runs through a state machine. States are the chunks an experiment naturally splits into — Instructions, Trial - Start, Stimulus, Response, Trial - Rest, End — and transitions are the rules for moving from one to the next. The LUIDA › Configure experiment automation › State Machine editor is where you draw this graph; the StateListeningItemBase.js runtime is what executes it.
The key idea on this page: your scene's behavior is keyed off state_currentID. Items watch this global integer, and react when it changes.
The shape of a LUIDA state machine
A state machine in LUIDA is an ordered list of states with optional self-loops. It is not an arbitrary directed graph — that's a deliberate constraint, because most experiments are sequential.
The list always has three fixed states at known positions:
| State | Position | Role |
|---|---|---|
Trial - Start | first state of the trial loop | Conditions for the next trial are computed when this state begins. |
Trial - Rest | between-trial breather | Runs once between two consecutive trials. Common place to show a fixation cross or "press to continue" prompt. |
End | last state | Terminal. Once reached, the experiment is over for this participant. |
You cannot rename or reorder these three. You can add states between them, repeat a sub-list, set exit timers — that's where customization happens.
A typical Stroop-task state list looks like:
1. Instructions (manual transition: participant reads, presses Start)
2. CalculationTask (auto-transition after 30s, repeats 5 times before
falling through)
3. Trial - Start ← fixed; conditions computed here
4. Stimulus (auto-transition after 1.5s)
5. Response (manual transition: participant clicks; logs answer)
6. Trial - Rest ← fixed; runs once per trial
7. PostExperimentSurvey (qID-bound questionnaire)
8. End ← fixed; terminalThe trial loop is implicit: states 3–6 repeat once per trial, with the trial counter advancing automatically. LUIDA computes how many trials to run from your variables; the loop terminates when the counter hits the total.
What makes states transition
A state changes — state_currentID increments — when one of three things happens:
- Exit timer expires — if you set "Has Exit Time" on the state and gave it a duration, it auto-transitions after that many seconds.
- Explicit signal — something in the world fires the
state_triggerTransitionsignal. The most common source is aLuidaToNextStateGimmicktriggered by a button press, a collision, or a state-listening action timer. - Repeat exhaustion — if a state has "Is Repeated" set and a repeat count, the state loops back to a previous state on transition until the count is reached, then falls through.
There's no fourth way. A state will not advance on its own without one of these three triggers, and you'll see this most often when debugging a stuck experiment: nothing happened, because no exit timer was set and no signal fired.
What runs at each state boundary
Each state has three lifecycle hooks. State-listening items can attach actions to any of them:
- On State Start — runs once, the moment the state becomes active. Use this for: showing a stimulus, starting a timer, logging "trial began."
- During State — runs every frame while the state is active. Use this for: per-frame position updates, watching for input that isn't a CCK trigger, syncing items to participant bones.
- On State Exit — runs once, just before the state changes. Use this for: hiding a stimulus, logging response time, clearing a UI.
LUIDA's state-listening item editor is a grid: rows are states, columns are items. Each cell is "what happens to this item when this state is on / starts / ends." The grid format is dense, but it makes the experimentwide structure easier to skim.
The trial loop in detail
Trials are the unit of repetition. One trial = one pass through the trial-loop subset of the state machine.
stateDiagram-v2 [*] --> Setup Setup --> TrialStart: pNum participants joined TrialStart --> Stimulus: ConditionManager.assignTrial() Stimulus --> Response: exit timer Response --> TrialRest: state_triggerTransition (button press) TrialRest --> TrialStart: trialID < trialCount TrialRest --> End: trialID >= trialCount End --> [*]
When a state transition would move from Trial - Rest back to Trial - Start:
ConditionManager.jsincrementsexp_trialID.ConditionManagerrecomputes theCONDITIONmap for the new trial (which within-subject combination is active this round).- State-listening items running on
Trial - Start's "On State Start" see the newCONDITIONvalues.
This is why you read CONDITION["color"] inside actions and get the right value — it's been swapped in by the manager between rest and the next trial.
When exp_trialID reaches trialCount - 1 and another trial would start:
ConditionManageremitsexp_readyToLeaveTrials. By default, the state machine then advances past the trial loop on the next transition out ofTrial - Rest.
Things that look like states but aren't
A few patterns researchers reach for that aren't states:
- A 1-second pause inside a state — don't make it a separate state. Use the
Sleepaction inside a state-listening action chain. - A "show this for 3 seconds" stimulus — you can make it a state with
Has Exit Time = 3s. You can also use a Sleep action. The state approach is cleaner if many items respond to that 3s window. - A choose-your-own-adventure branch — LUIDA's state machine is linear by design. If you need branching, use
Customized Actionto set a flag and have downstream states gate their behavior on the flag (via conditional actions).
How state interacts with conditions
When a state is active, every state-listening action has access to:
CONDITION["<within-subjects-variable-name>"]— the active value of each within-subjects variable for the current trial.CONDITION["<between-subjects-variable-name>"]— the value assigned to this participant for the whole session.PARTICIPANTS[1],PARTICIPANTS[2], ... — the player handles, 1-indexed.
So a Stroop trial's "Show stimulus" action might look like:
SetText(CONDITION["text"]); // either "RED" or "BLUE"
SetChildPosition("ColorBox", 0, CONDITION["depth"] === "near" ? 1.5 : 3.0, 0);
ShowItem();This is a common pattern in LUIDA: state controls when, condition controls what.
Where to go next
- Within vs between subjects — how
CONDITIONgets populated. - Conditions, sessions, participants — how LUIDA tracks participants, sessions, and condition assignments.
- Editor → State Machine tab — how to actually click your state list together.
- Reference → Signals — the global keys (state_currentID, state_triggerTransition, etc.) the state machine uses to talk to the rest of the world.