一键导入
nodered-flow-builder
Step-by-step operational guide for building, editing, testing, and debugging Node-RED flows using MCP tools.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Step-by-step operational guide for building, editing, testing, and debugging Node-RED flows using MCP tools.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Comprehensive rules for positioning and arranging nodes in Node-RED flows. Covers debug node placement, vertical centering around branch points, horizontal spacing standards, comment placement, group spacing, and bounding-box calculations. Use whenever creating or rearranging multi-node flows to ensure visual clarity and avoid overlapping elements.
Reference for @flowfuse/node-red-dashboard (Dashboard 2.0) v1.30.2, the officially recommended Node-RED dashboard. Covers widget catalog with properties, config nodes (ui-base, ui-page, ui-group, ui-theme), wiring patterns, and common dashboard recipes. Docs: https://dashboard.flowfuse.com/
Core vocabulary of Node-RED: nodes, flows (tabs), wires, messages, context, config nodes, design principles, and basic error handling concepts.
Comprehensive reference for the JSONata expression language (v2.2.0). Covers syntax, path navigation, predicates, operators, all built-in function categories (string, numeric, aggregation, boolean, array, object, date/time, higher-order), programming constructs (variables, functions, recursion), the processing model (sequences, flattening), and the JavaScript embedding API. Includes Node-RED-specific usage notes for change, switch, and inject nodes.
Comprehensive reference for the Mustache templating language (v1.3.0). Covers all tag types: variables (escaped and unescaped), sections, inverted sections, comments, partials, blocks, parents, and set delimiters. Documents dotted-name resolution, implicit iterator, lambdas, and the inheritance extension (blocks/parents). Includes Node-RED-specific usage notes for the template node.
Categorized reference of Node-RED built-in core node types, key properties, and a deep-dive into the function node's APIs, return semantics, and async patterns.
| name | nodered-flow-builder |
| description | Step-by-step operational guide for building, editing, testing, and debugging Node-RED flows using MCP tools. |
| tools | ["refresh-staging","deploy","get-staging-status","get-flows","get-config-nodes","get-subflows","get-flow-nodes","get-flow-diagram","get-node-detail","create-flow","update-flow","delete-flow","create-node","update-node","delete-node","create-subflow","update-subflow","delete-subflow","create-subflow-instance","add-nodes-to-group","remove-nodes-from-group","update-group","connect-nodes","disconnect-nodes","export-flow","import-flow","inject-message","read-debug-messages","get-context","search-nodes"] |
Step-by-step operational guide for building, editing, testing, and debugging Node-RED flows via MCP tools. Follow the numbered sequences exactly.
Prerequisites: Read
nodered-fundamentalsfirst for core vocabulary. Usenodered-node-referencefor node type properties.
Before starting ANY workflow (create, edit, delete, import), you MUST sync the staging state with the server. Follow this two-step sequence in order:
refresh-staging()
⚠️ CRITICAL: This is the very first operation before ANY editing session. It discards any stale un-deployed changes and re-fetches the latest flow state from Node-RED. This prevents version mismatch errors (HTTP 409) when deploying. If you skip this step and the Node-RED editor has been used to modify flows, your deploy will fail and ALL your staged changes will be lost.
When to refresh: Always call
refresh-stagingat the start of a new editing session. After deploy, staging is automatically synced — no manual refresh needed.
get-flows()
get-config-nodes()
get-subflows()
This returns the current list of flow tabs, their IDs, labels, node counts, lock status, config nodes, and subflows. Use this information to:
flowIdAfter every deploy, the staging store automatically re-fetches flows from the server — you do NOT need to call refresh-staging or get-flows manually after deploy. The internal state is already synced.
The primary workflow for creating a new flow. Follow these steps in order:
create-flow(label: "My Flow", info: "Description of what this flow does")
Save the returned flowId — you need it for every subsequent step.
create-node(type: "inject", flowId: "<flowId>", properties: { name: "Trigger", payload: "hello", payloadType: "str" }, x: 100, y: 100)
create-node(type: "function", flowId: "<flowId>", properties: { name: "Transform", func: "msg.payload = msg.payload.toUpperCase();\nreturn msg;", outputs: 1 }, x: 300, y: 100)
create-node(type: "debug", flowId: "<flowId>", properties: { name: "Output", complete: "payload", targetType: "msg" }, x: 500, y: 100)
Save all returned nodeId values.
💡 Setting a node description: Add info to the properties object:
create-node(type: "ping", flowId: "<flowId>", properties: { name: "Ping", host: "192.168.1.1", info: "Pings the main server" }, x: 300, y: 100)
The info field corresponds to the Description shown in the Node-RED editor UI. When a user asks to "add a description" or "describe the node", they mean setting info.
💡 Creating comment nodes: Use type: "comment" to annotate flows. The name field is a short label (1-3 words, visible on canvas). Use info for detailed documentation (tooltip on hover):
create-node(type: "comment", flowId: "<flowId>", properties: { name: "My Section", info: "Detailed notes about this section of the flow" }, x: 100, y: 300)
💡 Switch nodes — always set outputs explicitly:
outputs to rules.length. Without it, the editor defaults to 1 visible port even if wires are connected. Also set repair: false and checkall: "false" (string, not boolean).connect-nodes(fromNodeId: "<injectId>", outputPort: 0, toNodeId: "<functionId>")
connect-nodes(fromNodeId: "<functionId>", outputPort: 0, toNodeId: "<debugId>")
Critical: Always wire after creating nodes. An unwired node is isolated and will never receive messages.
get-flow-diagram(flowId: "<flowId>")
Review the Mermaid diagram. Confirm all nodes appear and wires connect as expected.
CRITICAL — Changes are staged, not live! All create-node, connect-nodes, update-node, etc. operations stage changes in a local workspace. They are NOT active until you deploy:
deploy(deployType: "nodes")
Default deploy type is "nodes" (least disruptive — only modified nodes restart). Use "flows" to restart modified flow tabs, or "full" for a complete restart.
Check what's pending before deploying:
get-staging-status()
🔄 Post-deploy sync: The deploy tool automatically refreshes all flows from the server after a successful deploy. The staging store is always in sync with Node-RED after deploy completes — no manual refresh needed.
inject-message(nodeId: "<injectId>")
read-debug-messages(nodeName: "<debugNodeName>", last: 5)
⚠️ Important: You MUST deploy before testing. inject-message will error if there are undeployed changes.
All output ports are 0-indexed. Port 0 is the leftmost output on the node.
┌──────────┐
│ switch │── port 0 (output 1)
│ │── port 1 (output 2)
│ │── port 2 (output 3)
└──────────┘
Example — switch node with 3 routes:
connect-nodes(fromNodeId: "<switchId>", outputPort: 0, toNodeId: "<route1Id>")
connect-nodes(fromNodeId: "<switchId>", outputPort: 1, toNodeId: "<route2Id>")
connect-nodes(fromNodeId: "<switchId>", outputPort: 2, toNodeId: "<route3Id>")
Batch wiring (wire multiple ports in one call):
connect-nodes(fromNodeId: "<switchId>", connections: [
{ outputPort: 0, toNodeId: "<route1Id>" },
{ outputPort: 1, toNodeId: "<route2Id>" },
{ outputPort: 2, toNodeId: "<route3Id>" }
])
⚠️ Never set
wiresinupdate-nodeproperties. Wiring is managed exclusively throughconnect-nodesanddisconnect-nodes.
Node positions on the canvas follow a grid pattern to keep flows readable and avoid overlapping nodes. Understanding node dimensions is critical for calculating proper coordinates.
Based on empirical measurements from Node-RED editor:
| Node Type | Width (px) | Height (px) | Notes |
|---|---|---|---|
| Standard function/debug/inject | ~160 | ~40 | Short names (≤20 chars) |
| Long name function | ~320+ | ~40 | Names >30 chars expand width |
| Config nodes | ~180 | ~40 | Slightly wider than standard |
| Function w/ 1-3 outputs | ~160 | ~40 | Height stays at ~40px |
| Function w/ 4-5 outputs | ~160 | ~60 | Height jumps ~20px at 4+ outputs |
| Higher outputs (6+) | ~160 | ~80+ | Extrapolated; verify empirically |
Key observations:
Important: When placing nodes with many outputs (4+), increase vertical spacing to avoid port overlap with adjacent rows. Use +60px Y delta instead of +40px.
Node height is fixed at ~40px for up to 3 outputs, then increases in ~20px steps for every additional group of 3 outputs:
/**
* Estimate a function node's height based on output count.
* @param {number} outputs - Number of output ports (1-based)
* @returns {number} Estimated height in pixels
*/
function estimateNodeHeight(outputs) {
const baseHeight = 40; // minimum height for 1-3 outputs
const stepHeight = 20; // extra height per group of 3 additional outputs
const groupSize = 3; // outputs per height group
const extra = Math.max(0, Math.floor((outputs - 1) / groupSize));
return baseHeight + extra * stepHeight;
}
Quick reference:
| Outputs | Height | ΔY to next row |
|---|---|---|
| 1–3 | 40px | +40px |
| 4–6 | 60px | +60px |
| 7–9 | 80px | +80px |
| 10–12 | 100px | +100px |
// When placing the next row, use the taller node's height:
const nextY = currentY + estimateNodeHeight(currentOutputs);
📍 CRITICAL: First Node Position
The first node in ANY flow MUST start at coordinates (x=120, y=80). This is the absolute top-left starting point — nothing should be placed above y=80 or to the left of x=120. (When using groups with comments, the first node may shift down by ~30-50px to accommodate the group header.)
| Convention | X | Y | Use Case |
|---|---|---|---|
| FIRST NODE (mandatory) | 120 | 80 | Absolute starting point |
| Inline next node | +180 | same Y | Sequential processing nodes (170-190px range) |
| Debug adjacent | +10 | ±40 | Debug alongside predecessor, above or below |
| Branch down (1-3 outs) | same X | +60 | Alternative path from switch |
| Branch down (4+ outs) | same X | +80 | Extra space for taller nodes |
| Branch row | +180 | +60 | Alternative row for complex branches |
─────────────────────────────────────────────────────────┐
│ CANVAS BOUNDARY │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [120,80] ← FIRST NODE STARTS HERE │ │
│ │ ↓ │ │
│ │ Row 1: [inject]──(+200)──[function]──(+200)──[debug] │
│ │ │ │ │
│ │ Row 2: └─(+200, +60)──[debug:error] │
│ │ │ │
│ │ ⚠️ Nothing above y=80 or left of x=120 │ │
│ └─────────────────────────────────────────────────────┘ │
─────────────────────────────────────────────────────────┘
Why (120, 80)?
📐 For advanced layout rules (debug placement, branch-point centering, group spacing, comment positioning, bounding-box calculations), see
nodered-flow-layout.
When placing nodes with long names, calculate width dynamically:
// Estimate node width based on name length
function estimateNodeWidth(nodeName) {
const baseWidth = 160; // minimum width for short names
const charWidth = 8; // approximate pixels per character
const padding = 40; // icon + ports padding
const textWidth = nodeName.length * charWidth;
return Math.max(baseWidth, textWidth + padding);
}
// Example calculations:
estimateNodeWidth("function 1") // → 160px
estimateNodeWidth("This is a very long function name") // → ~320px
Placement strategy for long-name nodes:
nextX = currentX + estimatedWidth + 40create-node, update-node, delete-node, connect-nodes, and disconnect-nodes all refuse to modify nodes in a locked flow. Check flow lock status with get-flows.From actual Node-RED deployment analysis:
Flow layout with 8 function nodes:
Row 1 (y=80):
• function 1: x=120, y=80
• function 2: x=240, y=80 ← Δx = 120px (standard spacing)
Row 2 (y=120):
• function 3: x=120, y=120
• function 4: x=240, y=120 ← Δx = 120px
Row 3 (y=160):
• function 5: x=120, y=160
• function 6: x=240, y=160 ← Δx = 120px
Row 4 (y=200):
• Long name node: x=200, y=200
• function 7: x=400, y=200 ← Δx = 200px (extra space for long name)
Takeaway: Standard nodes use 120px spacing, but long-name nodes require 200px+ to prevent overlap.
Empirical test with function nodes (1-5 outputs) placed at y=80 with debug reference nodes below:
Test flow: "Node Dimensions Test"
Row 1 (y=80): [1 output] [2 outputs] [3 outputs] [4 outputs] [5 outputs]
x=120 x=280 x=440 x=600 x=760
Row 2 (debug): [Debug 1] [Debug 2] [Debug 3] [Debug 4] [Debug 5]
y=120 y=120 y=120 y=140 y=140
Results:
| Outputs | Function Y | Debug Y | ΔY (height) | Node Height |
|---|---|---|---|---|
| 1 | 80 | 120 | 40px | ~40px |
| 2 | 80 | 120 | 40px | ~40px |
| 3 | 80 | 120 | 40px | ~40px |
| 4 | 80 | 140 | 60px | ~60px |
| 5 | 80 | 140 | 60px | ~60px |
Key findings:
Placement rule for multi-output nodes:
// Use the estimateNodeHeight() formula from the Height section above
const nextY = currentY + estimateNodeHeight(currentOutputs);
Use import-flow when you have a pre-built flow JSON (from export-flow or an example file):
regenerate (default, safest): Remaps all IDs to new UUIDs. Always creates a duplicate — safe, no overwrites.overwrite: Replaces nodes with matching IDs. Use to update an existing flow with new configuration.import-flow(flowJson: "<json>", conflictStrategy: "regenerate")
import-flow(flowJson: "<json>", targetFlowId: "<existingFlowId>")
This injects all non-tab nodes into the target tab. The tab node in the JSON is discarded.
When to prefer import over build-from-scratch:
export-flow)get-node-detail(nodeId: "<nodeId>")
Review the returned node object to understand current configuration.
update-node(nodeId: "<nodeId>", properties: { name: "New Name", func: "return msg;" })
Only include fields you want to change. Omitted fields are preserved.
💡 Adding or updating a description: Set the info property — the Description field in the Node-RED editor UI:
update-node(nodeId: "<nodeId>", properties: { info: "This node filters messages with temperature > 30°C" })
get-node-detail(nodeId: "<nodeId>")
Confirm the changes took effect.
🛑 Never include wires, id, or z in properties. The tool silently ignores them.
delete-node(nodeId: "<nodeId>")
Node-RED automatically cleans up dangling wire references on deploy. The previousState in the response contains the full node object — save it if you need to undo.
delete-flow(flowId: "<flowId>")
Returns previousState with ALL nodes in the tab. Refuses to delete locked flows.
Undo a deletion: Use import-flow with the previousState from the delete response and strategy "regenerate".
When building a new flow, validate each node's output before adding the next node. This catches format errors, type mismatches, and missing properties early — before you've built 10 nodes on top of a broken foundation.
After EVERY processing node you add, wire it to a
debugnode, deploy, inject, and verify the output. Only then add the next node.
// 1. Create first processing node + debug after it
create-node(type: "inject", flowId: "<fid>", properties: { name: "Start", payload: "test", payloadType: "str" }, x: 100, y: 100)
create-node(type: "debug", flowId: "<fid>", properties: { name: "Debug1", complete: "true", targetType: "full" }, x: 300, y: 100)
connect-nodes(fromNodeId: "<injectId>", toNodeId: "<debug1Id>")
// 2. Deploy — changes are NOT live until you do this
deploy()
// 3. Inject and check output format
inject-message(nodeId: "<injectId>")
read-debug-messages(last: 1)
// 4. If output looks good, remove the debug node (or keep it) and add next node
delete-node(nodeId: "<debug1Id>")
create-node(type: "function", flowId: "<fid>", properties: { name: "Process", func: "msg.payload = msg.payload * 2;\nreturn msg;", outputs: 1 }, x: 300, y: 100)
connect-nodes(fromNodeId: "<injectId>", toNodeId: "<functionId>")
create-node(type: "debug", flowId: "<fid>", properties: { name: "Debug2", complete: "true", targetType: "full" }, x: 500, y: 100)
connect-nodes(fromNodeId: "<functionId>", toNodeId: "<debug2Id>")
// 5. Deploy again and verify
deploy()
inject-message(nodeId: "<injectId>")
read-debug-messages(last: 1)
// 6. Repeat until flow is complete
Why this matters:
function node might return undefined or the wrong type — you catch it immediatelychange node might not set the right property — you see it in debug outputhttp request might return unexpected JSON structure — you can adjust before building downstream nodes⚠️ NEVER FORGET TO DEPLOY. Every edit is staged. If inject-message errors with "undeployed changes", call deploy first.
Follow this numbered sequence whenever a flow doesn't behave as expected:
get-flow-nodes(flowId: "<flowId>", nodeType: "debug")
Check that each debug node has active: true. If not:
update-node(nodeId: "<debugId>", properties: { active: true })
Note the current time (e.g., Date.now()) before triggering. This gives you an after filter anchor for read-debug-messages.
inject-message(nodeId: "<injectId>")
Use nodeId (not name) when possible — it's unambiguous.
Node-RED processes messages asynchronously. Wait ~500ms-1s, then:
read-debug-messages(nodeName: "<debugNodeName>", after: <timestamp>, limit: 10)
The after filter ensures you only see messages from this test run.
Check payload, topic, and any custom properties. Is the value what you expected? Is the type correct?
Make the needed changes (via update-node, function func edit, or wiring fix), then repeat from step 2.
| Property | Values | Description |
|---|---|---|
active | true/false | Enable/disable output. Disabled nodes collect nothing. |
complete | "true" (full msg), "false" (payload only), or property path like "payload.temperature" | What to display |
targetType | "full" or "msg" | Use "msg" when complete is a property path |
console | true/false | Also log to Node-RED server console |
tosidebar | true/false | Display in debug sidebar (required for read-debug-messages) |
Recommended debug node for flow validation:
{ active: true, complete: "true", targetType: "full", tosidebar: true, console: false }
This captures the entire message — you can inspect all properties during debugging.
Trigger an inject node to start a message through the flow:
// By nodeId (preferred — always unique)
inject-message(nodeId: "<injectId>")
// By name (convenient, but fails if duplicate names exist)
inject-message(name: "Trigger")
Important notes:
"inject". Other node types will error.flowId to scope name-based lookups: inject-message(name: "Trigger", flowId: "<flowId>").flowId, the tool returns an error listing the matching IDs — pick one.| Filter | Type | Use Case |
|---|---|---|
nodeId | string | Pinpoint output from a specific debug node |
nodeName | string | Match by name (case-insensitive substring) |
keyword | string | Search within stringified message payload |
after | timestamp (ms) | Only messages after this time (use Date.now() before trigger) |
before | timestamp (ms) | Only messages before this time |
last | number | Return the N most recent matching messages |
limit | number | Return the first N matching messages (default 50) |
last and limit are mutually exclusive — use one or the other, not both.
Typical debug read:
read-debug-messages(nodeName: "Output", after: 1717526400000, last: 5)
inject-message(nodeId: "<injectId>")
read-debug-messages(nodeName: "<firstDebugName>", last: 1)
If no message appears: check wiring (especially connect-nodes), verify the inject node fires, and confirm the debug node is on the same tab.
Place a temporary debug node after the suspect node:
create-node(type: "debug", flowId: "<flowId>", properties: { name: "Checkpoint", complete: "true", targetType: "full" }, x: <afterNodeX>, y: <sameY>)
connect-nodes(fromNodeId: "<suspectNodeId>", outputPort: 0, toNodeId: "<checkpointId>")
Trigger, read, diagnose, then delete the checkpoint node.
get-context(scope: "flow", id: "<flowId>", key: "myKey")
If null: your function node's context.set() or flow.set() isn't executing. Check that the function node is actually receiving messages.
get-node-detail(nodeId: "<functionId>")
Review the func property. Common issues: missing return msg;, syntax errors, wrong property names.
After building or modifying a flow, always verify:
get-flow-diagram(flowId: "<flowId>")
Confirm the Mermaid diagram shows correct wiring — every node connected as intended, no dangling branches.
get-flow-nodes(flowId: "<flowId>")
Verify node count, types, and names. Look for:
"name": "") — give them descriptive namescreate-node calls succeed?Look at the wires arrays in get-flow-nodes output. Each source node should have its expected targets listed.
Inject a message and read debug output. Confirm the payload transforms as expected at each step.
| Mistake | Symptom | Fix |
|---|---|---|
| Not wiring after create | New node never receives messages | Always call connect-nodes after create-node. Use the INSERT pattern: disconnect A→B, connect A→new, connect new→B |
| Wrong port index | Message goes to wrong output | Ports are 0-indexed. Switch output 1 = port 0, output 2 = port 1, output 3 = port 2 |
| Setting wires in update-node | Wires silently ignored | Use connect-nodes/disconnect-nodes exclusively for wiring |
| Overlapping coordinates | Nodes visually stack in editor | Use the grid: first at (100,100), inline +200 X, branch +100 Y |
Forgetting return msg; in function | Message stops at function node | Every function must return msg; (or return [msg1, msg2] for multi-output). return null; explicitly stops the message |
node.send() without return | Unpredictable behavior, duplicate messages | After calling node.send() in async code, ALWAYS return; immediately |
Debug node with active: false | No output visible | Set active: true on all debug nodes used for testing |
| Reading debug too soon | read-debug-messages returns empty | Wait ~500ms after inject-message before reading; Node-RED processes asynchronously |
| Deleting without backup | Cannot undo accidental deletion | Use export-flow before destructive operations; delete-* responses include previousState for recovery |
| Modifying locked flow | Tool returns error | Check get-flows for locked: true; locked flows must be unlocked in the editor first |