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.
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.
| Node | Persisted type | When it reads | Scope in this chapter |
|---|---|---|---|
| Events: state | server-state-changed | When a matching state_changed event arrives | The focus of this chapter; event-driven processing of old and new states |
| Poll State | poll-state | Periodically, at the configured interval | Consider only when an event source is unsuitable; limit its frequency and see Chapter 22 |
| Trigger: state | trigger-state | When a state event satisfies its constraints | More complex triggers and constraints; see the practical example in Chapter 13 |
| Current State | api-current-state | When an upstream msg arrives, using the cache | One-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
- 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. - Verify the fields for the pinned version.
Confirm that the type is
server-state-changed,versionis6, Server isPLACEHOLDER_HA_SERVER_CONFIG, and entity isPLACEHOLDER_ENTITY_ID. Do not use search and replace to insert production environment IDs. - Define the output contract first.
Record on paper that
msg.payloadis the string state,msg.datais the event data, andmsg.topicis the triggering entity ID. Debug is currently limited to the payload so that complete attributes do not expose information about locations, people, or devices. - Choose a policy for each exceptional state.
Decide separately how to route a null old_state and an old or new state of
unknownorunavailable. 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. - 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.
- 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.
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 event | Paper configuration | Expected output or no action |
|---|---|---|
PLACEHOLDER_STATE_A → PLACEHOLDER_STATE_B | entity_id: "PLACEHOLDER_ENTITY_ID"; only-change=true | The 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_B | only-change=true | No output and no action; if attribute monitoring is required, first write a separate, explicit contract |
The new state is unknown or unavailable | Set the corresponding ignore option or exceptional-state branch according to the paper policy | No 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.
| Selector | Suitable use | Primary risk | Safe practice |
|---|---|---|---|
| Exact entity | One or a small number of approved IDs | Missing events after an ID migration | Preferred; use PLACEHOLDER_ENTITY_ID to document the allowlist |
| Substring | A group of entities with a stable naming convention | Unintentionally including a new entity with the same substring | List the expected set and fail closed for new entries |
| Regex | A genuinely necessary structured set | Overly broad scope, performance costs, and false matches | Add 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 mapping | Source value | Missing-value and privacy boundary |
|---|---|---|
msg.payload ← entity state (string) | The new entity’s state after the package’s preliminary filters | unknown and unavailable remain strings; deletion events do not reach this node |
msg.data ← event data | Event data such as old_state and new_state | Large and potentially attribute-rich; inspect it only temporarily in an isolated Debug node |
msg.topic ← entity ID | The controller’s triggerId | Environment-specific information; replace it with a placeholder in public records |
| Custom msg/flow/global property | Entity state, event data, config, JSONata, and other sources | Writing 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.
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.
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:
unknownis 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.parseFloatmay 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
- HA WebSocket 0.80.3 pinned commit 2cbbb69: this chapter checks
src/nodes/events-stateandsrc/common/TransformState.ts, and confirms the registration boundaries for Poll State and Trigger State. - Official Events: state node documentation: user-facing fields and input/output descriptions. If the rolling documentation differs from the pinned commit, the pinned commit is this chapter’s baseline.
- Official Home Assistant state-object documentation: the data model for states and attributes.
- Official Home Assistant events documentation: background on the event bus and state_changed.
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.