For about two years, the bulk of my Home Assistant automation logic lived in Node-RED. The core reason was visual — I liked seeing the flow as a diagram, with branches and triggers and gates laid out left-to-right. I could glance at a flow and immediately understand what it did, in a way that I couldn’t with a 200-line YAML automation file.
In early 2026 I migrated all of it to native HA YAML automations. The migration took a long weekend. I’m glad I did it. This post is what triggered the move, what was harder than I expected, and the patterns I ended up using to keep the YAML readable.
Why Node-RED, originally
When I built the first version of my HA install, native automations were less expressive than they are now. The classic example: a multi-condition trigger like “person arrives home AND it’s after sunset AND the home mode is Away” was awkward to express in YAML — you’d write a trigger, then a conditions block, then in the action you’d need a separate template if any of the action steps depended on which trigger fired. In Node-RED, the same logic was three nodes connected by lines, and which trigger fired was self-evident from the flow.
For a couple of complex automations — the welcome-home flow, the morning briefing, the weather-alert escalation — Node-RED was genuinely easier to write and read. So Node-RED won, and over time more and more logic migrated into it.
What changed
A few things, accumulating, made me reconsider:
- HA’s automation engine got better. Trigger IDs (added in 2022.8) made it possible to know which of multiple triggers fired without ambiguity.
choose:andparallel:action blocks let you build branching logic in YAML that’s only slightly more verbose than Node-RED’s visual equivalent. Variables-from-triggers ({{ trigger.id }},{{ trigger.event.data.X }}) made it possible to extract context cleanly. - The Node-RED flow editor on a tablet was painful. I do a lot of HA editing from an iPad on the couch. The Node-RED editor really wants a desktop browser. Many small changes were “wait until I’m at my desk” tasks, and they piled up.
- Version control was awkward. Node-RED stores flows as a single big
flows.json. Editing it in a UI generates large, opaque diffs. I’d commitflows.jsonto git, and a one-line logic change would show up as a 2000-character diff because the editor reordered nodes. Native YAML automations diff cleanly — the change in YAML is the change in behavior. - The dependency surface was bigger than I wanted. Node-RED is an add-on container with its own runtime, its own update cadence, its own breakage modes. Twice in 2025 I had a Node-RED update that broke specific node types I was using; both times the fix was reverting the add-on. I wanted to reduce the surface area of “things that can break independently of HA itself.”
The migration plan
I gave myself a long weekend (Thursday evening through Sunday night) and worked through the flows in three phases.
Phase 1 (Thursday): inventory and triage. I exported flows.json, ran through every flow, and put each one into one of three buckets:
- Migrate — actively used logic that needs an HA equivalent
- Drop — flows I’d built and forgotten, or that had been replaced by something else without me cleaning up
- Defer — flows that did something genuinely hard to replicate in YAML, where I’d want to think harder before migrating
The “drop” bucket was about 30% of the total — flow rot is real. The “defer” bucket was small (3 flows). The “migrate” bucket was about 25 flows.
Phase 2 (Friday-Saturday): mechanical migration. I worked through the migrate list one flow at a time. Each one got rewritten as a YAML automation, named to match the original flow, dropped into the appropriate file under automations/. I disabled the Node-RED flow but didn’t delete it, so I could compare behavior.
Phase 3 (Sunday): comparison and cleanup. With both versions running side-by-side (Node-RED disabled but in place), I exercised the automations — opened doors, triggered presence, simulated weather conditions — and watched the YAML versions fire. Anywhere they didn’t match the Node-RED behavior, I fixed the YAML. Once a flow was confirmed working, I deleted it from Node-RED. By the end of Sunday I’d migrated everything and uninstalled the Node-RED add-on.
The patterns that made YAML readable
Three patterns turned out to matter more than I expected:
Trigger IDs, always
Every trigger gets an id. Even if there’s only one trigger today. The reason is that adding a second trigger later is a much smaller change if the first one already has an ID — you don’t have to retroactively rewrite the action block to know which trigger fired.
triggers:
- trigger: state
entity_id: person.charles
to: 'home'
id: charles_home
- trigger: state
entity_id: person.partner
to: 'home'
id: partner_home
Then in the action: {{ trigger.id }} tells you which person triggered, and you can branch on it cleanly with choose:.
choose: blocks instead of nested templates
The temptation in YAML is to write a single action with a templated value that handles all cases:
# DON'T do this
actions:
- action: notify.charles
data:
message: >
{% if trigger.id == 'charles_home' %}Welcome home Charles
{% elif trigger.id == 'partner_home' %}Welcome home Partner
{% else %}Someone arrived
{% endif %}
That works, but the logic gets opaque fast. choose: is more verbose but reads better:
actions:
- choose:
- conditions:
- condition: trigger
id: charles_home
sequence:
- action: notify.charles
data:
message: "Welcome home"
- conditions:
- condition: trigger
id: partner_home
sequence:
- action: notify.partner
data:
message: "Welcome home"
Each case is self-contained. Easy to add a third person; easy to delete a case; easy to read three months later.
Package files for related logic
The single biggest readability win was splitting automations into package files by domain. My packages/ directory has:
solar.yaml— anything solar-related (sensors, automations, utility meters)dashcam.yaml— dashcam ping sensors and notificationsalarmo_garage.yaml— Alarmo’s view of the garage door
A package can include sensor:, binary_sensor:, template:, automation:, script:, input_boolean: — anything HA understands. By keeping all the related logic in one file, you can read a feature top-to-bottom and understand it without bouncing between five files.
The flat automations/ directory is still organized by category — security.yaml, presence.yaml, seasonal.yaml, system.yaml — but each automation in those files is self-contained. Cross-feature logic that bridges domains lives in packages/.
What was harder than expected
A few things bit me:
Trigger ordering with for:. Node-RED’s trigger nodes have explicit “fire only if state has been X for Y seconds” handling. HA’s for: on triggers does the same thing, but the combination with state changes during the wait is subtle. If you have trigger: state, to: 'on', for: 5 minutes, and the entity goes on → off → on within five minutes, what happens? (Answer: the timer restarts on the second on, and only fires after a continuous 5-minute on. This is what you want, but it took testing to convince myself.)
Mode behavior. Node-RED flows are inherently parallel — every input message gets processed concurrently unless you explicitly serialize. HA automations default to mode: single — a second trigger while the first is still running gets dropped on the floor. For automations where I’d been relying on Node-RED’s parallel behavior (like the laundry-finished notification, where washer and dryer can finish within seconds of each other), I had to switch to mode: queued or mode: parallel and verify the behavior was what I wanted.
Logbook noise. YAML automations generate logbook entries when they trigger and when they execute. Node-RED flows didn’t (Node-RED has its own debug log, separate from HA’s logbook). After the migration the logbook got busier. Mostly fine, but a couple of high-frequency triggers (the presence sync, the household occupied template) generated enough log entries that I tuned them down with conditions.
What I’d do differently
Two things, with the benefit of hindsight:
Don’t do it in one weekend. Doing it in a long burst meant I made a few rushed decisions that I had to revisit later — automations that work, but were structured awkwardly because I didn’t think carefully about the right shape under time pressure. A migration spread over two or three weeks with a flow-per-day cadence would have produced cleaner output.
Migrate the simplest flows first to build confidence. I started with the most complex flows because they were what felt urgent (those were the ones I most wanted out of Node-RED). I should have started with the simple ones to validate my migration patterns and the YAML idioms before tackling the hard cases. Several of the early hard cases got rewritten later anyway, once I’d developed better patterns.
What still bothers me about YAML
Two things, honestly:
Templating is awkward when it gets complex. A multi-line Jinja template with nested conditionals is harder to read than a Node-RED function node with the same logic in JavaScript. For the few automations where the template is doing real work (the unlock-by-name notification template from the Z-Wave lock post, for example), I’m aware that Node-RED would have been more readable. Not enough to migrate back, but it’s a real cost.
Loops are missing. HA actions don’t have a clean way to iterate over a list with a per-item action. There’s repeat: with for_each:, which works, but it’s verbose and slightly undocumented. Node-RED’s split node was much cleaner for any case involving a list.
These are minor relative to the gains. Nine months in, I haven’t missed Node-RED on net. The version-control story alone — being able to look at a YAML file and see the actual logic, instead of a JSON blob — is worth the migration.
Should you migrate?
If you’re starting fresh: skip Node-RED. HA’s native automations in 2026 are good enough that the visual flow editor isn’t worth the extra dependency.
If you have an existing Node-RED install that works: probably don’t migrate just because you read this post. Migrate if (a) you’re hitting one of the friction points I hit (mobile editing, version control, dependency surface), (b) you’re already restructuring your HA config and the migration fits naturally, or (c) one of the new YAML features (trigger IDs, choose: improvements, label-based selection) finally lets you express something cleanly that was awkward before.
If you’re migrating: spread it out. Use id on every trigger from the start. Lean on choose:. Keep related logic in package files. Verify each automation against its Node-RED equivalent before deleting the flow.