Automations · Home Assistant

Advanced Automation Patterns.

Reading time
~21 min · 4,232 words
FAQ
0 questions
Status
Draft 1 · under review
Section
All Home Assistant pages

The automation fundamentals — trigger, condition, action — cover most agricultural needs. The patterns on this page cover everything beyond. Variables that capture trigger data for later use in the actions. Choose blocks that branch on conditions the way an if-elif-else would. Wait-for-trigger and wait-for-template that pause an automation until something happens in the real world. Repeat blocks that iterate over zones or run until a target is reached. Parallel actions that execute simultaneously rather than sequentially. State machines built on input helpers. Timeout handling that keeps failed operations from blocking forever. Error handling with continueonerror and default. Automation chaining. Shared-state coordination between automations. For agricultural operations, these patterns turn single-purpose automations into systems that handle the complexity of a real farm — a fertigation cycle that adjusts to tank levels and aborts on safety checks, a cooling escalation that tries gentler measures before aggressive ones, an irrigation scheduler that processes many zones in a controlled sequence without stepping on itself. This page covers each pattern with agricultural examples, the failure modes that affect advanced automations, and the design principles that keep advanced logic maintainable over time.

Before using advanced patterns.

Prerequisites from earlier pages:

The fundamentals. [Automation Fundamentals](/home-assistant/automations/fundamentals) covers the trigger-condition-action model, the four modes, and the basics of testing and debugging. Advanced patterns all live inside that structure; they do not replace it.

Template sensors comfortable. [Template Sensors](/home-assistant/automations/templates) covers Jinja2 in Home Assistant. Advanced automations make heavy use of templates — for variables, in choose-block conditions, in wait templates, in dynamic action parameters. Comfort with the basic Jinja2 patterns matters more than with any specific advanced feature.

Scripts and scenes in the vocabulary. [Scripts and Scenes](/home-assistant/automations/scripts-scenes) covers the reusable-logic structures that advanced automations call and the scenes they apply. An automation with twelve action steps is usually better refactored into an automation plus two scripts; knowing when to do that matters.

Hardware safety confirmed. Advanced automations are powerful. A ten-step irrigation sequence that aborts on any safety-check failure still depends on the hardware actually abiding by the commands it receives. The thermal cutoffs, mechanical float switches, and fused circuits remain the last line of defense. Advanced patterns make operations smarter; they do not replace safety hardware.

Variables and trigger data.

Variables capture values at the start of an automation or inside its actions, so the same value can be referenced multiple times without re-evaluation.

Why variables matter. A rule like "alert if Zone 1 temperature is above its target by more than 5°F" needs to reference the current temperature and the current target in both the condition evaluation and the alert message. Without variables, the templates re-evaluate each time, which can produce inconsistent values if sensors update mid-automation. Variables capture the value once and use it throughout.

Automation-level variables. The `variables:` block at the automation level sets values available to every condition and action in the automation. Variables can be static values or Jinja2 templates. Example: `currenttemp: "{{ states('sensor.greenhousezone1temperature') | float(0) }}"` captures the temperature at the start of the run.

Action-level variables. A `variables:` block inside the action sequence sets values available from that point on. Useful for capturing values part-way through an automation, after an earlier action changed something.

Trigger variables. Every trigger exposes its data through the `trigger` variable. For a state trigger, `trigger.tostate.state` is the new state, `trigger.fromstate.state` is the previous state, and `trigger.entity_id` is the entity that changed. For a numeric state trigger, the same plus `trigger.above` and `trigger.below` giving the thresholds. For a template trigger, `trigger.description` and the trigger-specific data. For time triggers, the time that fired. Trigger variables are what connect the event that happened to the action that responds.

Trigger IDs. When an automation has multiple triggers, giving each an `id:` lets the actions know which trigger fired by checking `trigger.id`. Useful for one automation that handles several related triggers with different actions per case.

Common agricultural variable patterns. Capture the triggering zone's name from an entity ID, capture the trigger value and threshold for alert messages, capture a timestamp for logging, capture the current target setpoint for comparison. Each of these patterns makes the subsequent actions simpler and more reliable.

Choose blocks.

Choose is the automation equivalent of if-elif-else. It evaluates conditions in order and runs the actions for the first matching one.

The structure. A `choose:` block contains a list of options, each with its own `conditions:` and `sequence:` of actions. An optional `default:` sequence runs if none of the options match. Choose evaluates options in order, picks the first whose conditions evaluate true, and runs its sequence. Subsequent options are not checked.

Agricultural choose examples.

A cooling escalation automation picks the intervention tier based on how far over target the temperature is. If over by 2-5°F, open vents. If over by 5-10°F, open vents plus start fans. If over by more than 10°F, emergency cool scene plus alert.

An irrigation automation picks the cycle length based on the moisture deficit. Moisture below 15% gets a long cycle. Between 15% and 22% gets a short cycle. Above 22% does nothing (the automation was triggered for another reason).

A fertigation recipe selector picks the nutrient mix based on the current growth stage. Seedling gets one recipe, vegetative gets another, flowering gets a third.

Order matters. Choose evaluates options top-down and runs the first match. The most specific conditions should come first; broader conditions should come later. An option for "over by more than 10°F" should come before an option for "over by more than 2°F" — otherwise the broader condition always matches first and the more specific tier never runs.

Default option. The `default:` block is the catch-all. It runs when no option matched. Useful as a safety net — log that nothing matched, send a diagnostic alert, or do nothing explicitly. A choose block without a default silently does nothing when no option matches, which is sometimes correct and sometimes a bug waiting to be discovered.

Wait-for-trigger and wait-for-template.

Wait actions pause an automation until something happens in the real world.

Wait-for-trigger. Pauses until one of the specified triggers fires, or until a timeout expires. Any trigger type that works as an automation trigger works inside wait-for-trigger — state changes, numeric thresholds, templates, time.

Agricultural example: start a cooling sequence by opening vents, then wait for the temperature to drop by 3°F (a numeric state trigger comparing to a variable captured before the wait), with a 30-minute timeout. If the drop happens, proceed to the next step. If the timeout hits, escalate.

Wait-for-template. Pauses until a Jinja2 template evaluates to true, or until a timeout. Often equivalent to wait-for-trigger with a template trigger, but simpler syntax for template-based waits.

Agricultural example: open an irrigation valve, then wait for flow to confirm ("flow sensor above 1 L/min for 10 seconds"), with a 60-second timeout. If flow confirms, proceed. If not, something is wrong — log the failure, close the valve, alert.

Timeouts are mandatory in production. A wait with no timeout can hang indefinitely if the awaited condition never occurs — a valve that fails, a sensor that dies, a controller that stops responding. Every wait in production code should have a timeout that is long enough for the normal case plus margin, but short enough that a failure gets noticed.

The continueontimeout flag. By default, a wait that times out raises an error that stops the automation. The `continueontimeout: true` option lets the automation continue past a timeout. Combined with `wait.completed` (which indicates whether the wait succeeded or timed out), this supports automations that handle the timeout case explicitly rather than failing.

Wait-for-trigger vs. delay. A fixed delay waits for a specified duration regardless of what happens. A wait-for-trigger waits for a condition to occur, which may happen before the timeout. "Wait 10 minutes" blocks for 10 minutes; "wait until temperature drops below 75°F or 10 minutes pass" completes as soon as either condition is true.

Repeat patterns.

Repeat actions loop over something. Home Assistant supports four forms.

Repeat count. Run the sequence a fixed number of times. `repeat: count: 3` runs three times. Useful for known-repetition operations — three flush cycles, two verification checks, five retries.

Repeat while. Run the sequence while a condition is true, check at the top of each iteration. A "keep irrigating while moisture is below target" loop runs cycles until moisture rises. Needs a safety cap — either an explicit count limit in the conditions or a hard timeout — to prevent runaway loops.

Repeat until. Run the sequence until a condition becomes true, check at the bottom of each iteration. Functionally similar to while but guarantees at least one execution before the condition is checked. "Keep mixing until tank pH reaches target" runs the mix step at least once, then checks pH.

Repeat foreach. Iterate over a list of values, running the sequence once per item. Useful for processing multiple zones, multiple valves, or any collection. `repeat: foreach: [zone1, zone2, zone_3]` runs the sequence three times, once with the loop variable set to each zone.

The loop variable. Inside a repeat block, `repeat.index` is the current iteration number (1-based), `repeat.first` and `repeat.last` indicate the boundaries, and `repeat.item` is the current value in a foreach loop. Use these to vary behavior per iteration — log different messages for first/last/middle iterations, apply different settings per zone in a foreach.

Safety caps on repeats. Every repeat should have a way to exit. Count-based repeats have their count. While and until loops should have a maximum iteration count, a total time limit, or both. A repeat block that relies entirely on an external condition becoming true is vulnerable to hanging when that condition is blocked by a bug. `repeat: while: ... sequence: ... (with a counter check inside)` is defensive.

Agricultural repeat examples. Running three flush cycles after a fertigation (count). Topping up a reservoir until a level sensor reads above threshold (until, with a fail-safe time limit). Checking each greenhouse zone in sequence and building a combined status report (for_each). Retrying a valve close up to five times if confirmation fails (count with an early-exit condition).

Parallel actions.

Parallel blocks run their contents concurrently instead of sequentially.

Why parallel. Sequential execution is the default and usually right. Some operations are independent and should run simultaneously — starting ventilation, starting fans, and sending an alert do not need to wait on each other. Parallel execution completes all three in the time of the slowest, not in the sum.

The structure. A `parallel:` block contains a list of sequences. Each sequence runs concurrently; the parallel block completes when all sequences finish.

When parallel fits. Actions that do not share resources and do not depend on each other's results are good candidates. Notifications to multiple channels. Commands to independent zones. Data capture to multiple logs.

When parallel hurts. Actions that share state can race in parallel mode. Two parallel sequences that both want to read and update a counter will produce inconsistent results. Actions that affect the same physical equipment should usually not run in parallel with each other — two sequences both trying to control the same valve will produce confusing behavior.

Parallel versus automation mode: parallel. The automation-level `mode: parallel` controls what happens when the automation is re-triggered while a previous run is executing. The action-level `parallel:` block runs multiple actions within one run of the automation concurrently. Two different concurrency controls; both can be used together or separately.

Agricultural parallel examples. Responding to a high-temperature alarm by starting ventilation, starting fans, applying the emergency-cool scene, logging the event, and sending notifications — all at once. Processing a multi-zone daily summary by building each zone's report in parallel rather than one at a time.

State machines with input helpers.

A state machine is a design pattern: a system has a finite number of states, and transitions between states follow rules. Home Assistant supports this pattern through input helpers, particularly `input_select`.

The basic pattern. An `inputselect` helper holds the current state ("vegetative," "flowering," "finishing"). Automations reference the helper in their conditions to decide what to do. The state changes through explicit transitions — a service call or scene that sets `inputselect.growth_stage` to a new value.

Why this pattern works. Home Assistant does not have a native state-machine construct. A state variable plus conditions that branch on it produces effective state-machine behavior without requiring one. The state is visible, loggable, graphable, and can be manipulated manually when needed.

Agricultural state machine examples.

Growth stage. `inputselect.zone1growthstage` holds "propagation," "vegetative," "flowering," "harvest." Each stage has different target setpoints, different photoperiod, different irrigation, different alerts. Automations reference the growth stage to select appropriate behavior.

Operational mode. `inputselect.zone1_mode` holds "auto," "manual," "maintenance," "disabled." Automations check the mode before acting — in maintenance mode, safety-related automations stay active but convenience ones pause; in disabled mode everything holds.

Season. `input_select.season` holds "spring," "summer," "fall," "winter." Seasonal ventilation strategies, irrigation schedules, and lighting patterns all reference the season. Transitioning happens on a defined date or by manual override.

Transitions as actions. Transitions usually happen through explicit automations. A "move to flowering stage" automation is triggered manually (by a button, voice command, or scheduled date) and its action sets the input_select and applies any initialization scenes. Making transitions explicit in automations keeps the state machine's behavior visible and testable.

History and logging. Because the state is just an entity value, its transitions show up in history graphs and the logbook. A grower can look at a season's worth of graph and see exactly when each zone transitioned between stages. This is hard to do with an informal "stage" concept; it is free with an input helper.

Timeout handling and graceful degradation.

Advanced automations make decisions based on external conditions — a sensor read, a confirmation signal, a response from a service. Any of these can fail. Graceful degradation means the automation handles the failure intelligently rather than crashing.

Timeouts on every wait. Every `waitfortrigger` and `waitfortemplate` needs a timeout. The timeout should be long enough for the normal case plus reasonable margin, and short enough that a real failure gets noticed soon. An irrigation-valve confirmation wait of 60 seconds is reasonable; 60 minutes is probably too long.

The verify-and-react pattern. After commanding an action, verify the action happened. If it did, proceed. If it did not, handle the failure — log, alert, try again, or abort safely. The pattern in general: command, wait-for-confirmation-or-timeout, check the wait.completed value, branch on success or failure.

Fallback paths. A choose block after a wait checks `wait.completed` and branches accordingly. On success, continue the intended sequence. On timeout, take the fallback path — a less aggressive action, an alert to the grower, or a safe hold.

Preserving the previous state. Some automations need to restore state if they fail partway. Scene snapshot at the start of the automation, then restore on failure, is the pattern. The [Scripts and Scenes](/home-assistant/automations/scripts-scenes) page covers `scene.create` and `scene.turn_on` for this use.

Agricultural graceful degradation examples.

Valve operation. Command valve open, wait for flow to confirm with 60-second timeout, if confirmed proceed, if not command valve closed and alert. The failure mode where a valve reports open but no flow happens (pump is off, feed line is blocked, valve is mechanically stuck) gets caught.

Cooling escalation. Open vents, wait for temperature to drop with 15-minute timeout, if dropped continue normal operation, if not start fans (next tier), wait again, if still not dropped apply emergency scene and alert.

Fertigation mix verification. Start mixing, wait for pH to reach target with 10-minute timeout, if reached begin injection, if not alert the grower and abort the cycle rather than injecting an unverified mix.

Error handling.

Beyond timeouts, actions can fail for other reasons — a service call that returns an error, a template that produces an invalid result, an entity that is unavailable.

Default behavior: stop on error. By default, an action that errors stops the automation. Subsequent actions do not run. This is safe but sometimes too strict.

continueonerror: true. A specific action can be marked `continueonerror: true`, causing the automation to continue past an error on that action. Useful for non-critical steps where failure should not block the rest of the automation. Sending notifications is a common case — if one notification channel fails, send through the others rather than aborting.

default: on templates. Jinja2 templates can have `| default(value)` to provide a fallback for invalid template results. `{{ states('sensor.temp') | float(0) }}` returns 0 if the template fails; `{{ states('sensor.temp') | float(default=0) }}` does the same more explicitly. For derived values, making the template output available or unavailable is often cleaner than falling back to a default that silently represents missing data.

Error paths as choose branches. More sophisticated error handling uses choose blocks to branch on error conditions. After a service call, check whether the expected state resulted; if not, the error branch handles it. This is more work than `continueonerror` but produces explicit error handling.

Logging errors. Home Assistant logs automation errors automatically. For operations that need deeper logging, a script that writes error events to a dedicated log via a notification service or a log file keeps a persistent record of what went wrong.

Chaining automations and scripts.

Automations and scripts can call each other, producing chains of related logic.

Calling a script from an automation. The typical pattern, covered in [Scripts and Scenes](/home-assistant/automations/scripts-scenes). An automation's action is a call to `script.`. The script runs; when it completes, the automation continues.

Calling another automation. The `automation.trigger` service runs another automation's actions. The called automation's trigger is bypassed. Useful for invoking shared alert-handling or logging logic when the called logic is already expressed as an automation rather than a script.

Passing data between them. Scripts with fields accept data through their `variables:` field at call time. The called script sees its parameters as variables. Automations can use `variables:` at the call site to set context. Data flow is explicit at the call site.

Receiving data back. Modern scripts can return values with the `stop` action and `response_variable`. An automation that calls a script and needs a result captures it in a subsequent action.

Designing chains. Chains get unwieldy quickly. Two or three levels deep is usually fine; five or six levels of nesting becomes hard to trace. When chains feel like they are getting too deep, the operation often benefits from factoring into fewer, clearer scripts with explicit responsibilities rather than many small pieces that are hard to follow.

Shared-state coordination.

Multiple automations that affect related systems need coordination. Without it, they race and produce conflicting behavior.

Input helpers as interlocks. An `inputboolean` named "irrigationinterlock" that blocks irrigation automations while set to true. A "maintenance_mode" boolean that blocks everything except safety alerts. Automations check the interlock as a condition before acting; anything that needs to temporarily prevent actions sets the interlock.

Input helpers as setpoints. An `inputnumber` holds a target value (target temperature, target DLI, target moisture). Automations reference the inputnumber rather than a hardcoded value. Changing the setpoint is now a simple input change that affects every automation using it.

Input helpers for mode selection. Already covered under state machines. An `input_select` that automations reference selects behavior.

The "currently running" flag pattern. An `input_boolean` that an automation sets true at its start and false at its end indicates that the automation is currently running. Other automations can check the flag before taking actions that would conflict. More robust than relying on automation mode alone because the flag is visible and manipulable.

Shared counters and accumulators. An `input_number` or a counter entity can accumulate across automation runs. "Daily irrigation minutes" as a counter that each irrigation automation adds its runtime to produces a daily total that automations can check against a cap.

Graceful conflict resolution. When two automations affecting the same system are both valid, the rule for which wins should be explicit. Priority levels on alerts. A "manual override" flag that pauses automation. A "last change wins" rule with a visible source indicator. Whatever the rule, it should be documented.

Common failure modes.

Specific advanced-pattern problems from real operations.

The choose block where the wrong branch always matched. The broader condition was listed before the more specific one; the broader condition always evaluated true for any case that would have matched the specific one. The specific branch never ran. Fix: order conditions from most specific to most general; test with inputs that should hit each branch.

The waitfortrigger with no timeout. An irrigation automation waited for flow confirmation that never arrived. The automation hung forever in `mode: queued`; subsequent triggers queued up behind it. Fix: always set timeouts; use the trace viewer to find stuck runs; set `continueontimeout: true` and handle the timeout case explicitly in downstream actions.

The repeat while that never exited. A tank top-up loop waited for a level sensor to rise above a threshold. The sensor was stuck low (hardware fault). The loop ran indefinitely, commanding the fill valve open continuously. The tank overflowed. Fix: safety caps on every repeat loop — maximum iterations, maximum duration, or both; hardware float switches that provide mechanical shut-off.

The parallel block that produced a race. Two parallel sequences both manipulated the same input_number. Their updates interleaved in undefined order; the final value was sometimes one and sometimes the other. Fix: do not share state across parallel sequences; if coordination is needed, sequence the operations or use explicit serialization.

The state machine that drifted. Transitions were made by many automations in different conditions. Over time, some transition paths stopped getting exercised; bugs accumulated in the unused paths. Fix: periodic review of state transitions and the automations that trigger them; tests that exercise each path.

The chain that lost data. An automation called a script, the script called another script, and the trigger data that mattered to the innermost script was not passed through the chain. Each level lost context. Fix: be explicit about what each level needs; pass variables through rather than relying on global state; or flatten the chain.

The continueonerror that hid a real problem. A critical action was marked `continueonerror: true` because its error during development was a nuisance. Later that action started failing in production for a different reason; the automation continued silently. The grower discovered the failure only when the downstream effect manifested. Fix: use `continueonerror` only for genuinely optional actions; log errors so they are visible even when they do not stop the automation.

The shared interlock that got stuck. An `input_boolean` interlock was set by a script that crashed before clearing it. Subsequent automations saw the interlock and blocked. Fix: use a `try/finally`-style pattern (Home Assistant does not have native try/finally, but ensuring cleanup actions run even on error approximates it); add a watchdog automation that alerts when an interlock has been set for longer than its expected maximum.

The timeout that was too short. A valve confirmation timeout was set to 10 seconds based on tests during good conditions. In cold weather, the valve took 15 seconds to confirm. Every cold-day automation run failed the timeout check and took the failure branch. Fix: set timeouts generously — 2-3x the observed normal maximum — and review them against real-world conditions.

The variable that was stale. An automation captured the temperature at start, then waited 30 minutes, then referenced the captured value. The temperature had changed significantly in the intervening half hour. The logic acted on obsolete data. Fix: capture variables near their point of use; re-read when the wait is significant; or use the latest state rather than a captured variable when "current" matters more than "at trigger time."

What not to do.

Patterns to avoid.

Don't nest choose blocks deeply. One level of choose is clear. Two levels is sometimes necessary. Three or more is a sign the logic should be refactored into scripts or rethought at the design level.

Don't use repeat when a trigger would do. A loop that waits for a condition and acts each time is often better expressed as an automation with that condition as a trigger. Let Home Assistant's event loop do the waiting.

Don't use parallel for dependent actions. If action B needs the result of action A, they must run sequentially. Parallel is only for independent work.

Don't build state machines without documenting the states and transitions. A state machine with six states and twelve transitions has rules that are not obvious from reading any single automation. Write down the states, the valid transitions, and the triggers for each transition. The documentation itself is cheap insurance against drift.

Don't skip timeouts "because it always works." Production failures come from the rare cases, not the common ones. A wait that always worked in testing will encounter its failure case in production eventually.

Don't abuse continueonerror to silence errors during development. Errors during development are feedback. Fix them or understand them; do not hide them with `continueonerror` and then forget the underlying issue exists.

Don't chain automations deeper than the logic actually requires. Chains are convenient but hard to trace. Flatter logic with explicit calls to a small number of focused scripts is more maintainable than deeply nested chains.

Don't share mutable state across automations without a coordination pattern. Two automations both updating the same counter without coordination produces race conditions. Use explicit locking patterns or serialize the updates through a single controlling automation.

Don't let advanced patterns camouflage bad design. If an automation is hard to understand, the fix is usually simpler structure, not more advanced features. Variables, choose blocks, and state machines are tools; they are not substitutes for clear thinking about what the automation is supposed to do.