[{"data":1,"prerenderedAt":453},["ShallowReactive",2],{"agmd-node-red-good-form":3,"application-guide-nav":345},{"id":4,"title":5,"blurb":6,"body":7,"description":20,"extension":335,"guide":336,"meta":337,"navOrder":338,"navTitle":5,"navigation":339,"parent":340,"path":341,"seo":342,"slug":13,"stem":343,"__hash__":344},"applicationGuideDoc\u002Fapplication-guide\u002Fnode-red\u002Fgood-form.md","Good form","A clean flow isn't luck — it's a handful of habits. Wire for reading, lay it out on a grid, decouple UI from logic, catch errors where you can see them, and keep data on a stable contract. Follow these and a flow stays readable, reusable, and out of spaghetti.",{"type":8,"value":9,"toc":331},"minimark",[10,14,21,33,324],[11,12,5],"h1",{"id":13},"good-form",[15,16,17],"p",{},[18,19,20],"strong",{},"Good form — how not to make spaghetti",[15,22,23,24,32],{},"A clean flow isn't luck — it's a handful of habits. The golden rule: ",[18,25,26,27,31],{},"shared things are ",[28,29,30],"em",{},"called"," rather than funneled into, and every beginning is its own left-justified path."," Everything below follows from that.",[34,35,36,109,154,218,263],"guide-tabs",{},[37,38,40,46,52,68,73],"guide-tab",{"label":39},"Wire for reading",[15,41,42,45],{},[18,43,44],{},"Call shared things; don't funnel into them"," — many paths wiring into one shared node is the core spaghetti anti-pattern.",[47,48],"flow-diagram",{":edges":49,":nodes":50,"align":51},"[\"a>call\",\"b>call\",\"c>call\",{\"from\":\"call\",\"to\":\"svc\",\"dir\":\"both\",\"label\":\"result back\"}]","[{\"id\":\"a\",\"label\":\"path A\",\"accent\":\"indigo\",\"col\":1,\"row\":1},{\"id\":\"b\",\"label\":\"path B\",\"accent\":\"indigo\",\"col\":1,\"row\":2},{\"id\":\"c\",\"label\":\"path C\",\"accent\":\"indigo\",\"col\":1,\"row\":3},{\"id\":\"call\",\"label\":\"link call\",\"sub\":\"the shared service\",\"accent\":\"teal\",\"col\":2,\"row\":2},{\"id\":\"svc\",\"label\":\"SQL \u002F broker \u002F model\",\"sub\":\"one dependency\",\"accent\":\"green\",\"col\":3,\"row\":2}]","left",[15,53,54,55,58,59,62,63,67],{},"When many paths share a dependency — a DB pool, a broker, a model call — don't wire them all ",[28,56,57],{},"into"," one node (a funnel). Expose it once as a service each path ",[18,60,61],{},"calls"," with a ",[64,65,66],"code",{},"link call","; the result returns and per-path handling happens after. The shared node keeps exactly one wire in and stays reusable.",[15,69,70],{},[18,71,72],{},"The rules",[74,75,76,91,97,103],"ul",{},[77,78,79,82,83,86,87,90],"li",{},[18,80,81],{},"No funnels into a shared dependency"," — when many paths need the same service, they should ",[28,84,85],{},"call"," it, not all wire into it. (Aggregators like a ",[64,88,89],{},"join"," node that legitimately gather many inputs are the exception — that's their job, not a funnel.)",[77,92,93,96],{},[18,94,95],{},"Each beginning is its own path"," — one straight, left-justified path per entry; don't merge them through a shared front door.",[77,98,99,102],{},[18,100,101],{},"Flow left → right"," — beginning → prep → service call → format → sink. A backward (right-to-left) wire reads as tangle.",[77,104,105,108],{},[18,106,107],{},"A crossing is a missing link node"," — if two wires cross, row-align the column with its targets or bridge it with a link node. Same for any wire longer than the canvas — a link node reads cleaner.",[37,110,112,118,150],{"label":111},"Lay it out",[15,113,114,117],{},[18,115,116],{},"One column per role, everything on the grid"," — layout is what makes a flow scannable at a glance.",[74,119,120,126,132,138,144],{},[77,121,122,125],{},[18,123,124],{},"One entry column"," — every beginning (inject, http in, mqtt in) shares one left edge. That column is the visual anchor.",[77,127,128,131],{},[18,129,130],{},"Columns by role"," — beginning → prep → service call → format → sink, left to right, each role in its own column.",[77,133,134,137],{},[18,135,136],{},"On the grid, no overlaps"," — snap to the 20px grid, leave a gap between neighbours, and never let two nodes (or two groups) overlap.",[77,139,140,143],{},[18,141,142],{},"Align and pad groups"," — left-justify group boxes to a common column, keep members inside the box with a little padding, and drop empty groups.",[77,145,146,149],{},[18,147,148],{},"Comments are short labels"," — a couple of words on the canvas; put the detail in the comment's info field, not a paragraph that runs off-page.",[47,151],{":edges":152,":nodes":153,"align":51},"[\"begin>prep\",\"prep>call\",\"call>fmt\",\"fmt>sink\"]","[{\"id\":\"begin\",\"label\":\"beginning\",\"sub\":\"one entry column\",\"accent\":\"indigo\",\"col\":1,\"row\":1},{\"id\":\"prep\",\"label\":\"prep\",\"col\":2,\"row\":1},{\"id\":\"call\",\"label\":\"service call\",\"accent\":\"teal\",\"col\":3,\"row\":1},{\"id\":\"fmt\",\"label\":\"format\",\"col\":4,\"row\":1},{\"id\":\"sink\",\"label\":\"sink\",\"accent\":\"indigo\",\"col\":5,\"row\":1}]",[37,155,157,163,168,171],{"label":156},"Decouple UI from logic",[15,158,159,162],{},[18,160,161],{},"Widgets are an API"," — the backend sends a display-ready view-model; the frontend emits intent. Treat the dashboard-to-logic boundary like a client and a server.",[47,164],{":edges":165,":nodes":166,":legend":167},"[{\"from\":\"fe\",\"to\":\"be\",\"label\":\"intent · { action, payload }\"},{\"from\":\"be\",\"to\":\"fe\",\"label\":\"view-model · display-ready\",\"dashed\":true},\"be>db\"]","[{\"id\":\"fe\",\"label\":\"widget\",\"sub\":\"renders + emits\",\"accent\":\"indigo\"},{\"id\":\"be\",\"label\":\"flow logic\",\"sub\":\"holds the truth\",\"accent\":\"indigo\"},{\"id\":\"db\",\"label\":\"SQL database\",\"sub\":\"records\",\"accent\":\"green\"}]","[{\"line\":\"neutral\",\"label\":\"intent · frontend → backend\"},{\"line\":\"neutral\",\"dashed\":true,\"label\":\"view-model · backend → frontend\"}]",[15,169,170],{},"The frontend renders state and emits intent; the backend holds the truth. When you build payloads inside templates or cram logic next to a widget, every change touches both.",[74,172,173,179,185,195,212],{},[77,174,175,178],{},[18,176,177],{},"Reads"," — the backend sends a finished, display-ready view-model. Templates bind and display; they never fetch, transform, or decide.",[77,180,181,184],{},[18,182,183],{},"Writes"," — the widget emits one consistent intent message: an action plus a payload. The backend decides what it means.",[77,186,187,190,191,194],{},[18,188,189],{},"One place for state"," — hold state in one shared ",[64,192,193],{},"global"," object the widgets read. The wire carries events, not fat objects, and never a live subscription per widget.",[77,196,197,203,204,207,208,211],{},[18,198,199,202],{},[64,200,201],{},"ui-template"," is the escape hatch"," — reach for it for the one custom widget, never to build the whole UI as one block. Let it auto-size (",[64,205,206],{},"height=\"0\"",") so content isn't clipped, and remember tables ",[18,209,210],{},"replace, not append",".",[77,213,214,217],{},[18,215,216],{},"Give every page an on-load trigger"," — dashboard widgets emit only on real interaction, so a page that waits for a click to populate opens empty. Fire the load path on page-show.",[37,219,221,227,232,235],{"label":220},"Catch errors visibly",[15,222,223,226],{},[18,224,225],{},"Every work path has a catch"," — otherwise errors drop silently and you're debugging blind.",[47,228],{":edges":229,":nodes":230,":legend":231},"[{\"from\":\"in\",\"to\":\"work\"},{\"from\":\"work\",\"to\":\"sink\",\"label\":\"ok\"},{\"from\":\"work\",\"to\":\"catch\",\"label\":\"in scope\",\"accent\":\"red\",\"dashed\":true},{\"from\":\"catch\",\"to\":\"errpath\",\"accent\":\"red\",\"dashed\":true}]","[{\"id\":\"in\",\"label\":\"in\",\"col\":1,\"row\":1},{\"id\":\"work\",\"label\":\"work\",\"sub\":\"may throw\",\"col\":2,\"row\":1},{\"id\":\"sink\",\"label\":\"sink\",\"sub\":\"on success\",\"col\":3,\"row\":1},{\"id\":\"catch\",\"label\":\"Catch\",\"sub\":\"scoped to the work\",\"accent\":\"red\",\"col\":2,\"row\":2},{\"id\":\"errpath\",\"label\":\"log · notify · return\",\"sub\":\"the error path\",\"accent\":\"red\",\"col\":3,\"row\":2}]","[{\"line\":\"neutral\",\"label\":\"success\"},{\"line\":\"red\",\"dashed\":true,\"label\":\"Catch scope · not a wire\"}]",[15,233,234],{},"Anything that talks to the outside world — an HTTP call, a DB write, a broker publish, a model call — will fail sometimes. Route those failures somewhere you control. (A Catch node has no input wire — it registers to catch errors from every node in its scope automatically; the dashed line marks that scope, not a connection you draw.)",[74,236,237,243,249],{},[77,238,239,242],{},[18,240,241],{},"No work without a catch"," — a tab with function \u002F request \u002F DB \u002F link-call \u002F AI nodes and no Catch node drops its errors silently.",[77,244,245,248],{},[18,246,247],{},"Scope the catch to cover the path"," — make sure every reachable work node is in the catch's scope, or the ones outside it throw where nothing is listening.",[77,250,251,254,255,258,259,262],{},[18,252,253],{},"In a shared service, format and return the error"," — return it via ",[64,256,257],{},"link out"," in return mode so the caller sees ",[64,260,261],{},"msg.error"," and a bad call never hangs.",[37,264,266,272],{"label":265},"Keep data on a contract",[15,267,268,271],{},[18,269,270],{},"Swap a source by keeping the message shape"," — good seams mean a data change touches one node, not the whole flow.",[74,273,274,288,308,314],{},[77,275,276,279,280,283,284,287],{},[18,277,278],{},"Stable msg contract"," — a query returns rows on the same property; the broker path is ",[64,281,282],{},"msg.topic"," + ",[64,285,286],{},"msg.payload",". Keep the shape and only one node changes when you swap the source behind it.",[77,289,290,293,294,297,298,301,302,304,305,307],{},[18,291,292],{},"SQL goes on the property your node reads"," — the Postgres query node reads ",[64,295,296],{},"msg.query"," (with ",[64,299,300],{},"msg.params"," for parameters); the mysql \u002F sqlite nodes read ",[64,303,282],{},". Either way it's never ",[64,306,286],{}," — put SQL on the wrong property and the query silently runs empty.",[77,309,310,313],{},[18,311,312],{},"Parameterize"," — use parameterized queries and quote case-sensitive identifiers; don't string-build SQL into the payload.",[77,315,316,319,320,323],{},[18,317,318],{},"Preserve the message through the chain"," — return context (a callback, a link-call return) has to survive every hop, so keep functions and query nodes passing ",[64,321,322],{},"msg"," through.",[325,326,328],"callout",{"icon":327},"i-lucide-check",[15,329,330],{},"Follow these and a flow reads cleanly — beginnings in one column, shared things called not funneled, UI and logic on their own sides of a contract, errors on a path you can see, and data on a stable shape. That's good form.",{"title":332,"searchDepth":333,"depth":333,"links":334},"",4,[],"md","node-red",{},3.3,true,"patterns","\u002Fapplication-guide\u002Fnode-red\u002Fgood-form",{"title":5,"description":20},"application-guide\u002Fnode-red\u002Fgood-form","NH1VvRfsrjBsUrxsW45WUkBCDP3a7GYj7imK-nFMFv0",[346,354,357,363,366,372,376,382,387,393,398,400,405,410,416,422,428,433,438,443,447],{"guide":347,"slug":348,"title":349,"navOrder":350,"parent":351,"blurb":352,"path":353},"flowfuse","overview","Overview",1,null,"The map of the FlowFuse guide — apps, architectures, and a worked example.","\u002Fapplication-guide\u002Fflowfuse\u002Foverview\u002F",{"guide":336,"slug":348,"title":349,"navOrder":350,"parent":351,"blurb":355,"path":356},"The map of the Node-RED guide — the pattern families that turn an app into a clean flow.","\u002Fapplication-guide\u002Fnode-red\u002Foverview\u002F",{"guide":347,"slug":358,"title":359,"navOrder":360,"parent":351,"blurb":361,"path":362},"foundations","Foundations",2,"The foundation to build on: what FlowFuse is, its core pieces, and how code is shared across teams.","\u002Fapplication-guide\u002Fflowfuse\u002Ffoundations\u002F",{"guide":336,"slug":358,"title":359,"navOrder":360,"parent":351,"blurb":364,"path":365},"The handful of concepts you need to build with Node-RED, and how they fit together.","\u002Fapplication-guide\u002Fnode-red\u002Ffoundations\u002F",{"guide":347,"slug":367,"title":368,"navOrder":369,"parent":351,"blurb":370,"path":371},"app-delivery-methods","App delivery methods",3,"Two different units of code, delivered two ways. Ship the whole app — a complete, versioned project promoted through environments — or publish one reusable piece — a package the whole team installs and upgrades in one place. Pick by what you're shipping: the app, or a part of it.","\u002Fapplication-guide\u002Fflowfuse\u002Fapp-delivery-methods\u002F",{"guide":336,"slug":340,"title":373,"navOrder":369,"parent":351,"blurb":374,"path":375},"Patterns","The moves that turn an architecture into a clean, reusable flow — find the seams and reuse well, then handle data on the right paths.","\u002Fapplication-guide\u002Fnode-red\u002Fpatterns\u002F",{"guide":347,"slug":377,"title":378,"navOrder":379,"parent":367,"blurb":380,"path":381},"hardware-apps","Hardware apps",3.1,"The three shapes a FlowFuse app takes when it runs on a device. Pick by how much varies per site: nothing (Packaged App), a few settings (Configurable App), or you assemble it yourself (Edge Building Block).","\u002Fapplication-guide\u002Fflowfuse\u002Fhardware-apps\u002F",{"guide":336,"slug":383,"title":384,"navOrder":379,"parent":340,"blurb":385,"path":386},"design-patterns","Design patterns","The structural choices you select for a flow: find the seams it breaks into, then reuse each piece at the lightest level that solves it — link in\u002Fout, link call, subflow, or packaged node.","\u002Fapplication-guide\u002Fnode-red\u002Fdesign-patterns\u002F",{"guide":347,"slug":388,"title":389,"navOrder":390,"parent":367,"blurb":391,"path":392},"software-apps","Software apps",3.2,"The three shapes a FlowFuse app takes when it runs on the platform. Pick by what it needs: a headless job (Packaged App), a user-facing app driven by data (Data-Driven App), or a reusable piece other apps embed (Shared Building Block).","\u002Fapplication-guide\u002Fflowfuse\u002Fsoftware-apps\u002F",{"guide":336,"slug":394,"title":395,"navOrder":390,"parent":340,"blurb":396,"path":397},"handling-data","Handling data","Classify each signal by shape, purpose and direction, then pick the methods it needs — separate the paths, pace the flow, hold state in context, and manage config. The methods you select to move a flow's data.","\u002Fapplication-guide\u002Fnode-red\u002Fhandling-data\u002F",{"guide":336,"slug":13,"title":5,"navOrder":338,"parent":340,"blurb":6,"path":399},"\u002Fapplication-guide\u002Fnode-red\u002Fgood-form\u002F",{"guide":347,"slug":401,"title":402,"navOrder":333,"parent":351,"blurb":403,"path":404},"data-plane","Data plane","Before you pick where things run, decide how data is handled. Two stores come built into every FlowFuse server install — the Team Broker and relational Tables — exposed to every instance with nothing extra to stand up. Everything else you bring your own: run it (a time-series DB, an existing database, a model) and expose it to the fleet over Project Link, no inbound ports. This is the data plane the architectures on the next pages all sit on.","\u002Fapplication-guide\u002Fflowfuse\u002Fdata-plane\u002F",{"guide":336,"slug":406,"title":407,"navOrder":333,"parent":351,"blurb":408,"path":409},"worked-examples","Worked examples","Turn an app concept into a Node-RED flow — or a few — leaning on the design patterns and data handling. The method, then the OEE apps end to end.","\u002Fapplication-guide\u002Fnode-red\u002Fworked-examples\u002F",{"guide":336,"slug":411,"title":412,"navOrder":413,"parent":406,"blurb":414,"path":415},"oee-edge-aggregator","OEE - Edge Aggregator",4.1,"The edge app from the OEE use case as a Node-RED flow — a straight-line flow packaged as a subflow and configured per line (its PLC tags, via a config UI and a get-config node), with the data treated as a stream and its counts held in context.","\u002Fapplication-guide\u002Fnode-red\u002Foee-edge-aggregator\u002F",{"guide":336,"slug":417,"title":418,"navOrder":419,"parent":406,"blurb":420,"path":421},"oee-central-dashboard","OEE - Central Dashboard",4.2,"The cloud app from the OEE use case as a Node-RED flow — one link out fanning to two link ins on separate tabs (dashboard and batched history), so the live and history paths stay separate and easy to read.","\u002Fapplication-guide\u002Fnode-red\u002Foee-central-dashboard\u002F",{"guide":347,"slug":423,"title":424,"navOrder":425,"parent":351,"blurb":426,"path":427},"architectures","Architectures",5,"Every FlowFuse deployment is the same building blocks arranged for where it runs — pick the world you're designing for.","\u002Fapplication-guide\u002Fflowfuse\u002Farchitectures\u002F",{"guide":347,"slug":429,"title":430,"navOrder":431,"parent":423,"blurb":351,"path":432},"it-architectures","IT architectures",5.1,"\u002Fapplication-guide\u002Fflowfuse\u002Fit-architectures\u002F",{"guide":347,"slug":434,"title":435,"navOrder":436,"parent":423,"blurb":351,"path":437},"ot-architectures","OT architectures",5.2,"\u002Fapplication-guide\u002Fflowfuse\u002Fot-architectures\u002F",{"guide":347,"slug":439,"title":440,"navOrder":441,"parent":423,"blurb":351,"path":442},"iiot-architectures","IIoT architectures",5.3,"\u002Fapplication-guide\u002Fflowfuse\u002Fiiot-architectures\u002F",{"guide":347,"slug":406,"title":407,"navOrder":444,"parent":351,"blurb":445,"path":446},6,"Start from a use case, break it into apps, and draw the architecture that ties them together — the same method a FlowFuse Proof of Value runs.","\u002Fapplication-guide\u002Fflowfuse\u002Fworked-examples\u002F",{"guide":347,"slug":448,"title":449,"navOrder":450,"parent":406,"blurb":451,"path":452},"worked-example","OEE, end to end",6.1,"One use case — OEE across three lines — broken into two apps and two shared services, then drawn out end to end.","\u002Fapplication-guide\u002Fflowfuse\u002Fworked-example\u002F",1787843652734]