Skip to main content
Call Eric:863-698-8266
CURRYCONTROLS.COMControls & Automation Knowledge Hub
ReferencePLCProgrammingDesignControl

State Machines in PLC Programs

Writing sequences as explicit states instead of tangled interlock chains: what a state is, how transitions are written, the pattern in structured text and in ladder, publishing the state to the HMI, and the failure handling every state machine needs.

11 min readUpdated Sep 5, 2026Published Sep 5, 2026By Eric Sullivan

The short answer

State Machines in PLC Programs

A state machine writes a sequence as a set of named states, exactly one of which is active, with explicit transitions between them. Each state defines what the outputs do while it is active and what condition moves the machine to the next state. The pattern replaces the seal-in rungs and interlock chains that grow unmaintainable, makes the current step visible on the HMI, and forces the programmer to decide what happens on a fault, a stop, and a power loss in every state.

Key points

  • One state active at a time, held in one integer or enumeration.
  • Outputs are decided by the state; transitions are decided by conditions. Keep the two apart.
  • Every state has a way out on fault, on stop, and on timeout.
  • Publish the state number and a description to the HMI. That is the troubleshooting tool.
  • Decide what happens on power loss: restart, resume, or go to a safe state.

The problem state machines solve

A backwash sequence, a generator transfer, a chemical batch, a well pump startup with its pump-to-waste period: each is a series of steps that depend on time and on conditions. Written as ladder with seal-in bits, each step is a rung that latches when the previous step is done and unlatches when its own completion is seen, plus the interlocks, plus the fault handling, plus the reset. By the twelfth step nobody can say with confidence which bits are set, and a fault mid-sequence leaves latches in a combination that was never tested.

A state machine replaces all of that with one variable, the current state, and a structure that makes each state explicit. The program can be in state 30, Combined Wash, and nothing else. What the outputs do in state 30 is written in one place. What moves the sequence to state 40 is written in one place. What happens in state 30 on a fault is written in one place. The HMI shows the number and the name.

The structure

State variable
An integer or enumeration, one per machine, retentive if the sequence must survive a power cycle. Numbered with gaps, 10, 20, 30, so a state can be inserted later without renumbering.
States
Each has a name, a description, and a purpose. Idle, Drain, Air Scour, Combined Wash, Rinse, Return to Service, Aborting, Fault. Idle and Fault always exist.
Outputs by state
What each output does in each state, written as a function of the state, not as a latch. In state 20 the air scour blower runs; in states where it should not, it does not. Because outputs are derived from state, they cannot be left on by a forgotten unlatch.
Transitions
The condition that moves from one state to the next: a timer expired, a level reached, a valve confirmed open, an operator acknowledgment. Written per state, and the only place the state variable is changed.
Timeouts
Each state that waits for something has a maximum time. A valve that does not confirm open in 60 seconds moves the machine to Fault, not to waiting forever.
Fault and abort paths
From any state, a fault condition goes to the Fault state, and a stop request goes to Aborting, which returns the equipment to a safe configuration and then to Idle. These transitions are written once, above the per-state logic.
A filter backwash sequence as a CASE statement
(* Global exits: evaluated before the per-state logic, every scan. *)
IF Fault_Any THEN
    State := 900;                          (* Fault *)
ELSIF Stop_Request AND State > 0 AND State < 800 THEN
    State := 800;                          (* Aborting *)
END_IF;

CASE State OF
    0:   (* Idle *)
        IF Backwash_Request AND Permissives_OK THEN
            State := 10;  Step_Timer := T#0s;
        END_IF;

    10:  (* Close influent, drain to wash level *)
        IF Influent_Valve_Closed AND Level < Wash_Level THEN
            State := 20;  Step_Timer := T#0s;
        ELSIF Step_Timer > T#5m THEN
            State := 900;  Fault_Code := 10;
        END_IF;

    20:  (* Air scour *)
        IF Step_Timer > Air_Scour_Time THEN
            State := 30;  Step_Timer := T#0s;
        END_IF;

    30:  (* Combined air and water wash *)
        IF Step_Timer > Combined_Time THEN
            State := 40;  Step_Timer := T#0s;
        END_IF;

    40:  (* Water rinse until turbidity clears *)
        IF Wash_Turbidity < Rinse_Limit AND Step_Timer > Min_Rinse_Time THEN
            State := 50;  Step_Timer := T#0s;
        ELSIF Step_Timer > Max_Rinse_Time THEN
            State := 900;  Fault_Code := 40;
        END_IF;

    50:  (* Return to service *)
        IF Influent_Valve_Open AND Effluent_Valve_Open THEN
            State := 0;
        END_IF;

    800: (* Aborting: close wash valves, stop blower, then idle *)
        IF Wash_Valves_Closed AND NOT Blower_Running THEN
            State := 0;
        END_IF;

    900: (* Fault: hold safe until reset *)
        IF Fault_Reset AND NOT Fault_Any THEN
            State := 0;
        END_IF;
ELSE
    State := 900;  Fault_Code := 999;      (* unknown state *)
END_CASE;

(* Outputs derived from state. Nothing latches. *)
Air_Scour_Blower_Cmd := (State = 20) OR (State = 30);
Wash_Water_Valve_Cmd := (State = 30) OR (State = 40);
Influent_Valve_Cmd   := (State = 0) OR (State = 50);

The shape is the whole lesson. Exits that apply everywhere are at the top. Each state changes the state variable and nothing else. Outputs are computed from the state at the bottom, so there is nothing to unlatch and nothing left on when the sequence leaves a state by any route. The ELSE catches a state value that should not exist, which happens after a program edit or a memory fault.

The same pattern in ladder

Where the site standard requires ladder, the pattern survives. One integer holds the state. A rung per transition compares the state and the condition and moves a new value into the integer. A rung per output compares the state and drives the coil. It is more rungs than the CASE statement and it is still one variable, one place per transition, and outputs that follow the state. What it must not become is a set of seal-in bits with the state number added as decoration.

Power loss and restart

The one decision a state machine forces that latch logic lets a programmer avoid is what happens on a power cycle. Three answers are common, and the control narrative picks one per sequence.

BehaviorHowWhere it fits
Restart from IdleState variable not retentive, or first-scan logic sets it to 0Sequences that are safe and cheap to start over: a pump startup, a valve exercise
Resume where it stoppedState variable retentive; timers and step data retentive tooLong sequences where restarting wastes product or time: a batch, a long backwash; only if every state is safe to resume into
Go to a safe state and waitFirst-scan logic moves any active state to Aborting or a Hold state that needs an operatorAnything where resuming blind could be unsafe: chemical feeds, sequences with manual steps

Publishing the state

The state number is the single most useful troubleshooting tag in a sequence. Publish it, and publish a text description of the current state and the reason the machine is waiting: state 40, Rinse, waiting for turbidity below 5 NTU, 3:20 elapsed of 10:00 maximum. A sequence that says why it is waiting does not need someone to open the program. Log every state change with a timestamp to the historian; the record of when each step started and ended is the commissioning evidence and the maintenance history.

Frequently asked questions

Should I use a sequential function chart instead?
SFC is the graphical form of the same idea and some platforms implement it well. It excels at simple linear sequences and at showing the active step. It becomes hard to read with many exits and parallel branches, and online troubleshooting depends on the platform. A CASE statement in structured text is portable and explicit; SFC is a fine choice where the platform and the site are comfortable with it.
How do I handle two things happening at once?
Two state machines. A filter with a backwash sequence and a separate flow control loop is two machines that exchange status. Trying to express parallel activity inside one state machine produces the branching mesh that the pattern was meant to avoid.
Where do the interlocks go?
Permissives gate the transition out of Idle. Interlocks that must stop the sequence go into the fault condition and produce the transition to Fault from every state. Interlocks that protect a single output stay on that output, after the state logic, so an output the state calls for is still blocked when its interlock says so.
The sequence is stuck in a state. How do I find out why?
Read the published wait reason and the step timer. If the machine does not publish them, look at the transition condition for that state in the program and evaluate each term. The fix after the event is to add the wait reason so the next time does not need the program.

Direct contact

Have a controls question?

Reach Eric Sullivan directly about anything on this site, a controls or automation topic, or one of his personal projects.