Node-RED for Home Assistant
Build Node-RED flows using node-red-contrib-home-assistant-websocket nodes.
First Step: Clarify Platform
If the user's request does NOT explicitly mention "Node-RED" or "flow", ASK:
"Do you want this as:
- Node-RED flow (visual, drag-drop, importable JSON)
- Home Assistant YAML (automations.yaml, scripts.yaml)
- ESPHome config (device firmware for ESP32/ESP8266)"
NEVER assume Node-RED. A request like "make a motion light" could be any of these. Only proceed with this skill if user confirms Node-RED.
Critical: Node Names Have Changed
STOP. If you're about to use any of these node types, you're using outdated names:
| WRONG (Old) | CORRECT (Current) |
|-------------|-------------------|
| server-state-changed | trigger-state or events:state |
| poll-state | poll-state (unchanged but check config) |
| call-service | api-call-service |
Trigger Node Configuration (Current API)
{
"type": "trigger-state",
"entityId": "binary_sensor.motion",
"entityIdType": "exact",
"constraints": [
{
"targetType": "this_entity",
"propertyType": "current_state",
"comparatorType": "is",
"comparatorValue": "on"
}
],
"outputs": 2
}
entityIdType options: exact, substring, regex
There is NO list type. To monitor multiple entities, use regex:
"entityId": "binary_sensor\\.motion_(1|2|3)",
"entityIdType": "regex"
Service Call Configuration (Current API)
{
"type": "api-call-service",
"domain": "light",
"service": "turn_on",
"entityId": ["light.living_room"],
"data": "",
"dataType": "json"
}
Or dynamic via msg:
{
"type": "api-call-service",
"domain": "",
"service": "",
"data": "",
"dataType": "msg"
}
With function node before:
msg.payload = {
action: "light.turn_on",
target: { entity_id: ["light.living_room"] },
data: { brightness_pct: 80 }
};
return msg;
Current State Node - Single Entity Only
api-current-state queries ONE entity, not patterns.
{
"type": "api-current-state",
"entity_id": "person.john"
}
To check multiple entities, use function node:
const ha = global.get("homeassistant").homeAssistant.states;
const people = Object.keys(ha)
.filter(id => id.startsWith("person."))
.filter(id => ha[id].state !== "home");
msg.awayPeople = people;
return msg;
Entity Nodes Require Extra Integration
The following nodes require hass-node-red integration (separate from the websocket nodes):
ha-entity(sensor, binary_sensor, switch, etc.)- Entity config nodes
Always mention this prerequisite when using entity nodes.
Timer Pattern (Motion Light)
Use single trigger node with extend: true:
{
"type": "trigger",
"op1type": "nul",
"op2": "timeout",
"op2type": "str",
"duration": "5",
"extend": true,
"units": "min"
}
Do NOT create separate reset/start timer nodes. The extend property handles this.
Flow JSON Guidelines
- Never include server config node - User configures separately
- Leave
serverfield empty - User selects their server - Use placeholder entity IDs - Document what to change
- Add comment node - Explain required configuration
Function Node: External Libraries
WRONG: Using global.get('axios') or similar for HTTP requests.
This requires manual configuration in settings.js:
// settings.js - requires Node-RED restart
functionGlobalContext: {
axios: require('axios')
}
CORRECT: Use the built-in http request node instead:
{
"type": "http request",
"method": "GET",
"url": "https://api.example.com/data",
"ret": "obj"
}
When you MUST use function node for HTTP:
- Complex request logic that can't be handled by http request node
- Requires settings.js configuration (warn user!)
- Use
node.send()andnode.done()for async:
// Async pattern in function node
const axios = global.get('axios'); // Requires settings.js config!
async function fetchData() {
try {
const response = await axios.get(msg.url);
msg.payload = response.data;
node.send(msg);
} catch (error) {
node.error(error.message, msg);
}
node.done();
}
fetchData();
return null; // Prevent sync output
Context Storage
Three scopes available:
| Scope | Syntax | Shared With |
|-------|--------|-------------|
| Node | context.get/set() | Only this node |
| Flow | flow.get/set() | All nodes in tab |
| Global | global.get/set() | All flows |
// Store state
flow.set('machineState', 'washing');
flow.set('history', historyArray);
// Retrieve
const state = flow.get('machineState') || 'idle';
For persistence across restarts, configure in settings.js:
contextStorage: {
default: { module: "localfilesystem" }
}
Error Handling Pattern
Use catch node scoped to specific nodes:
{
"type": "catch",
"scope": ["call_service_node_id"],
"uncaught": false
}
Error info available in msg.error:
msg.error.message- Error textmsg.error.source.id- Node that threw errormsg.error.source.type- Node type
Retry pattern: Use delay node with delayv type to read delay from msg.delay.
Common Mistakes Table
| Mistake | Reality |
|---------|---------|
| Using server-state-changed | Node renamed to trigger-state |
| entityIdType: "list" | No such type. Use regex for multiple entities |
| api-current-state with pattern | Only accepts single entity_id |
| Using ha-entity without warning | Requires separate hass-node-red integration |
| Complex timer reset logic | Use extend: true on trigger node |
| dataType: "jsonata" for service data | Use msg when passing dynamic payload |
| global.get('axios') for HTTP | Use http request node, or warn about settings.js |
| return msg in async function | Use node.send(msg) + node.done() + return null |
Pre-Output Checklist
Before outputting flow JSON:
- [ ] Using current node type names?
- [ ] Entity filtering uses valid type (exact/substring/regex)?
- [ ] Service call has domain/service OR uses msg payload correctly?
- [ ] Single entity nodes don't assume pattern matching?
- [ ] Entity nodes mention hass-node-red requirement?
- [ ] Server field left empty for user configuration?