Indigo actions are not guaranteed to execute sequentially

Below is a revised version of your text with edits for clarity, flow, style, spelling, and punctuation. I also tightened the sequencing section to make it more concrete and less abstract. After that, I’ve added a new section, in your voice and structure, describing how to model this as a state machine.


Although I’ve used Indigo — the macOS home automation ecosystem — for over a decade, I never picked up on the fact that Actions attached to Schedules, Triggers, and web UI elements are not executed sequentially. The application user interface strongly implies sequential execution, but not only is that not guaranteed, the app actually attempts to execute the actions in parallel.

See this note buried in the documentation:

Important! While you can order the actions in any order you like, Indigo will attempt to execute all actions in parallel. It’s not always possible for various reasons, but that’s the intent. If you want to order the execution, then you’ll need to add delays which will delay the action’s execution from the time of the event. So, if you have 3 actions and you want the first to execute immediately, the second to execute a minute after the event, and the third to execute two minutes after the event, then add a one minute delay to the second and a two minute delay after the third.

This creates an obvious foot-gun for race conditions, like I just encountered, where an action that appeared second in a list of two was executed before the earlier-listed action.

Sequencing by moving separate actions into a single script

The documentation encourages users to add delays, which is the worst way to deal with race conditions because it still leaves the system in a non-deterministic state. A delay does not guarantee that a prior action has completed; it merely reduces the probability of overlap.

Since Indigo does not provide mechanisms for locks, semaphores, or atomic access to shared resources, the approach I’ve taken is to consolidate multi-action events into a single Python script. This creates a single execution context where ordering is explicit and deterministic. The Python API is comprehensive enough that most multi-step actions can be handled in this way, and the script itself becomes the authority on sequencing rather than the Indigo action list.

Sequencing by conditioning one action on the outcome of another

Another approach is to avoid sequencing entirely and instead make later actions conditional on observable state changes produced by earlier ones.

Consider two actions triggered when a user presses a button to open or close the garage door. One action speaks the intent — “Will open the right garage door” or “Will close the right garage door” — over the house speaker system. The other action trips the relay that performs the requested operation. Since the speech action needs to know the current state of the door, that state must be captured before the relay changes it.

Instead of relying on execution order, the UI action can set an intermediate intent variable, for example r_garage_door_intent. Variable change triggers can then respond to this intent:

  • One trigger handles the speech announcement.
  • Another trigger performs the relay action.
  • Each trigger can independently verify the current state before acting.

This shifts the system away from ordered execution and toward state-driven behavior, which is inherently more deterministic.

Implementing this as a state machine

A more structured version of the above is to model the workflow explicitly as a state machine. Rather than thinking in terms of “do A then B,” the system moves through well-defined states, with transitions triggered by events or observed conditions.

Using the garage door example, we might define states such as:

  • idle
  • opening_requested
  • opening_announced
  • opening_in_progress
  • open
  • closing_requested
  • closing_announced
  • closing_in_progress
  • closed

The UI action does only one thing: it changes the system state. For example, pressing the “open” button sets a variable like r_garage_door_state = opening_requested.

From there:

  • A trigger watching for opening_requested handles the announcement, then updates the state to opening_announced.
  • Another trigger watching for opening_announced activates the relay and updates the state to opening_in_progress.
  • A sensor or polling trigger detects when the door has finished moving and sets the state to open.

Each step is driven by state transitions rather than timing assumptions. This provides several benefits:

  • Ordering is explicit and enforced by design.
  • Actions can be retried safely if needed.
  • The current state is always observable for debugging.
  • Race conditions are minimized because each transition has a single responsibility.

This approach requires slightly more setup, but it scales well as workflows grow more complex and provides a much clearer mental model of how the system behaves.

In the case of a simpler workflow with two actions, and where the Pyhon API is up to the task, rolling everything into a script is much less complex and fragile.

Once actions are understood as parallel by default, the design strategy shifts from ordering steps to managing state transitions. This approach minimizes race conditions and results in workflows that are both deterministic and observable.