Chapter 9

Events: state and State Changes

Events: state emits a message when an HA state_changed event arrives, making it suitable for identifying when a change occurs. It does not poll or query history again. This chapter is based on the HA WebSocket 0.80.3 persisted type—the node type stored in exported flow JSON—server-state-changed, node version 6. It covers selectors, old and new states, types, outputs, duration conditions, and reconnection boundaries.

Why This Chapter Matters

State events are often misread as representing only “on” and “off.” In Home Assistant’s original state_changed schema, an event may change attributes only. An entity-creation event may have no old state, while an entity-deletion event represents the absence of a new state as new_state: null. Meanwhile, unknown and unavailable are actual string states, not missing objects. The package version pinned for this chapter does not pass deletion events to Events: state, however. The discussion below therefore distinguishes the original schema from the events the node can actually receive.

Events: state is a zero-input event node. It subscribes to events matching its entity selectors, evaluates the ignore options, only-change, If State, and For, and then uses Output Properties to construct a new msg. This differs both from Current State, which reads the cache when it receives a message, and from Poll State, which reads at regular intervals.

This chapter is limited to non-operational review: The example node remains disabled, and both Server and entity use PLACEHOLDER_* values. Read the JSON first and verify the selectors and outputs. Do not connect it, deploy it, or attach it to an Action node. Any environment-specific operation requires a separate decision after you complete an on-site review.

The v6 Processing Sequence and Node Selection

The 0.80.3 v6 controller can be understood in this order: it first confirms that the node is enabled and HA is running, then checks the entity selectors and five ignore conditions. It transforms the old and new states if required, applies only-change to normal events, and then evaluates If State and For. Finally, it writes the configured Output Properties to the message. A true If State result uses the first output; when a condition is configured, a false result uses the second output.

NodePersisted typeWhen it readsScope in this chapter
Events: stateserver-state-changedWhen a matching state_changed event arrivesThe focus of this chapter; event-driven processing of old and new states
Poll Statepoll-statePeriodically, at the configured intervalConsider only when an event source is unsuitable; limit its frequency and see Chapter 22
Trigger: statetrigger-stateWhen a state event satisfies its constraintsMore complex triggers and constraints; see the practical example in Chapter 13
Current Stateapi-current-stateWhen an upstream msg arrives, using the cacheOne-time queries and conditions; see Chapter 10

The table compares Events: state, Poll State, and Trigger: state so that you can choose according to the trigger requirement. Poll State does not make data more “real-time”; it merely creates periodic work. Trigger: state does not replace the failure policy you need for null, unknown, and unavailable.

Safely Review the Disabled Example

  1. Inspect the download as plain text first.

    Open 04-events-state.json and confirm that it contains only one Events: state node and one Debug node. The HA node contains "d": true, meaning that it remains disabled after import. This step does not run any Node-RED or HA operation.

  2. Verify the fields for the pinned version.

    Confirm that the type is server-state-changed, version is 6, Server is PLACEHOLDER_HA_SERVER_CONFIG, and entity is PLACEHOLDER_ENTITY_ID. Do not use search and replace to insert production environment IDs.

  3. Define the output contract first.

    Record on paper that msg.payload is the string state, msg.data is the event data, and msg.topic is the triggering entity ID. Debug is currently limited to the payload so that complete attributes do not expose information about locations, people, or devices.

  4. Choose a policy for each exceptional state.

    Decide separately how to route a null old_state and an old or new state of unknown or unavailable. Do not conflate these cases in a single “has a value” test. Entity deletion requires a different entry point that can observe the original event; do not expect downstream nodes to receive it from this version of Events: state.

  5. Check startup and timing conditions.

    In the example, Output on Connect is false, only-change is true, and For is 0. Before changing these settings in the future, model reconnection and timer cancellation according to the boundaries in this chapter. Do not experiment directly on production events.

  6. Keep side effects disconnected after the review.

    Even if you later test in an isolated environment, connect only a limited-field Debug node or a validation node with no external I/O. Do not connect an Action node, notification, HTTP request, MQTT message, or file write. First preserve the disabled state and review record; then let the on-site change process determine what happens next.

Safe test data: Represent events as paper examples or unit-test objects containing entity_id: "PLACEHOLDER_ENTITY_ID"; include only the required old and new states and fictitious timestamps. Do not copy a real HA event from Debug. Complete attributes may contain names, locations, media details, or lock information.

Three-row offline exercise: Keep the Events: state node disabled and disconnected from HA, and keep Action disconnected throughout. Evaluate only the following fictitious events and expected results on paper.

Fictitious eventPaper configurationExpected output or no action
PLACEHOLDER_STATE_A → PLACEHOLDER_STATE_Bentity_id: "PLACEHOLDER_ENTITY_ID"; only-change=trueThe limited Debug node receives msg.payload="PLACEHOLDER_STATE_B" and the placeholder topic; Action remains disconnected
State remains PLACEHOLDER_STATE_A, while an attribute changes from PLACEHOLDER_ATTRIBUTE_A to PLACEHOLDER_ATTRIBUTE_Bonly-change=trueNo output and no action; if attribute monitoring is required, first write a separate, explicit contract
The new state is unknown or unavailableSet the corresponding ignore option or exceptional-state branch according to the paper policyNo output if ignored; otherwise, route only to a NO_ACTION_INVALID_STATE Debug node, with no Action node attached

Entity Selectors and the Old/New Model

In v6, entities contains three selector groups: entity performs exact matching, substring performs substring matching, and regex performs regular-expression matching. When only exact selectors are present and the other two groups are empty, the runtime creates a more specific event-topic listener for each entity. If either substring or regex is used, it subscribes to the general state_changed topic and evaluates each event with shouldIncludeEvent. An event need only match any one of the three groups, not all three.

SelectorSuitable usePrimary riskSafe practice
Exact entityOne or a small number of approved IDsMissing events after an ID migrationPreferred; use PLACEHOLDER_ENTITY_ID to document the allowlist
SubstringA group of entities with a stable naming conventionUnintentionally including a new entity with the same substringList the expected set and fail closed for new entries
RegexA genuinely necessary structured setOverly broad scope, performance costs, and false matchesAdd start and end anchors, then test offline against a set of fictitious IDs

In a raw Home Assistant event, event.old_state and event.new_state are either state objects or null. An entity-creation event commonly has no old_state, while an entity-deletion event has no new_state. These cases are entirely different from an existing state object whose state string is unknown or unavailable. In 0.80.3, however, the WebSocket handler first checks the new_state object and entity ID. If either is falsy, it discards that state_changed event before emitting the package’s event topic. Events: state v6 therefore does not receive raw new_state: null deletion events and cannot guarantee a downstream deletion branch.

Do not assume that every state_changed event changes the state string. Changes to attributes or time fields may also produce events. With only-change enabled, the controller compares the transformed old and new state values and emits no normal event when they are equal. If you need to monitor attributes, you therefore cannot also assume that only-change will pass attribute-only updates.

State Strings, Conversion, and Output Properties

An HA state’s original value is a string. The default v6 stateType is str. When a non-string conversion is selected, the controller preserves original_state on the old and new entity objects before rewriting state. Numeric conversion uses parseFloat. The general Boolean conversion uses JavaScript’s !!value, so every nonempty string is true—including "off". For this reason, state tests should normally retain strings or use explicit HA Boolean rules instead of coercing arbitrary states to Boolean values.

Example mappingSource valueMissing-value and privacy boundary
msg.payload ← entity state (string)The new entity’s state after the package’s preliminary filtersunknown and unavailable remain strings; deletion events do not reach this node
msg.data ← event dataEvent data such as old_state and new_stateLarge and potentially attribute-rich; inspect it only temporarily in an isolated Debug node
msg.topic ← entity IDThe controller’s triggerIdEnvironment-specific information; replace it with a placeholder in public records
Custom msg/flow/global propertyEntity state, event data, config, JSONata, and other sourcesWriting to context extends retention; minimize the data and define retention first

Output Properties are explicit mappings, not a blanket guarantee that “everything is in the payload.” The example uses three default mappings. If you remove or rename one, update the downstream contract as well. Downstream nodes should operate only on explicitly mapped, validated fields and should not treat object-sharing behavior as a stable contract.

Detail specific to version 0.80.3: The default event-data → msg.data mapping stores a reference to the same eventData object. Before sending the message, the controller subsequently adds new_state.timeSinceChangedMs, so the default mapping exposes that field. A downstream node that requires it should explicitly validate its presence and type rather than assume that later versions will continue to share the object.

When If State is configured, the node has two outputs: a true result goes to the first, and a false result goes to the second. Without a condition, there is normally only one output. The second output does not indicate a runtime error; it represents a false condition. Observe genuine configuration or input errors separately with Catch or Status, as discussed conceptually in Chapter 18.

Ignore Options, Only-Change, If State, and For

The five ignore options are evaluated before conversion for events that have reached the node: old state does not exist, old state is unknown, old state is unavailable, new state is unknown, and new state is unavailable. Each option determines only whether to skip the entire event. A raw new_state: null is indeed different from unavailable, but 0.80.3 has already discarded it in the WebSocket handler upstream of Events: state. These five options and downstream branches cannot restore it.

outputOnlyOnStateChange compares the old and new states for normal events and returns without output when they are equal. Output on Connect, however, invokes its synthetic events with runAll=true, bypassing this only-change return. Enabling Output on Connect may therefore produce initial output for every cached entity that matches the selectors. This is the main source of startup floods.

If State compares the converted new state. It supports is, is not, greater-than and less-than comparisons, includes/not in, and JSONata. When If State is not configured, do not describe the first output as “condition passed”; it is simply the normal output. Once If State is configured, false messages still leave through the second output and are not discarded automatically. Attaching a side-effect node to the second output is equally hazardous.

The For value can come from a number, JSONata, flow context, or global context and must be nonnegative; an empty string or 0 means no delay. Valid timers are tracked separately by entity ID. After a normal event starts a timer, a nonmatching If State event for the same entity marks the timer inactive and follows the false path. A new valid event clears any existing timeout and starts another. This timer exists only in memory; it is not an HA history query, and a restart cannot preserve it as a countdown.

For is not retrospective validation: When the timeout expires, the node still emits the cloned eventMessage captured when the timer began. The controller does not query the current state again at expiry. If your risk model requires the condition to remain true at that moment, use a downstream Current State node for a second, read-only query. Handle a disconnected cache and missing values before considering any side effect.

Startup, Reconnection, and Safe Testing Boundaries

With Output on Connect enabled, the node converts the current cache into synthetic state_changed events when its listener starts if HA is already running. If HA is not ready, it waits for InitialConnectionReady before generating them. Each synthetic event points both old_state and new_state to the same current entity and is processed with runAll=true. It is not a genuine history and cannot reveal the intermediate changes that occurred while disconnected.

runAll=true does not start a For timer; the controller returns directly on the valid-timer path. If Output on Connect and For are both enabled, do not assume that each currently matching entity will wait for the complete For duration after connection before producing output. Review this combination against the v6 implementation, not by guessing from field names.

The controller also requires homeAssistant.isHomeAssistantRunning; it does not process normal events when the WebSocket is connected but HA is not yet running. After reconnection, the cache is a snapshot, and the event stream is not guaranteed to replay the disconnected interval. Safety-critical automation should therefore fail closed: draw no conclusions while disconnected, query the current state after reconnection, and use a constrained Get History query when historical evidence is required rather than inferring the intervening sequence from two events.

At minimum, safe testing covers a genuine state change, an attribute-only change, a null old_state, unknown and unavailable in both old and new states, Output on Connect, a brief disconnection and reconnection, and reversal while a For timer is active. A separate WebSocket-boundary test should confirm that raw new_state: null is discarded before Events: state, rather than expecting node output. Use only a disabled flow and fictitious paper or unit-test events with placeholders. Never toggle real lights, locks, alarms, climate equipment, or notifications merely to generate events.

Consider Trigger: state when you need a trigger only after a condition is satisfied; consider Poll State only when periodic sampling is necessary; use Current State when the state must be checked again at expiry. None provides exactly-once guarantees automatically. Any design connected to an Action node also needs idempotency, cooldown, reconnection, and manual-recovery policies; see Chapter 11.

Troubleshooting

  • No output at all: First confirm whether the node is intentionally still disabled. In a separately approved isolated test, check v6, Server readiness, exact/substring/regex selectors, the five ignore options, and If State, in that order. Do not begin by broadening the regex or connecting a real side effect.
  • No message after an attribute change: Check whether only-change is true. It compares the old and new states and skips a normal event when they are equal. If the requirement is genuinely to monitor attributes, define an explicit attribute allowlist rather than sending the complete event to Debug.
  • A burst of messages after startup: Check Output on Connect. Synthetic events traverse the cache and can bypass only-change. Before disabling it, confirm whether downstream logic depends on the initial snapshot, and disconnect every side-effect branch first.
  • unknown is treated as missing: unknown is a state string; null means that the state object does not exist. Record the cases separately and check the corresponding ignore option rather than conflating them with a truthiness test.
  • The state has changed by the time For expires: For preserves the initial event; it does not query again at expiry. First use Current State for read-only confirmation and handle a missing cache. Keep subsequent Action nodes disabled until the result is confirmed.
  • Numeric or Boolean comparisons behave unexpectedly: Check stateType and original_state. parseFloat may produce NaN, while general !! conversion treats a nonempty off string as true. Prefer the original string and an explicit comparison.
  • Events appear to be missing after reconnection: An event subscription does not provide historical replay. Record the disconnected window and, if the requirement justifies it, query Get History with a constrained time window. Never invent an Action from a reconnection snapshot.

Pinned Sources

The downloadable safe example is 04-events-state.json. It uses placeholder Server and entity values, and its HA node is disabled. It is intended only for reviewing the v6 fields before import; it does not constitute approval to run the flow in your environment.

FAQ

Are unknown, unavailable, and null the same thing?
No. The first two are string states within a state object; null means that the original old_state or new_state object does not exist. v6 can separately ignore a null old state and old or new states of unknown or unavailable. In 0.80.3, however, the WebSocket boundary discards a null new_state first, so downstream nodes cannot receive the deletion branch from Events: state.
With only-change enabled, is Output on Connect guaranteed to produce no output?
No. The initial synthetic events are processed with runAll, while the only-change equality check applies only to normal events with runAll=false. You must therefore estimate how many entities in the cache match the selectors.
Does For confirm the current state again when it expires?
No. The v6 timeout uses the cloned event captured when timing began. If the state at expiry is part of a safety decision, perform a separate, read-only Current State query and fail closed when the cache value is missing.
Why not convert a state directly to Boolean?
The general Boolean conversion uses JavaScript truthiness. Every nonempty string is true, so off also becomes true. Preserve the string and compare it explicitly, or use a reviewed HA Boolean rule.
Can Events: state recover every change that occurred while disconnected?
No. A reconnection cache or initial output is not a history replay. When history is required, use Get History with a constrained time window and result limit, and never create a side effect from incomplete data.
Can I connect the example directly to an Action node for testing?
No. The example is deliberately disabled and uses placeholders. Review it offline first, and connect only a limited Debug node. The Action target, data, queue, response, and error contracts require a separate review under Chapter 11.