Skip to Content

Nothing you store remembers on its own, and one connection is already built for you

assume nothing survives
Node-RED Guide · Part 4

Nothing you store remembers on its own, and one connection is already built for you

Everything so far has lived inside a single message as it crossed the canvas. This part introduces the first state that outlives a message — a countdown that keeps running after the wire that started it has gone quiet, a counter that has to mean the same thing the next time a different message asks for it. It also introduces the thing every later part in this guide leans on: the map of the 32 Home Assistant node types your add-on already ships, and the one connection among them you are not supposed to build yourself. Get both wrong and the failure looks the same — something that seemed to work now quietly does not, on a restart you did not even think to test.

32
NodeTypes registered by HA WebSocket 0.80.3 — 19 executable, 9 companion-entity, 3 shared config, 1 kept only for compatibility
30s
localfilesystem's default flushInterval. A context value can sit in memory that long before it is actually written to disk
3
Version floors that all sound like “the” Home Assistant requirement — the WS package README says 2024.3+, the add-on manifest says 2023.3.0, a source constant says 2023.12
Two things that do not remember

“It worked yesterday” is not the same claim as “it survives a restart”

Take a request as ordinary as “turn off the light two minutes after motion stops.” That single sentence quietly asks for timing, for handling a second motion event before the two minutes are up, for canceling the countdown if the room is occupied again, and for deciding what happens if Node-RED itself restarts halfway through the wait. “How many notifications went out today?” asks a smaller but just as easy to get wrong question: which part of the flow owns that number, and does it still mean anything after the add-on comes back up?

This part builds neither light nor notification. Every example below ends at a Debug node, the same discipline Part 2 established, because the two subjects here — timers and state — are exactly the ones people get burned by once they are wired to something that actually flips a switch. Later parts move this into real Home Assistant flows once the underlying behavior is second nature.

None of this is a durable job scheduler. A Delay node's queue, a Trigger node's pending timers, and memory or file-backed context are not guaranteed to survive a restart. Even a localfilesystem context store does not automatically preserve a timer that is counting down inside a node — the countdown lives in the node's memory, not in the store, no matter what you have configured.

The second half of this part turns to a different kind of “it looks done and is not”: the 32 node types your Home Assistant integration ships with. The add-on already has a working connection to Home Assistant before you drag a single node onto the canvas. The most common way to break that connection is to try to be helpful and build a second one.

In plain terms

A Delay queue and a Trigger countdown are both more like a kettle left on the stove than a slow cooker with a timer built in. Walk away and it keeps going for a while, on its own heat. Cut the power — a restart — and there is no plug-back-in that resumes where it left off. Whatever was mid-boil is just gone, and the pan starts cold the next time you turn the stove on.

Delay and Trigger

Queueing and resetting are different jobs, and one node is not secretly the other

In the Node-RED 5.0.2 runtime, Delay supports a fixed delay, a dynamic delay overridden by msg.delay, a random delay, and several rate-limiting paths. In delay mode, a message carrying msg.reset clears whatever is waiting in the queue, and one carrying msg.flush releases it early. Rate-limit mode can queue messages or drop them, and it is bounded by the runtime's nodeMaxMessageBufferLength setting. Treat all four of those — delay, rate, reset, flush — as control inputs: do not let untrusted input set any of them freely.

Trigger, in the same 5.0.2 build, can run one shared timer for every message or keep a separate timer per value of a chosen message property, most often msg.topic. With extend enabled, a new message restarts the wait instead of queuing behind it; msg.reset cancels the matching timer outright. That behavior is closer to “only send a turn-off candidate after the last motion event has gone quiet” — and it is still only a candidate. Query the entity's current state and check occupancy again before you act on it. A message that is two minutes old is not a current fact.

You need “send one notification per minute and queue the rest” — that is Delay's rate-limit mode, set to queue. Every message matters and the total is bounded.
You need “keep only the newest reading and drop anything older” — that is Delay's rate-limit mode, set to drop. Telemetry the next value supersedes anyway.
You need “turn the light off only once motion has stopped for good, and start the wait over if motion comes back” — that is Trigger, with extend switched on.
You need “send a value now and a different one after a fixed wait” — that is Trigger's ordinary two-stage output, with no extend involved.
RequirementBetter fitRequired safeguard
Delay every message by the same intervalDelayLimit queue length and input rate
Control the rate of API callsDelay, rate limitTimeout, an error branch, and a queue policy
Output only after the last messageTrigger, with extendSeparate timers by topic, and provide a reset path
Send a start value now and an end value laterTriggerDefine repeated-input and restart behavior up front
flowchart TD
  A["A new message reaches
a Delay or a Trigger node"] --> B{"Which one, and
which mode?"} B -->|"Delay, queue"| C["The message waits.
msg.flush releases it early,
msg.reset clears the queue"] B -->|"Delay, rate limit, drop"| D["Only the newest message
survives. Older ones in the
same window are discarded"] B -->|"Trigger, extend on"| E["The countdown restarts from
zero. msg.reset cancels it,
with no end output at all"]
Three outcomes, one decision pointAll three end boxes hang off the same diamond, and none of them is a dead end in the bad sense — they are three legitimate behaviors, not one right answer and two mistakes. Read the diamond's question first: it is not “did this work,” it is “which node and mode did you actually pick.”
Emergency alerts do not belong behind a shared Delay queue. Rate limiting controls cadence and nothing else — it does not confirm a downstream success, and it is not a substitute for retries or backoff. And if timers are separated by msg.topic, that set of topics has to stay bounded. A source that keeps generating new topic values — broken code, or something hostile — leaves Trigger's topic map accumulating pending timers forever. Put a Switch allowlist in front, or map anything unrecognized to one fixed quarantine topic, before it reaches the node.
node, flow, global

Pick the narrowest scope, because the wider ones do not tell you who else is using them

Node-RED gives you three places to keep a value between messages, and the question that chooses between them is always the same: which nodes actually need to read this? node context belongs to one node instance — use it for that node's own counter or previous value. flow context is shared by every node on the same tab — use it to coordinate one automation. global context reaches every flow in the whole runtime, which is exactly why it should hold only data that truly has to cross flows and has one clear owner.

A fourth term sits next to those three and answers a different question. Scope answers “who can read this value?” A context store answers “where does it actually live?” Do not conflate the in-memory default with a named localfilesystem store — the next section is entirely about that gap.

flowchart TD
  subgraph GLOBAL["Global scope, reachable from every flow"]
    G["global.get() / global.set()"]
  end
  subgraph TABA["Flow tab A"]
    A1["Node 1, its own node context"]
    A2["Node 2, its own node context"]
    AF["Flow context for tab A,
shared by nodes on this tab"] A1 --> AF A2 --> AF end subgraph TABB["Flow tab B"] B1["Node 3, its own node context"] BF["Flow context for tab B,
a separate value from tab A"] B1 --> BF end AF --> G BF --> G
Only the flow boxes reach upFour node-context boxes sit inside their two tabs, and none of them has an arrow of its own into Global — only the two flow-context boxes do, one per tab. Tab A's flow context is fed by two node-context boxes and stays a completely separate value from Tab B's, even though both happen to write up to the same Global box underneath.
StateRecommended scopeWhyLimit or cleanup
A node's consecutive error countnodeNo sharing requiredCap it, and reset to zero on recovery
A notification-suppression flag for one roomflowSeveral nodes coordinateA boolean or small timestamp, with an explicit reset
Site-wide maintenance modeglobal, used cautiouslyMultiple flows need to read itOne clear owner, a safe default, and a change log
Complete sensor historyNot context at allUnbounded, and queried differently every timeA managed data system with its own retention policy
In plain terms

node context is a sticky note on your own desk — nobody else even knows it is there. flow context is the whiteboard in your team's own room — anyone on that team reads and writes it, and a different team down the hall has never seen it. global context is the noticeboard in the building lobby: every team walks past it, which is exactly why you do not pin your team's rough draft there. The wider the board, the more people can quietly change what it says.

Name a context key by what it is for, not by what it happens to describe — notificationCount or maintenanceMode, not data or temp, and never an address, a username, or a device ID folded into the key itself. Deleting a flow or moving a node can change the identity behind its node or flow scope, so rerun your initialization tests after any upgrade or refactor. A Change node's typed input, or the Function API, can also name a specific store: if that store does not exist, the runtime logs an unknown-store warning and quietly falls back to the default store. That fallback is not successful persistence — check the runtime log and the actual restart behavior before you trust it.

What survives a restart

Memory forgets on purpose. localfilesystem remembers on a delay, not instantly

When no contextStorage plugin is configured, the Node-RED 5.0.2 runtime uses the memory store by default. That is fast, and entirely appropriate for a value that only needs to last a few messages — but it is not expected to survive a process restart. Getting localfilesystem means configuring it explicitly in the runtime settings, and this part does not walk you through touching your add-on's shared settings directly: a mistake there affects every flow at once, so back up first and make that particular change during a maintenance window.

localfilesystem keeps its own memory cache turned on by default, and its documented default flushInterval is 30 seconds — the minimum gap between writes to storage, there to reduce wear on the underlying disk. A set updates that cache immediately and marks the write as pending; a timer flushes it to disk later, and an ordinary shutdown also attempts one last flush. Nothing you write is durable the instant you write it. A sudden power loss or a process crash can lose whatever updates had not been flushed yet.

sequenceDiagram
  participant F as A flow, calling context.set()
  participant M as Memory cache
  participant D as Disk (localfilesystem)
  F->>M: first set() call
  M->>M: cache updated, write marked pending
  Note over M,D: up to flushInterval (30s default) passes
  M->>D: the pending write flushes to disk
  F->>M: a second set(), moments before a crash
  Note over M: the process crashes right here
  M--xD: this second write was still pending, and never arrives
One write survives, one does notTwo calls leave the flow for the memory cache, and only the first one crosses the note-bounded gap into Disk before the crash. The second arrow is drawn broken with a cross, meaning it never lands — that is the write the crash swallows, and the diagram has no arrow at all showing it reaching Disk.
In plain terms

Memory context is a note on your own hand — instantly there, and gone the moment you wash up. localfilesystem is more like a mailroom that does one pickup every 30 seconds: drop a letter in the tray and it is not actually in the postal system yet, it is just sitting in the tray waiting for the next round. Most of the time that is fine. The one moment it is not fine is a fire in the mailroom between pickups — the letter you dropped seconds ago is gone with it, and no amount of trusting the tray changes that.

Saving to disk this way is not a database transaction, and it is not a backup. Serialization logs a warning on a circular reference, and live objects, functions, and anything else unsuitable for JSON should never be stored. A full disk, a permissions error, file corruption, or a failed backup or restore can all still cause data loss on top of the flush gap. For anything safety-critical, let Home Assistant or a dedicated data service stay the authoritative source, and re-check it when the flow starts rather than trusting old context blindly.

A conceptual settings.js sketch, for review only — do not apply anything like it without a backup and a maintenance window:

KeyValueWhat it does
default"memoryOnly"Names which store below is used when a node does not ask for one by name
memoryOnly.module"memory"The fast, non-durable store — the runtime's own out-of-the-box default
durable.module"localfilesystem"A second, named store — only values explicitly pointed at this one get flushed to disk
Do not shorten the flush interval and call it zero data loss. Writing more often increases wear and I/O without ever turning a cache-then-flush design into a transaction. If your environment approves a durable store at all, keep high-frequency transient counters in memory and send only the handful of small values that truly must outlive a restart to the durable one. After the change, check the runtime startup log and the store shown in the Context sidebar, then test both halves of the actual contract: write, wait long enough to flush, restart normally, and verify — and separately, accept that an abnormal interruption can still lose whatever had not flushed yet.

Four limits every long-running flow needs

Bound four things at the input, rather than trusting host memory to catch the overflow for you: the maximum number of pending messages, the maximum number of timers or topics, the maximum size of any one context value, and the maximum lifetime a piece of state is allowed to have. Route anything over those limits to a Catch, Status, or diagnostic branch, strip the payload down, and record only the count — not a full household event log.

TestProcedureExpected observation
Single messageInject one message by handStart value, the wait, then the end value
Repeated messageInject again during the waitWhether it queues, drops, or extends the timer — on purpose, not by accident
CancellationSend a reset with a fixed topicOnly the matching timer clears, with no end output at all
Parallel timersInterleave two fixed topicsTimers stay isolated, and the topic map itself stays bounded
RestartRestart during a countdown, through an approved processThe pending timer is gone, not resumed; startup enters a safe state
ContextRestart after writing a valueMemory state disappears; a configured file store is checked against its real contract, not its name
Over limitTest the limit with a small, controlled burstA visible, deliberate rejection — never an unbounded queue

A safe startup policy for entrance lighting, in plain terms: after a restart, do not resend an old turn-off command left over from before. Wait for the next real event, then query current occupancy and the light's current state before doing anything. A notification counter can reset at a daily boundary and carry a hard maximum, rather than climbing forever. Daylight saving time, time zones, and calendar-style scheduling belong to the Home Assistant time nodes covered later in this guide — Delay's millisecond wait is not a calendar.

Test with the side effect disconnected first. Simulate with a disabled Action node, or a Debug node alone. Only once that passes does every downstream node with a real side effect still need a state query, an allowlist, error handling, and a manual recovery path of its own. And do not load-test the production add-on with a flood of Inject messages — run that kind of test in an isolated environment with a defined stop condition.
The connection you already have

Home Assistant nodes are more than a button that calls a service

The Home Assistant integration bundled with this guide's pinned add-on — 22.0.1, carrying Node-RED 5.0.2 and HA WebSocket 0.80.3 — registers 32 node types. Some receive events, some read from a cache, some call an action with a real side effect, and a handful create companion entities inside Home Assistant itself. Treat them all the same and you will eventually mistake a read for a trigger, use a shared config node as if it were a flow step, or hand untrusted input straight to a high-risk action node.

The add-on's own manifest declares homeassistant_api: true, granting it an authorization capability, and the package documentation offers add-on users a specific “I use the Home Assistant Add-on” connection mode. Together those two facts describe the preconfigured path this whole guide follows.

If you are on the add-on, do not create or paste a long-lived access token. The preconfigured connection does not need one. Authenticating a standalone, non-add-on Node-RED install is a different deployment altogether, with its own credential handling under a least-privilege policy — not something to reach for here just because a field for it exists on screen.

Confirming the connection without touching it

  1. Step 1

    Confirm the baseline, and back up first

    Check the version on the add-on's information page before you change anything, and back up your flows and credentials. Say “add-on 22.0.1, bundling Node-RED 5.0.2,” not “Node-RED 22.0.1” — they are not the same number, and mixing them up makes troubleshooting harder for everyone, including future you.

  2. Step 2

    Start with a read-only node

    Drop in a Current State node, or another node that only queries after a manual message. Do not reach for Action, Fire Event, API, Webhook, or a companion switch for a first connection test.

  3. Step 3

    Select the existing Server config — do not create one

    In that node's Server field, pick only the Server config the add-on already preconfigured. If it is missing from the list, stop and go straight to the troubleshooting table below rather than adding a substitute of your own.

  4. Step 4

    Use a placeholder entity

    If a test needs an entity, use one approved for testing in your own environment, and write sensor.example_temperature in any notes or documentation you keep — never the real ID. The same goes for device, area, tag, zone, and webhook IDs.

  5. Step 5

    Deploy narrow, then validate the failure paths

    Use the narrowest Deploy scope, check the node's status and the add-on log, and keep Debug limited to the fields you actually need — never the whole config, headers, or state attributes. Then, without touching credentials, test a missing entity and an unknown or unavailable state, with Catch and Status nodes confirming that a failure never reaches a node with a real side effect.

The Server editor itself carries the connection mode, a connection delay, a base URL and access token for non-add-on connections, certificate options, a heartbeat, plus settings for exposing HA booleans to global context, the autocomplete cache, and selector or status UI behavior. Do not disable certificate validation to work around a troubleshooting dead end, and do not paste a base URL, an access token, or a full config export into a public issue or a message to someone helping you.

Current State, Events: state, Action, and several other nodes all point at that one Server config, so changing it is a shared change, never one confined to a single dialog. Check its references before you touch it, and review the actual scope Node-RED reports under Modified Nodes and Flows afterward. Connection delay and heartbeat manage the connection itself — they are not application-level retries, and neither one can guarantee that a particular action will succeed.

A cached state is not necessarily a fresh one. Current State and Get Entities both read from the HA server cache, which makes them fast but does not make the value current. Handle disconnection, startup, unknown, and unavailable explicitly, along with entities that get added or removed later. When you output state attributes anywhere downstream, allowlist only the fields the decision actually needs.
32 node types, four categories

Executable, entity, config, deprecated — and only one is not for a new flow

You do not have to memorize all 32 before you can be useful. The learning path this guide follows runs: state events first, then state queries, then the Action node for the first real side effect, then the remaining event and time nodes, and only after that the practical projects — motion lighting, notifications, and the companion entities Home Assistant sees back. What matters right now is being able to place any node you meet into one of four buckets before you wire it into anything.

flowchart TD
  T["HA WebSocket 0.80.3
32 NodeTypes total"] --> E["19 executable nodes
Events, Current State,
Action, and more"] T --> N["9 companion-entity nodes
Sensor, Switch, Button,
and Update Config"] T --> C["3 config nodes
Server, Device Config,
Entity Config"] T --> D["1 deprecated node
the old generic Entity"]
Four branches, declared in this orderAll four boxes hang directly off the total at the top, left to right in the order they are declared here: executable, companion-entity, config, then deprecated last on the right. Only that rightmost box, Deprecated, is not meant for a new flow — the other three are all still current.
You want to react to a light turning on — that is Events: state, an executable node firing on the state_changed event.
You want to check what a thermostat reads right now, only when a message arrives — that is Current State, a query, not a subscription.
You want to turn a switch on — that is Action, the node with the real side effect. Its persisted type is still api-call-service, whatever the palette currently calls it.
You want Node-RED itself to show up as a device — that is a companion-entity node such as Sensor or Switch, not a query of something that already exists in Home Assistant.

The palette's current label for that third example is Action; older flows and documentation still call it Call Service. Use the old name only to recognize existing material — in 0.80.3 the persisted type field in your flow JSON stays api-call-service regardless of what the editor displays. Never hand-edit an exported JSON file's type to action, and never recreate credentials just because a label changed.

Config nodes hold settings; executable nodes act on a message

In plain terms

A config node is the address book entry, not a stop on the delivery route. The Server config holds the address, the phone number, the way in — a dozen different nodes look it up before they act, but the address book itself never moves a single package. Action, Current State, and the rest are the actual couriers: they read the address book, then go do something with a real message in hand. Delete the address book entry and every courier who relied on it is suddenly standing in front of a door with no address to give.

A Server config is shared configuration, persisted as type server. It is not an ordinary step with an input and an output — it can sit invisible on the canvas even while more than a dozen other nodes depend on it, which is exactly why deleting an invisible setting can silently disconnect a whole group of nodes at once. Device Config and Entity Config are the other two real config nodes, holding metadata for companion devices and entities. Update Config looks similar by name but is not one of the three — it is an executable utility with one input and one output that edits that companion metadata.

IssueLayer to fix it in
HA connection failure, or the shared heartbeat/configThe Server config, and the add-on log
Wrong selector or output property on one entityThat specific executable node
A companion entity's name or metadataEntity Config or Device Config, plus the entity node itself
Target, data, or response for one particular actionThe Action node's own input contract — not the Server

Read the current Action UI as three parts: action, target, and data. Target selects entities, devices, or areas; data supplies the parameters. What comes back depends entirely on the underlying HA action and is not guaranteed to land in msg.payload — Current State, Get Entities, Action, and several others all let you configure the output property, so never assume a node always overwrites the payload by default.

Update Config takes an explicit allowlist, and nothing else. It reads a target id from its input and can update name, icon, entityPicture, and options. Build the message reaching it from that allowlist alone; strip every other override before the node, and refuse anything arriving from HTTP, MQTT, or a webhook without validation first. Never wire raw msg.payload from an unverified source straight into Update Config — or, for that matter, into the API, Fire Event, or Action node, or any companion switch or button. Those are exactly the surfaces with real side effects, and Action and Current State's own Block Input Overrides setting does not generalize to every node on this list.

Binary Sensor and Sensor mostly output state; Button triggers a flow when pressed inside Home Assistant; Number, Select, Text, and Time accept a constrained value; Switch carries get, set, and listen behavior all at once. Every one of those types still needs its own policy for availability, input validation, uniqueness, and what happens when it is removed or migrated — none of that is automatic just because the node exists.

All 32 node types, by category

home_assistant — 19 executable nodes

NodeType / persisted typeRole
Action / api-call-serviceCalls an HA action; has a real side effect
API / ha-apiAdvanced WebSocket and HTTP API access — needs an allowlist, timeouts, and care with sensitive output
CurrentState / api-current-stateQueries one cached entity when a message arrives
Device / ha-deviceUses an HA device automation trigger or action definition
EventsAll / server-eventsSubscribes to the HA event bus, optionally filtered by event type
EventsCalendar / ha-events-calendarFires on calendar entity event times
EventsState / server-state-changedHandles state_changed events and old/new state
FireEvent / ha-fire-eventSends an HA event; has an external side effect
GetEntities / ha-get-entitiesFilters cached entities into array, count, random, or split output
GetHistory / api-get-historyQueries HA history; bound the time range and result size
PollState / poll-stateReads an entity on a schedule; prefer an event-driven design first
RenderTemplate / api-render-templateAsks HA to render a Jinja template — a different language from Mustache or JSONata
Sentence / ha-sentenceAn Assist/conversation sentence trigger
TriggerState / trigger-stateCombines a state trigger with conditions and constraints
Tag / ha-tagReceives HA tag events; de-identify the tag ID in any notes
Time / ha-timeSchedules by a fixed or entity-derived time, day, and offset
WaitUntil / ha-wait-untilWaits for an entity condition or a timeout; bound the number of pending waits
Webhook / ha-webhookReceives a webhook trigger; its URL and ID are secret entry points
Zone / ha-zoneDetects entry and exit; location data is sensitive

home_assistant_entities — 9 nodes

NodeType / persisted typeRole
BinarySensor / ha-binary-sensorExposes and updates a binary sensor in HA
Button / ha-buttonExposes a button whose press triggers a flow — validate the source and its side effects
Number / ha-numberExposes a bounded number entity
Select / ha-selectExposes a select entity with allowlisted options
Sensor / ha-sensorExposes and updates a sensor; avoid high-frequency or sensitive attributes
Switch / ha-switchExposes a switch; a set operation can enter the flow and cause a side effect
Text / ha-textExposes a text entity; limit input length and permitted content
TimeEntity / ha-time-entityExposes a time entity — different from the executable ha-time trigger
UpdateConfig / ha-update-configExecutable utility that updates companion metadata — restrict fields, reject untrusted overrides

config — 3 nodes, plus 1 deprecated

ClassificationNodeType / persisted typeRole
configServer / serverShared HA connection and package settings
configDeviceConfig / ha-device-configCompanion device metadata
configEntityConfig / ha-entity-configCompanion entity metadata
deprecatedEntity / ha-entityLegacy generic entity — kept for maintenance and migration, not for new flows
Three version floors

Three numbers that all sound like the same requirement, and are not

The 0.80.3 package README lists its own user prerequisites: Home Assistant 2024.3+, Node-RED 3.1.1+, and Node.js 18.2.0+. Those last two are also verifiable straight from the package metadata. That trio is what the package itself asks of you.

The add-on's own config.yaml separately declares homeassistant: 2023.3.0 — the Supervisor's installation floor for the add-on, which does not replace the package's 2024.3+ requirement. A third number, the internal constant HA_MIN_VERSION = '2023.12' inside the package's own source, cannot lower that user-facing floor either. All three numbers live in different files, answer different questions, and need to be reported separately if you ever ask for troubleshooting help.

SourceValueCorrect reading
HA WS READMEHA 2024.3+The user-facing prerequisite for 0.80.3
HA WS package.jsonNode-RED ≥3.1.1, Node ≥18.2.0The package's own engine and host floors
Add-on config.yamlHA 2023.3.0The Supervisor's installation floor — not a guarantee the package's features all work
HA WS const.ts2023.12An internal constant; it does not replace the README's number
Add-on package.jsonNode-RED 5.0.2, HA WS 0.80.3This guide's own pinned baseline

Before upgrading any one of those layers, back up first, read the release notes and migration guidance, and test the change in a flow with every side-effecting node disabled. Legacy Entity nodes, the persisted Action type, output-property settings, and unknown, unavailable, or null old/new states are the migration hazards that actually show up in practice. Do not search-and-replace a flow's exported JSON types by hand, and never treat a new palette label as if it were the persisted type underneath it.

Every entity, device, area, tag, zone, and webhook ID is environment-specific. Webhook URLs and IDs are especially sensitive, since they are entry points into your system. In documentation or an issue report, use a placeholder such as sensor.example_temperature and strip access tokens, headers, locations, calendar contents, and state attributes before you share anything. Never copy a fictional ID into a production flow, and never let a documentation example bypass the allowlist your real environment actually enforces.
When it does not work

Twelve symptoms, and which of them is actually a bug

SymptomLikely causeWhat to do
Every motion event triggers a later action, and actions pile up You are queueing with Delay when the job actually calls for Trigger with extend Disconnect the side-effect output first, clear the queue, then use repeated manual inputs to verify the reset behavior before reconnecting anything
Trigger's reset does nothing Timers are separated by topic, and the reset message's property does not exactly match the original Compare the two topic values directly, and make sure unknown topics cannot silently create new timers
A counter returns to zero after a restart No contextStorage is configured, so context is running on memory Confirm the current store. If the value truly must survive restarts, back up and review the change before configuring a named localfilesystem store, then run the restart test yourself
A recent file-store write is missing after a power failure The write was sitting in the memory cache, inside the flush gap, when power was lost This is consistent with the cache-and-flush design, not a bug. Never promise zero data loss from it; let an authoritative system reconstruct anything safety-critical instead
The Context sidebar shows a value a Function node cannot read A mismatch in scope, tab, key, or named store between the two places Check node/flow/global scope, the tab, the key, and the store name all line up, and look for an unknown-store warning in the runtime log
Memory or storage keeps growing An unbounded Delay queue, Trigger topic map, pending Join, or context array Stop the source first, then set limits and expiry, and back up any de-identified diagnostics you need before cleanup
The add-on's preconfigured Server config is missing Something is wrong with the install or the connection itself, not with your flow Stop immediately. Confirm you are in the right add-on editor, that startup and the add-on log look normal, and check the installation against the pinned add-on documentation — do not add a Server config or paste in a token to work around it
Only one node produces no output Its trigger or input mode, entity selector, or output property is wrong, or it received unknown/unavailable Isolate it with a manual Inject and a Debug node scoped to only the fields you need — do not touch the shared credentials to chase this
A legacy Entity node appears after an import That is the deprecated ha-entity type Back the flow up, read the migration guidance, and map it to a specific companion entity by hand — never edit its JSON type directly
You cannot find a JSON type called action The palette label changed; the persisted type did not Expect api-call-service in the exported JSON — that is correct, expected compatibility behavior, not something to fix
A version above the stated install floor still looks incompatible You are comparing against the wrong one of the three floors above Do not confuse the add-on's HA 2023.3.0 floor with the package's HA 2024.3+ prerequisite. Record all five layers — add-on, Node-RED, HA WS, HA, and Node.js — when you ask for help
Debug output discloses an ID or an attribute it should not Full-message Debug output was left on somewhere Turn it off immediately, clean the sidebar, logs, and any exports, and substitute placeholders before opening a public issue. If a credential may have leaked, rotate it through your incident-response process and do not pass the value itself to anyone helping you
Questions people ask

The ones that come up again and again

Repeated events produce several notifications in a short window. Delay or Trigger?
If the goal is to limit the rate or send messages in sequence, look at Delay first. If each new event should reset or extend the same waiting window instead, look at Trigger. Either way, bound the number of queued messages or timers, and wire Debug in front so you can watch bursts, resets, and timeouts before anything real is connected.
Does flow.get("exampleKey") inside a Subflow read the parent tab's value?
No. A Subflow has its own flow context, separate from its parent. Only when the design genuinely needs to cross that boundary should you reach for flow.get("$parent.exampleKey"), which is how the Node-RED context API accesses the parent flow's context on purpose — and document the owner, default value, and cleanup policy when you do. Otherwise keep the value inside the Subflow and avoid the hidden coupling entirely.
Does localfilesystem guarantee that no write is ever lost?
No. Its default cache mode updates memory first and writes to disk on its flush interval, so a sudden interruption can lose whatever was still pending. It is not a database transaction, and it is not a backup.
Can I keep a complete Home Assistant state object in global context?
It is not recommended. Doing so widens both privacy exposure and coupling, and the stored data can grow without any real bound. Keep only the small, de-identified derived values a flow actually needs, with size and expiry limits attached, and go back to the authoritative source whenever you need the current state.
Will a Trigger countdown resume after a restart?
Do not assume so. A pending timer sitting inside a node and a context store are two entirely separate mechanisms. Define a safe startup policy instead — discard the old candidate, wait for a fresh event, and query current state again before acting on anything.
Do add-on users need to create a long-lived access token?
No, and they should not. Select only the Server config the add-on already preconfigured. If it is missing, stop and troubleshoot rather than adding a connection of your own, and never put a credential into a msg or a Debug output.
Can Update Config double as a general way to write entity state?
No. It updates configuration and metadata for a companion entity, and should only be used once an approved field genuinely needs changing. Validate the source of any external input first, then pass through only an approved target plus the id, name, icon, entityPicture, or options allowlist — reject every other override.
Does every flow need its own Server config?
No. The add-on path reuses the one existing preconfigured shared config. Check every reference to it before changing or deleting it, and if the preconfigured config is missing, troubleshoot rather than creating a second one yourself.
Which Home Assistant version floor should I actually trust?
Go back to the compatibility section above in this same part: the package's own README states HA 2024.3+ as the real user-facing prerequisite. The add-on's 2023.3.0 is a separate Supervisor installation floor, and the source constant reading 2023.12 is internal and does not override either number.
Must a flow create a companion entity the moment it calculates a temporary value?
Not necessarily. If only the current flow's own decision needs the value, a bounded msg or a small piece of context is enough. Reach for a companion entity only once the HA UI or another HA automation genuinely needs a stable reading, and settle availability, input validation, privacy, uniqueness, and removal policy before you create it.
Can an external HTTP request update a companion entity's name directly?
It must never connect straight to Update Config. Authenticate the source first, then enforce an allowlist of targets and fields, passing through only approved id, name, icon, entityPicture, or options values and rejecting everything else.
Next

Where to go from here

context ends, Home Assistant begins

Part 1 is done. Nothing you built in it could reach a light or a lock.

Every flow across these four parts stopped at a Debug node on purpose. Part 5 opens Part 2 of this guide by wiring the first node that actually listens to Home Assistant — Events: state, watching a real state_changed event go by, using the Server config this part told you not to rebuild. The scope discipline, the restart tests, and the placeholder-entity habit all travel forward unchanged; the only thing that changes from here is that a real entity is finally on the other end of the wire.

Open the full guide

Part 4 of the WoowTech Complete Node-RED Guide series on the Apporo blog.

Adapted from the WoowTech Complete Node-RED Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.

Light · Air · Water · Control · apporo

msg carries more than payload, and guessing what type is inside gets expensive fast