# Enabling Soft-Launched Features for Specific Teams During a soft launch campaign, new features are gated behind PostHog feature flags and enabled on a per-team basis. This guide describes the process for enabling a feature flag for a specific team on FlowFuse Cloud. ## Process ### 1. Change Request A change request is created in the **CloudProject** repository. The request must include: - The **internal team ID** of the team that needs the feature enabled - The **feature** that needs enabling ### 2. Identify the Feature Flag Key The developer or admin handling the request should know which PostHog feature flag key gates the requested feature. If unsure, ask in the **Slack engineering channel**. ### 3. Update the Feature Flag in PostHog 1. Log into **PostHog** on the **production project** 2. In the left-hand menu, navigate to **Features > Feature Flags** 3. Find the relevant feature flag key in the list and open it for editing ### 4. Add a Release Condition Under **Release conditions**, add a new condition set for the team: 1. Click to add a new condition set 2. Give the condition set a **description** — use the team or customer name for readability (see [Organizing Condition Sets](https://flowfuse.com/#organizing-condition-sets) below) 3. Add a filter: **`team-id`** — **equals** — **``** 4. Set the **rollout percentage** to **100%** 5. **Save** the feature flag ### 5. Verify and Close - Verify that the feature flag change is reflected on **production/cloud** — confirm the team now has access to the feature - Once verified, close the change request in the CloudProject repository ## Organizing Condition Sets When adding release conditions, prefer **one condition set per customer**. If a customer has multiple teams, group them under the same condition set. This approach keeps things readable because each condition set can have a description identifying the customer, avoiding the need to track and correlate raw team IDs. If the feature flag already has existing condition sets, follow the pattern that was established when the flag was first created. Use your judgement on the best approach — the goal is to keep the list of conditions manageable and easy to audit. # Kubernetes Hardening This guide walks you through the best ways to secure a Kubernetes cluster running FlowFuse. Locking down your cluster shrinks your attack surface and keeps things contained if a single component gets compromised. These tips work best when implemented together, so roll out as many of them as your environment allows. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} These are general hardening recommendations. Adapt them to your organisation's security policies and your cluster's specific configuration. ::: :: ## Network Policies By default, Kubernetes allows unrestricted network traffic between all pods, across all namespaces. Any pod can reach any other pod on any port. This flat network model means that a single compromised pod, for example a vulnerable instance, can be used to reach and attack every other workload in the cluster. Network Policies let you enforce the principle of least privilege at the network layer: a pod should only be able to talk to the workloads it genuinely needs. Restricting traffic contains lateral movement, so a breach in one component cannot trivially spread to others. ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} **Do not treat the policies below as a copy-and-paste solution.** They are illustrative examples, tied to the assumptions of the environment they were written for - namespace names, the ingress controller, the CNI, service ports, which components are deployed, and where operators live. Applied blindly they will either break platform traffic or leave gaps you believe are closed. Network Policies are one of the easiest things in Kubernetes to get subtly wrong: a rule that *looks* correct can silently drop traffic (wrong port direction, Service vs pod port, a missing return path) or silently allow it (an unenforced CNI, an overly broad selector). Implement them deliberately, with a working understanding of how traffic actually flows in your cluster - pod-to-pod, cross-namespace, ingress, egress, and DNS. Roll out one policy at a time, start in a non-production environment, verify each addition against real traffic (and check the affected pods' logs and Service endpoints), and confirm your CNI actually enforces policies before relying on them for security. ::: :: ### FlowFuse and Network Policies FlowFuse runs the platform (namespace of your choice, selected during Helm chart installation) and the hosted Node-RED instances (configured with the `forge.projectNamespace` Helm chart value) in separate namespaces. If you enforce Network Policies, you must explicitly allow the traffic FlowFuse needs - otherwise the instances cannot reach the platform. See [I use Kubernetes Network Policies, how can I configure them?](https://flowfuse.com/docs/install/kubernetes#i-use-kubernetes-network-policies-how-can-i-configure-them) for the required policy. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The following examples assume the default namespaces `flowfuse`, as a core application namespace, and `projects` for Hosted Instances namespace. If you have configured different namespaces, replace them accordingly. ::: :: The two namespaces have very different trust levels, so they are hardened differently: - **`flowfuse`** runs trusted first-party components (core app, MQTT broker, private registry, local database). Here we restrict **inbound** traffic only — deny all ingress, then allow the connections the platform needs. Egress is left open so the platform can reach external services (licensing, npm registry, SMTP, etc.) without maintaining a brittle allow-list. - **`projects`** runs Node-RED instances executing **user-supplied flows** - untrusted code. Here we lock down **both ingress and egress** to contain a malicious or compromised flow: it should reach only the platform services it legitimately needs, and nothing else. ### Core platform namespace (`flowfuse`) **1. Deny all inbound traffic.** Egress is intentionally not restricted here. ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress namespace: flowfuse spec: podSelector: {} policyTypes: - Ingress ``` **2. Allow inbound from the ingress controller** so users can reach the application (namespace usually `traefik`): ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-from-traefik namespace: flowfuse spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik ``` **3. Allow inbound from the Hosted Instances** so Node-RED instances can reach the MQTT broker, core app and the private registry: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-from-projects namespace: flowfuse spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: projects ``` **4. Allow traffic between the platform's own components** in this namespace - the core app connecting to the database, broker and registry, plus broker clustering. As these are all trusted first-party components, we allow intra-namespace traffic: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-intra-namespace namespace: flowfuse spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: flowfuse ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The Helm chart also ships a `flowforge-database-policy` that permits the core app → database connection when using the embedded database. The `allow-intra-namespace` rule above is a superset of it; keep both if you prefer defence in depth. ::: :: **5. Allow inbound from the EMQX operator** - The MQTT broker cluster is managed by the EMQX operator, which usually runs in its own namespace (`emqx-operator` by default) and polls the broker's management API (port `18083`) to set a pod *readiness gate*. If this is blocked, the check times out, the readiness gate never turns true, the broker pods are marked `NotReady`, their Service endpoints go empty, and every broker client fails to connect with a `503` error response. This rule is required for the operator to manage the broker cluster correctly: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-from-emqx-operator namespace: flowfuse spec: podSelector: matchLabels: apps.emqx.io/instance: emqx policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: emqx-operator ports: - protocol: TCP port: 18083 ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The same pattern applies to any other operator, admission webhook, or metrics controller that must reach pods in this namespace: allow ingress from its namespace, or its readiness/reconcile checks will silently break your Services. If a Service unexpectedly loses its endpoints after applying policies, check the managing controller's logs for connection timeouts. ::: :: ### Hosted Instances namespace (`projects`) This namespace runs untrusted user flows, so we deny **both** directions by default and add back only what an instance legitimately needs. **1. Deny all inbound and outbound traffic:** ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: projects spec: podSelector: {} policyTypes: - Ingress - Egress ``` **Instance isolation comes for free here.** With no rule permitting `projects` → `projects` traffic, Node-RED instances cannot reach another in either direction. A separate "deny pod-to-pod" policy is not needed since a compromised instance already cannot talk to its neighbours. **2. Allow DNS resolution** (cluster DNS, usually in `kube-system`): ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns namespace: projects spec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - protocol: TCP port: 53 ``` **3. Allow traffic with the ingress controller**: - **Ingress:** users reach the Node-RED editor and dashboards - **Egress:** allow a Hosted Instance to reach the core app and private npm registry through the ingress controller ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-traefik namespace: projects spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik ``` **4. Allow inbound from the platform** so the core app can manage and health-check instances: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-from-flowfuse namespace: projects spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: flowfuse ``` **5. Allow outbound to the platform services** - the MQTT broker, core app and private npm registry. Instances **should not** connect to the database directly, so it is deliberately not allowed: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-egress-to-flowfuse namespace: projects spec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: flowfuse ports: - protocol: TCP port: 1883 # MQTT broker - protocol: TCP port: 1884 # MQTT over WebSocket - protocol: TCP port: 3000 # core app pod port - protocol: TCP port: 4873 # private npm registry ``` **6. (Optional) Allow outbound to the public internet.** Thanks to the `default-deny-all` policy, instances **do not have** access to the Internet. If your flows need to call external APIs, add the rule below. It allows outbound to the internet while excluding private ranges: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-egress-external namespace: projects spec: podSelector: {} policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 # RFC1918 private range - 172.16.0.0/12 # RFC1918 private range - 192.168.0.0/16 # RFC1918 private range - 169.254.0.0/16 # link-local: blocks the cloud metadata endpoint (169.254.169.254) and node-local DNS - 100.64.0.0/10 # CGNAT range used internally by some managed clusters (GKE, EKS) ports: - protocol: TCP port: 443 - protocol: TCP port: 80 ``` The `except` list is what makes this rule safe: it blocks instances from reaching internal networks even while internet access is open. Note the `169.254.0.0/16` entry in particular as it blocks the cloud metadata endpoint (`169.254.169.254`), which would otherwise let a compromised flow retrieve the node's cloud IAM credentials. `100.64.0.0/10` covers the CGNAT range that some managed Kubernetes providers use internally. Include it if your cluster does. Note that excluding `169.254.0.0/16` also blocks node-local DNS cache, which runs on a link-local address. If your cluster uses it, add an egress rule allowing UDP/TCP `53` to that address (commonly `169.254.20.10`) so DNS keeps resolving. ## TLS for Ingress Without TLS, all traffic between users and the platform, including login credentials, session cookies, API tokens and flow data, travels in plaintext. Anyone able to observe the network path (a compromised router, a shared Wi-Fi network, a malicious intermediary) can read or tamper with it. Enabling TLS encrypts this traffic and lets clients verify they are talking to the genuine platform, protecting against eavesdropping and man-in-the-middle attacks. Serving the platform over HTTPS is a baseline requirement for any production deployment. FlowFuse supports TLS termination either at a cloud Load Balancer or at the Kubernetes Ingress Controller (via [Cert-Manager](https://cert-manager.io/docs/){rel=""nofollow""}). Full configuration steps are in the installation guide: - [I would like to secure the platform with HTTPS, how can I do that?](https://flowfuse.com/docs/install/kubernetes#i-would-like-to-secure-the-platform-with-https-how-can-i-do-that) ## Database Hardening FlowFuse stores all its data in a PostgreSQL database. The database is a critical component of the platform, and if it is compromised, an attacker can read or modify all data. The following recommendations reduce the risk of compromise and limit the impact if it does happen. ### Use a dedicated, least-privilege database user The FlowFuse application should connect using a dedicated database user that owns only its own database — never the PostgreSQL superuser (`postgres`). If the application's credentials are leaked, a scoped user limits the blast radius to the FlowFuse database rather than the entire database server. Create a dedicated user and database, for example: ```sql CREATE USER flowfuse WITH PASSWORD 'a-strong-generated-password'; CREATE DATABASE flowforge OWNER flowfuse; ``` Then configure FlowFuse to connect with these credentials. See [How to use external database server?](https://flowfuse.com/docs/install/kubernetes/#how-to-use-external-database-server%3F){rel=""nofollow""} for how to configure the connection in `customization.yml`. Additional recommendations: - **Use a strong, randomly generated password** - **Require TLS for database connections** so credentials and data are encrypted in transit between the platform and the database - **Restrict network access** to the database so only the FlowFuse platform can reach it (see [Network Policies](https://flowfuse.com/#network-policies), or your cloud provider's firewall / security group rules for managed databases) ### Backups Regular backups protect against data loss from accidental deletion, corruption, or a failed upgrade. A hardened deployment is not complete without a tested backup strategy. - **External / managed database:** use your database provider's backup and point-in-time-recovery features - **Embedded database:** if you use the Helm chart's internal PostgreSQL (`forge.localPostgresql: true`), you can schedule backups with a Kubernetes CronJob running `pg_dump`. A ready-to-use `CronJob` + `PersistentVolumeClaim` example is provided in the installation guide: [How to backup embedded database?](https://flowfuse.com/docs/install/kubernetes/#how-to-backup-embedded-database%3F){rel=""nofollow""} ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} **Test your restores.** A backup is only useful if it can be restored. Periodically verify that you can restore from a backup into a clean database. ::: :: ## RBAC (Role-Based Access Control) Kubernetes RBAC controls *who* (users, groups, service accounts) can perform *what* actions (verbs like `get`, `list`, `create`, `delete`) on *which* resources. RBAC is the primary mechanism for enforcing least privilege inside the cluster. The core objects are: - **Role / ClusterRole** - a set of permissions. A `Role` is namespace-scoped; a `ClusterRole` applies cluster-wide. - **RoleBinding / ClusterRoleBinding** - grants a Role or ClusterRole to a subject (user, group or service account). ### Least-privilege principles - **Grant the minimum.** Give each user and service account only the permissions they actually need, scoped to the narrowest namespace and resource set that works. Avoid broad wildcards (`verbs: ["*"]`, `resources: ["*"]`) - **Never grant `cluster-admin` casually.** Reserve it for a small number of trusted administrators, day-to-day operations rarely need it - **Prefer namespaced `Role`s over `ClusterRole`s** unless a permission genuinely must span the whole cluster - **Audit regularly.** Review bindings periodically and remove access that is no longer needed. Command like `kubectl auth can-i --list` may help you inspect effective permissions ### Example: a read-only namespaced Role ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: flowfuse name: pod-reader rules: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-pods namespace: flowfuse subjects: - kind: User name: user@example.com apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io ``` This grants the user `user@example.com` read-only access to pods and their logs in the `flowfuse` namespace - and nothing else. ## Other High-Level Best Practices The following practices further reduce the attack surface of your cluster: - **Keep Kubernetes and node images patched.** Run a supported Kubernetes version and apply security updates to the control plane, nodes, and container images promptly - **Manage secrets properly.** Store credentials in Kubernetes Secrets (ideally with encryption at rest enabled, or an external secrets manager such as HashiCorp Vault or a cloud KMS). Never commit secrets to version control - **Apply Pod Security Standards.** Enforce the `restricted` [Pod Security Standard](https://kubernetes.io/docs/concepts/security/pod-security-standards/){rel=""nofollow""} where possible: run containers as non-root, drop unnecessary Linux capabilities, use a read-only root filesystem, and disallow privilege escalation - **Set resource requests and limits.** CPU and memory limits prevent a single workload — such as a runaway flow — from starving others and provide a defence against resource-exhaustion denial-of-service - **Enable audit logging.** Kubernetes audit logs record who did what and when, which is essential for detecting and investigating incidents - **Limit access to the cluster API and nodes.** Restrict the API server to trusted networks, avoid exposing the Kubelet, and disable SSH access to nodes where you can - **Scan images for vulnerabilities.** Use image scanning in your CI pipeline and admission control to block images with known critical vulnerabilities - **Use namespaces for isolation.** Separating workloads into namespaces makes RBAC and Network Policies easier to reason about and enforce # Administering FlowFuse ## Getting started - [Understanding the FlowFuse Architecture](https://flowfuse.com/docs/contribute/architecture) - [Install/Upgrade](https://flowfuse.com/docs/install) - requirements, deployment models, installation methods and upgrading - [`flowforge.yml` configuration](https://flowfuse.com/docs/install/configuration) - base platform configuration, done before you run. - [First Run Setup](https://flowfuse.com/docs/install/first-run) - create your admin user - [FlowFuse Concepts](https://flowfuse.com/docs/user/concepts) - [Usage Telemetry](https://flowfuse.com/docs/admin/telemetry) - [Single-Sign On](https://flowfuse.com/docs/admin/sso/) - [Licensing](https://flowfuse.com/pricing/){rel=""nofollow""} - [User management](https://flowfuse.com/docs/admin/user-management) - [Platform Monitoring](https://flowfuse.com/docs/admin/monitoring) - [Soft Launch Enablement](https://flowfuse.com/docs/admin/feature-flags) ## Administering FlowFuse ### Accessing the Admin Settings The Admin Settings can be accessed from the main menu: ![](https://flowfuse.com/docs/admin/images/admin-menu-option.png){width="300"} ### Admin Settings The Admin Settings view lets you manage the platform and its users. The following settings are available: - **Allow new users to register on the login screen** (default: `false`) :br With this option enabled, the platform login page allows visitors to register with the platform. :br This option is only available if email sending has been enabled. - **Create a personal team for users when they register** (default: `false`) :br With this option enabled, the platform will automatically create a Team for the user. This allows them to start creating Node-RED instances straight away. :br By default, this doesn't happen, which means the user must either manually create the Team (if that option is enabled), or be invited to an existing Team. :br When enabled, a choice of what type of team should be created for the user is shown. - **Allow users to reset their password on the login screen** (default: \`false) :br With this option enabled, a 'forgot your password' link is shown on the login screen, and provides a workflow where a user can reset their password via a link emailed to them. :br This option is only available if email sending has been enabled. - **Allow users to create teams** (default: `false`) :br This option allows users to create new Teams on the platform. By default, it is not enabled which means all Teams must be created by an Admin. - **Allow users to invite external users to teams** (default: `false`) :br This option allows users to invite people to join a Team who are not currently registered users of the platform. It sends an email with an invitation to sign-up to the platform and join the Team. :br By default, this is not enabled - users must be added by an Admin. :br This option is only available if email sending has been enabled. ### Managing Users The Users page of Admin Settings can be used to manage the user on the platform. It can be used to: - Add new users to the platform. :br With the 0.1.0 release, the admin sets the new user's password and it is left to the admin to share the login details with the user outside of the platform. - Edit a user's details. :br This includes making them an admin - giving them full access to the platform. It also provides a list of all pending user invitations, showing who invited whom to which team. ### Managing Teams With the 0.1.0 release, the Teams page just lists the teams on the platform. Further team management options will come in later releases. ### Managing Team Types The Team Types page can be used to manage the Team Types on the platform. They determine what features of the platform are available to teams of a given type, including what Instance Types are available and any limits that should be applied. #### Team Type Features - **Team Library**:br Enables Team Level sharing of flow fragments and examples. More details [here](https://flowfuse.com/docs/user/shared-library) - **Project Nodes**:br Enables the FlowFuse Project Nodes which allows Instances within a Team to pass messages between them. More details [here](https://flowfuse.com/docs/user/projectnodes) - **Custom NPM Catalogs**:br Allows .npmrc entries to be passed to Hosted and Remote Instances and also custom Node-RED Catalog URLs to be injected. This allows Node-RED nodes to be loaded from 3rd party registries (e.g. npm proxy instances). More details [here](https://flowfuse.com/docs/user/instance-settings#palette) - **NPM Packages**:br Allows a Team to upload and then access their own private Node-RED nodes (or other NodeJS module packages). This can include Subflows packaged using the FlowFuse Subflow Packaging Sidebar (Requires Team Library Feature to be enabled). More details [here](https://flowfuse.com/docs/user/custom-npm-packages) - **Certified Nodes**:br Allows the Team access to [FlowFuse Certified nodes](https://flowfuse.com/certified-nodes/){rel=""nofollow""} (Requires security token from FlowFuse, only available to Enterprise license holders) - **FlowFuse Exclusive Nodes**:br Allows the Team access to [FlowFuse Exclusive nodes](https://flowfuse.com/node-red/flowfuse/){rel=""nofollow""} (Requires security token from FlowFuse, only available to Enterprise license holders) - **Email Alerts**:br Allows Teams to enable email alerts when an Hosted Instance crashes or is approaching the CPU/Memory limits set for it's Stack (requires email to be enabled). More details [here](https://flowfuse.com/docs/user/instance-settings#alerts) - **Protected Instances**:br Allows Hosted Instances to be marked as Protected. This disables Editor access and requires all flows to be updated via a Pipeline. More details [here](https://flowfuse.com/docs/user/devops-pipelines#protected-instances) - **Git Integration**:br This allows a GitHub project to be used as the source or output of a Pipeline. More details [here](https://flowfuse.com/docs/user/devops-pipelines#git-repository-stage) - **API/Debug Length Limits**:br Lets a Team change the size of the max payload a Hosted Instance can accept and also the size messages sent to the Debug Sidebar will be truncated to. More details [here](https://flowfuse.com/docs/user/instance-settings#editor) - **Static Assets**:br Allows Hosted Instances to share static files stored in the Instance Persistent Storage space via HTTP. More details [here](https://flowfuse.com/docs/user/static-asset-service) - **Instance Resources**:br Enables CPU and Memory historical charts for Hosted and Remote Instances. - **Application Level RBAC**:br Allows Team level Roles to be applied at an Application level inside the Team. - **Team-based Endpoint Security**:br Allows HTTP endpoints provided by a Hosted or Remote Instance to require a FlowFuse account and membership to the allow access. Also applies to Node-RED Dashboards hosted on the instances. More details [here](https://flowfuse.com/docs/user/http-access-tokens) - **Device Groups**:br Allows Remote Instances to be placed in groups which can be used as the source or target of a Pipeline stage. More details [here](https://flowfuse.com/docs/user/device-groups) - **Device Auto Snapshot**:br Creates a snapshot on every deploy when the Remote Instance is in Developer Mode. These snapshots keep a rolling set of last 10 deploys. More details [here](https://flowfuse.com/docs/user/snapshots) - **Instance Auto Snapshot**:br Creates a snapshot on every deploy from a Hosted Instance. These snapshots keep a rolling set of the last 10 deploys. More details [here](https://flowfuse.com/docs/user/snapshots) - **Custom Hostnames**:br Allows Hosted instances to be accessed on a second hostname with an arbitrary domain (Kubernetes only, requires specific configuration). More details [here](https://flowfuse.com/docs/user/custom-hostnames) - **High Availability**:br Allows for 2 copies of Node-RED to be running for a single Hosted Node-RED instance to provide Load Balanced higher throughput and fail over protection (Kubernetes only). More details [here](https://flowfuse.com/docs/user/high-availability) - **Bill of Materials / Dependencies**:br Provides a Team level view of what Node-RED nodes are installed and what versions are being used and which have newer versions available. More details [here](https://flowfuse.com/docs/user/bill-of-materials) - **Version History Timeline**:br Enables a graphical representation of Snapshot creation to allow a clearer picture of when they are created. More details [here](https://flowfuse.com/docs/user/snapshots#timeline-view) - **Team Broker**:br Provides a Team scoped MQTT broker and Asynchronous API documentation generation. More details [here](https://flowfuse.com/docs/user/teambroker) - **Tables**:br Provides a Team scoped shared SQL Relational Database (Requires specific configuration). More details [here](https://flowfuse.com/docs/user/ff-tables) - **AI Features**:br Global toggle for all AI functionality within the team. When disabled, all AI features below are unavailable regardless of their individual settings. Requires `ai.enabled: true` in the platform configuration. - **Expert Assistant**:br Enables the FlowFuse Expert chat assistant for the team. Provides AI-powered support for building and debugging Node-RED flows. Enabled by default (opt-out). Requires the AI Features flag to be enabled. More details [here](https://flowfuse.com/docs/user/expert/) - **Expert Insights**:br Enables the Insights mode of FlowFuse Expert, allowing users to query live operational data from Node-RED instances via MCP. Enabled by default (opt-out). Requires the AI Features flag to be enabled. More details [here](https://flowfuse.com/docs/user/expert/chat/) - **Assistant Inline Code Completion**:br Allows LLM assistance when writing Function nodes. Requires the AI Features flag to be enabled and assistant service configuration. More details [here](https://flowfuse.com/docs/user/expert/node-red-embedded-ai/) - **Generated Snapshot Descriptions**:br Enables AI-generated descriptions of Snapshots. Requires the AI Features flag to be enabled and assistant service configuration. More details [here](https://flowfuse.com/docs/user/expert/node-red-embedded-ai/) ### Managing Instance Types The Instance Types page can be used to manage the Instance Types on the platform. When billing is enabled, an instance type can be associated with a particular Stripe Product/Price - allowing each type to have a different monthly price associated with it. The Instance Types page shows what types are currently active, how many stacks each type has been assigned to it, and how many instances have been created of that type. Whenever a new Instance Type is created, it will need to be manually enabled for the individual [Team Types](https://flowfuse.com/#managing-team-types) before they will be available for teams to use. Instance Types also control the default Stack (and in turn, the default Node-RED versions) that Instances will run with if there are multiple available Stacks associated with an Instance Type. ### Managing Stacks > Admin Settings > Stacks The Stacks page can be used to manage the Stacks on the platform. It can be used to create and edit the stacks on the platform. For Deployment specific information about working with stacks, refer to the documentation of your chosen deployment model: - [Local Stacks](https://flowfuse.com/docs/contribute/local/stacks) - [Docker Stacks](https://flowfuse.com/docs/install/docker/stacks) - [Kubernetes Stacks](https://flowfuse.com/docs/install/kubernetes/stacks) #### Create Stack ##### Upgrading Stacks You can create a new version of an existing stack via the drop-down menu in the stack table. This allows the platform to notify users that an update is available for their instance, allowing them to upgrade the stack at their convenience. ![Screenshot of FlowFuse showing where admins can create new stacks](https://flowfuse.com/docs/admin/images/admin-stacks-create-new-version.png){dataZoomable=""}*Screenshot of FlowFuse showing where admins can create new stacks* Node-RED instances that use the old stack will offer the new stack as a one-click upgrade option. ##### Create New Stack Alternatively, click 'Create stack' to create an entirely new stack. ![Screenshot of FlowFuse showing the "Create Stack" dialog](https://flowfuse.com/docs/admin/images/admin-stack-create.png){dataZoomable=""}*Screenshot of FlowFuse showing the "Create Stack" dialog* When prompted for the Node-RED version, the value here depends on the setup you're running: - **Local**: Provide the exact stack name (Node-RED version) that was installed. For example, if you ran the script with `latest` and it resulted in `3.1.9` being installed, you should enter `3.1.9`. This must match the directory name created in your `stacks` directory. If you changed the directory name for any reason, make sure to use that name. - **Docker**: Support the container image name ([docs](https://flowfuse.com/docs/install/docker/stacks)) - **k8s**: Support the container image name ([docs](https://flowfuse.com/docs/install/kubernetes/stacks)) #### Updating Stacks It is *not* possible to edit a stack that is being used by Instances. ### Managing Templates With [templates](https://flowfuse.com/docs/user/concepts/#template) administrators can apply Node-RED configuration options as default. For these options, the administrator can lock the selected value so users cannot change them, or keep them editable by end-users. If you edit a template that is being used by an Application Instance, those changes will get applied the next time the instance is restarted. #### Disallow using nodes On FlowFuse Cloud, but recommended on self-managed installs on Docker and Kubernetes, where certain nodes will not work so are excluded from being used by the template. When adding `31-tcpin.js,32-udp.js,10-file.js,23-watch.js,90-exec.js` to the `Exclude node by filename` section of a template and locking this value users are prevented from using these. # Platform Monitoring FlowFuse provides an API end-point that can be used to monitor statistical information about the platform. The end-point is accessible to any logged-in Admin user at the url (replace `example.com` with the domain of your FlowFuse instance) - `https://example.com/api/v1/admin/stats` By default, it returns a JSON object containing key statistics such as the number of users, instances, and other information. If the `accept` header of the http request includes `application/openmetrics-text` then the response is formatted as OpenMetrics text. This can be directly consumed by tools such as Prometheus. ## Enabling token-based access In Admin Settings there is an option to allow token-based access to platform statistics. When enabled, the platform will generate an access token that can be used to access the end-point without having a full Admin login. This is useful when configuring tools such as Prometheus to monitor the platform. 1. Under Admin Settings -> General, check the 'allow token-based access' option 2. A dialog is shown containing the token. This is the *only* time the token will be shown - make sure you record its value. The token should be provided as a bearer token in any http request to the end-point; ```bash TOKEN=your_generated_token curl -H 'Authorization: Bearer $TOKEN' https://example.com/api/v1/admin/stats ``` ## Configure Prometheus to scrape the Platform statistics endpoint To configure Prometheus to scrape the FlowFuse Platform statistics endpoint, you need to add a new scrape job to your Prometheus configuration file (usually `prometheus.yml`). Since the statistics endpoint is secured, you will need to provide the access token in the scrape configuration (see the previous paragraph on enabling token-based access). Once you have the token, you can add a new scrape job to your Prometheus configuration (replace `` and `` with yor FlowFuse Platform-sepcific values): ```yaml scrape_configs: - job_name: flowfuse_platform_stats metrics_path: /api/v1/admin/stats scheme: https authorization: type: Bearer credentials: "" scrape_protocols: ["OpenMetricsText1.0.0"] static_configs: - targets: [""] ``` # Observability Observability is the ability to understand the internal state and behaviour of a system by analysing its outputs, without needing to know its internal workings. For a self-hosted FlowFuse platform running on Kubernetes, this means having a holistic view of the platform and the cluster it runs on - their health, performance, and any potential issues - so you can detect and diagnose problems before they affect your users. This page describes the kinds of tools you can use and what each is for, so you can plan an observability stack that fits your environment. It does not prescribe a specific deployment; the tools named below are common open-source choices, but the concepts apply equally to managed or commercial alternatives. ## The three pillars Observability is usually described in terms of three complementary signals: - **Metrics** - numeric measurements sampled over time (request rates, memory usage, pod counts). Good for dashboards, trends, and alerting thresholds. - **Logs** - timestamped records of discrete events emitted by the platform and by the workloads running on the cluster. Good for understanding *why* something happened. - **Dashboards & visualization** - a unified place to explore metrics and logs together, spot correlations, and share views across a team. A complete setup collects all three and ties them together, so an alert on a metric can be investigated against the corresponding logs. ## Tools ### Prometheus [Prometheus](https://prometheus.io/){rel=""nofollow""} collects and stores time-series metrics by scraping HTTP endpoints at regular intervals. For a self-hosted FlowFuse platform there are two useful sources of metrics: - **Kubernetes cluster metrics** - cluster-level metrics such as CPU and memory usage, pod status, and node health, typically gathered via [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics){rel=""nofollow""} and [node-exporter](https://github.com/prometheus/node_exporter){rel=""nofollow""}. - **Hosted Instances metrics** - every Hosted Instance is a pod running on the Kubernetes cluster. It produces metrics that are exposed by cAdvisor at the `/metrics` endpoint, which Prometheus can scrape. This includes CPU, memory and networking usage. - **Platform metrics** - FlowFuse core deployment exposes a `/metrics` endpoint that Prometheus can scrape. This includes resources performance statistics, request rates, and error counts, etc. ### Loki [Loki](https://grafana.com/oss/loki/){rel=""nofollow""} is a log aggregation system designed to pair with Prometheus. It collects, stores, and lets you query logs from the platform and from the pods running on your cluster, so you can analyse logs alongside metrics using the same labels. ### Grafana [Grafana](https://grafana.com/oss/grafana/){rel=""nofollow""} is an open-source platform for building and sharing dashboards. It provides a single interface over your observability data: - **Data source integration** - connect Prometheus, Loki, and other sources to visualize metrics and logs in one place. - **Customizable dashboards** - build tailored views for platform health, resource usage, and workload behaviour. - **Alerting** - define alerting rules against your metrics and logs to be notified of problems proactively. ## Quick start The fastest way to get started is the **LGTM stack** - Loki (logs), Grafana (dashboards), Tempo (traces), and Mimir/Prometheus (metrics) - is to either use Grafana Cloud or deploy it self-hosted: - **Grafana Cloud (hosted)** - sign up for [Grafana Cloud](https://grafana.com/products/cloud/){rel=""nofollow""} and install the [Kubernetes Monitoring](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/){rel=""nofollow""} integration. It deploys collectors (Grafana Alloy) into your cluster that ship metrics and logs to Grafana's managed backend. Point one scrape target at the FlowFuse Platform metrics endpoint and you have platform plus cluster visibility with minimal setup. - **Self-hosted LGTM** - run the stack inside your own cluster using the [`kube-prometheus-stack`](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack){rel=""nofollow""} (Prometheus + Grafana + Alertmanager) and [`loki`](https://github.com/grafana/loki/tree/main/production/helm/loki){rel=""nofollow""} Helm charts. You keep full control of your data and retention, at the cost of maintaining the stack yourself. Start with Grafana Cloud if you want results quickly and are comfortable sending telemetry off-cluster; choose self-hosted LGTM if data residency or cost at scale matters. The concepts in the rest of this page apply to either. ### Initial Grafana dashboards Rather than building Grafana dashboards from scratch, import a few proven community dashboards to get cluster visibility on day one. In Grafana, use **Dashboards → New → Import** and enter the dashboard ID: - **[Node Exporter Full](https://grafana.com/grafana/dashboards/1860){rel=""nofollow""}** (ID `1860`) - per-node CPU, memory, disk, and network from node-exporter. - **[Kubernetes / Views / Global](https://grafana.com/grafana/dashboards/15757){rel=""nofollow""}** (ID `15757`) - cluster-wide health and resource usage across nodes, namespaces, and workloads. - **[Kubernetes / Views / Pods](https://grafana.com/grafana/dashboards/15760){rel=""nofollow""}** (ID `15760`) - per-pod CPU, memory, and network, useful for inspecting individual Hosted Instances. > If you deployed the `kube-prometheus-stack` Helm chart, a full set of `Kubernetes / Compute Resources / *` dashboards is installed automatically - check the dashboard list before importing duplicates. Once the cluster basics are in place, build a FlowFuse-specific dashboard from the platform metrics described under [Prometheus](https://flowfuse.com/#prometheus) above. ## Next steps - [Platform Monitoring](https://flowfuse.com/docs/admin/monitoring) - details of the FlowFuse statistics endpoint used by Prometheus. - [Installing FlowFuse on Kubernetes](https://flowfuse.com/docs/install/kubernetes/) - deploying and configuring the platform on a cluster. # Configuring Single Sign-On *This feature is only available on self-hosted Enterprise licensed instances of FlowFuse.* FlowFuse allows users to sign in through their SAML identity provider, such as Google Workspace, or using LDAP against a directory service provider. The platform can be configured with multiple SSO configurations and uses the user's email domain to identify which provider should be used. Since FlowFuse 2.7, the SSO configuration can be configured to automatically registers when they first sign in. Otherwise, a user will have to first register on the platform, providing a temporary password in order to create an account. They will then be able to log in via their SSO provider. By default, admin users can still log in with their original FlowFuse username and password, which ensures they are not locked out if there is a problem with the SSO configuration. ## SAML SSO SAML based SSO allows the FlowFuse platform to authenticate users against their identity provider such as Google Workspace. Once enabled for a particular email domain, regular users on that domain will be directed to the Identity Provider in order to log in. They will no longer be able to log in with their local password, nor will they be able to change their email address in User Settings. - [Configuring SAML SSO](https://flowfuse.com/docs/admin/sso/saml) ## LDAP SSO LDAP based SSO allows the FlowFuse platform to authenticate users against a directory service provider, such as OpenLDAP. When logging in, the users credentials are passed to the service provider to verify. - [Configuring LDAP SSO](https://flowfuse.com/docs/admin/sso/ldap) # Configuring LDAP based Single Sign-On *This feature is only available on self-hosted Enterprise licensed instances of FlowFuse.* The SSO Configurations are managed by the platform Administrator under the `Admin Settings > Settings > SSO` section. The user must already exist on the FlowFuse platform before they can sign in via SSO. ### Create a SSO Configuration 1. Click 'Create SSO Configuration' to create a new config :br![](https://flowfuse.com/docs/admin/sso/images/create-sso-config-ldap.png) 2. Give the configuration a name to help identify it, and provide the email domain name this configuration should apply to. Ensure the LDAP option is selected - this cannot be changed after the configuration is created. 3. Click 'Create configuration' :br At this point, the configuration has been created and metadata generated for the configuration, but it is not active. ### Configuration LDAP The following fields are provided for configuring LDAP. You will need to refer to your LDAP service provider details for the correct values to enter: ![](https://flowfuse.com/docs/admin/sso/images/edit-sso-config-ldap.png) - `Server` - the address of the LDAP server, including port number. - `Username` - the bind DN to use to connect to the server. The user must have permission to lookup users in the directory. - `Password` - the password to connect to the server with. - `Base DN` - the base object under which user searches are performed. - `User Search Filter` - the filter used to search for a user. See below for more details. - `Enable TLS`- whether to use TLS for the LDAP connection - `Verify Server Certificate` - when TLS is enabled, whether to perform strict certification validation. You can save the configuration at any time by clicking the `Update configuration` button. The configuration will only be enabled when you tick the `active` checkbox and save the changes. ### Session Length Overrides Each SSO configuration can override the platform default Max Session life an Max Session Idle time. These are Controlled by the "Custom Session Expiry (hours)" and "Custom Session Idle Time (hours)" respectively. ![Settings for Custom Session lifetime](https://flowfuse.com/docs/admin/sso/images/edit-sso-custom-session.png)*Settings for Custom Session lifetime* #### User Search Filter The search filter is used when checking if a user exists within the directory, using the standard LDAP query notation. The default search filter is `(uid=${username})`. The platform will replace `${username}` and `${email}` with the user's details when they attempt to login. ## Creating new users With FlowFuse 2.7, the SSO Configuration now includes an option to automatically register users who sign in via the configuration. This option is not enabled by default, but can be enabled but selecting the `Allow Provisioning of New Users on first login` option in the SOO configuration. When creating the user, the platform will use information provided by the LDAP provider to create the username. The user will be directed to their settings page where they can modify their user details to their preferred values. ## Managing Team Membership with LDAP Groups LDAP implementations can also be used to group users To enable this option, select the `Manage roles using group assertions` in the SSO configuration. The following configuration options should then be set: - `Group DN` - this is the base DN to be used to search for group membership. - `Team Scope`- this determines what teams can be managed using this configuration. There are two options: - `Apply to all teams` - this will allow the SAML groups to manage all teams on the platform. This is suitable for a self-hosted installation of FlowFuse with a single SSO configuration for all users on the platform. - `Apply to selected teams` - this will restrict what teams can be managed to the provided list. This is suitable for shared-tenancy platforms with multiple SSO configurations for different groups of users, such as FlowFuse Cloud. When this option is selected, an additional option is available - `Allow users to be in other teams`. This will allow users who sign-in via this SSO configuration to be members of teams not in the list above. Their membership of those teams will not be managed by the SSO groups. If that option is disabled, then the user will be removed from any teams not in the list above. ### LDAP Groups configuration A user's team membership is managed by what groups they are in. When the user logs in, the LDAP provider will be queried for a list of groups they are a member of. This can be either as a `member` or `uniqueMember` of a `groupOfNames` or `groupOfUniqueNames` respectively. The group name is used to identify a team, using its slug property, and the user's role in the team. The name must take the form `ff--`. For example, the group `ff-development-owner` will container the owners of the team `development`. The valid roles for a user in a team are: - `owner` - `member` - `viewer` - `dashboard` *Note*: this uses the team slug property to identify the team. This has been chosen to simplify managing the groups in the LDAP Provider - rather than using the team's id. However, a team's slug can be changed by a team owner. Doing so will break the link between the group and the team membership - so should only be done with care. An optional prefix and suffix can be include in the group name to support LDAP providers that have existing naming policies. The SSO configuration can be configured with the lengths of these values so they will be stripped off before the group name is validated. For example, if an organisation requires all groups to begin with `acme-org-`, a prefix length of `9` can be set and the group `acme-org-ff-development-owner` will be handled as `ff-development-owner`. ### Application Level Groups In addition to being able to add Users to Teams using groups, Role overrides for specific Applications within those groups can also be controlled. The User must have a membership of the Team the Application belongs to for an Application override to take effect. If the User is not a member of that Team, the override is ignored. Groups for Application overrides use a similar pattern to Team Groups and use the same prefix and suffix length modifiers. They take the form `ff-[]-`, where `` can be the Application name or id and `` must be one of the valid roles listed above, but can also be `none` to remove access from an Application within the Team. Any unrecognised role is ignored. For example, given a Team called `development` and an Application called `test`, owner-level access to the Team would be granted by membership of a group named `ff-development-owner`, and an override to `viewer` for the Application `test` would be granted by `ff-development[test]-viewer`. If multiple groups grant different roles for the same Application, the highest role is applied. Application overrides applied this way are managed by the SSO provider: they cannot be edited in the FlowFuse UI, and if the corresponding group is removed the override is cleared the next time the User logs in. ## Managing Admin users The SSO Configuration can be configured to manage the admin users of the platform by enabling the `Manage Admin roles using group assertions` option. Once enabled, the name of a group can be provided that will be used to identify whether a user is an admin or not. \**Note:* the platform will refuse to remove the admin flag from a user if they are the only admin on the platform. It is *strongly* recommended to have an admin user on the system that is not managed via SSO to ensure continued access in case of any issues with the SSO provider. ## Providers The following is the node-exhaustive list of the providers that are known to work with FlowFuse LDAP SSO. - [OpenLDAP](https://www.openldap.org/){rel=""nofollow""} # Configuring SAML based Single Sign-On *This feature is only available on FlowFuse Cloud and self-hosted Enterprise licensed instances of FlowFuse.* The SSO Configurations are managed by the platform Administrator under the `Admin Settings > Settings > SSO` section. To fully configure SAML SSO, you will need to generate a configuration in FlowFuse, provide some of the generated values to your Identity Provider, and copy back some values they provide. ## Configuring SSO on FlowFuse Cloud Configuring SSO on FlowFuse Cloud requires co-ordinating tasks between the customer and FlowFuse Cloud administrators. **All changes must be made via a [Production Change Request](https://github.com/FlowFuse/CloudProject/issues/new?assignees=&labels=change-request&projects=&template=change-request.yml&title=Change%3A+){rel=""nofollow""} in the CloudProject repository - even if you are actioning it directly.** When a customer requests SSO to be setup for their users, we require the following information: 1. Confirm the customer's entitlement for SSO enablement. It is only available to *Enterprise* tier customers. 2. The email domain that will be covered by the configuration. Note that each SSO configuration can only be applied to a single domain. If a customer has multiple domains, each one will require its own SSO configuration. [Issue #5011](https://github.com/FlowFuse/flowfuse/issues/5011){rel=""nofollow""} has been raised to make this more flexible in the future. 3. Whether it is SAML or LDAP based SSO 4. What Identify Provider they are using for their SSO. Once this information has been provided, create a Change Request issue in the CloudProject repository recording this information. We can then create a draft SSO configuration in the Admin/Settings/SSO section. The configuration should not be marked as active yet. From the draft configuration, the values of `ACS URL` and `Entity ID / Issuer` can be given to the customer. These values will be required by their Identify Provider. Refer to the provider-specific documentation for how those values get applied. In return, the customer then needs to provide: 1. `Identity Provider Single Sign-On URL` 2. `Identity Provider Issuer ID / URL` 3. `X.509 Certificate Public Key` Again, refer to the provider-specific documentation for where to find these values as each provider has its own terminology. These values should be applied to the draft SSO configuration in FlowFuse Cloud. The configuration can then be marked as active. It is recommended to do this final step whilst on a call with the customer so they can test the setup in real time. ### Common Issues Aside from navigating the mismatched terminology between services providers, the most common issue we hit is where a login attempt fails and 'Invalid Document Signature' is shown in FlowFuse logs. This is because we expect both the SAML Assertions and Responses to be signed by the public certificate. The default configuration for many providers is to only sign the assertions - check the provider-specific documentation for the appropriate option to enable to address this. ## Configuring SSO The following instructions give more details information on how to setup SSO. ### Create a SSO Configuration 1. Click 'Create SSO Configuration' to create a new config :br![](https://flowfuse.com/docs/admin/sso/images/create-sso-config.png) 2. Give the configuration a name to help identify it, and provide the email domain name this configuration should apply to. Ensure the SAML option is selected - this cannot be changed after the configuration is created. 3. Click 'Create configuration' :br At this point, the configuration has been created and metadata generated for the configuration, but it is not active. :br![](https://flowfuse.com/docs/admin/sso/images/edit-sso-config.png) 4. Copy the `ACS URL` and `Entity ID / Issuer` values as you will need to configure your Identity Provider with these values. You can save the configuration at any time by clicking the `Update configuration` button. The configuration will only be enabled when you tick the `active` checkbox and save the changes. ### Configure your Identify Provider Every Identity Provider uses slightly different terminology and varies what information they require and what they provide. This can make it a tricky task to complete. We provide specific guides for the providers we have verified below. If you have a working configuration for a provider not listed here, please do share the details so we can add them to the list. The general points are: 1. Your Identity Provider will supply you with some of the following values that should be entered into your FlowFuse SAML SSO Configuration: - `Single Sign-On URL` - also referred to as 'SAML Endpoint', 'Login URL' or 'IdP SSO URL'. - `Issuer ID / URL` - `X.509 Certification Public Key` - the public key of a certificate used to sign SAML requests. 2. Configure the `NameID` SAML option to be `EmailAddress` and have it return the email of the user logging in. This is how FlowFuse will verify they are a known user on the platform. ### Session Length Overrides Each SSO configuration can override the platform default Max Session life an Max Session Idle time. These are Controlled by the "Custom Session Expiry (hours)" and "Custom Session Idle Time (hours)" respectively. ![Settings for Custom Session lifetime](https://flowfuse.com/docs/admin/sso/images/edit-sso-custom-session.png)*Settings for Custom Session lifetime* ### Enable your SAML SSO Configuration Once you have setup both sides of the configuration you can enable it for use by ticking the `active` checkbox and clicking `Update configuration`. ## Creating new users With FlowFuse 2.7, the SSO Configuration now includes an option to automatically register users who sign in via the configuration. This option is not enabled by default, but can be enabled but selecting the `Allow Provisioning of New Users on first login` option in the SOO configuration. When creating the user, the platform will use information provided by the SAML provider to create the username. The user will be directed to their settings page where they can modify their user details to their preferred values. \## Managing Team Membership with SAML Groups Some SAML providers allow user group information to be shared as part of the sign-in process. When properly configured, this can be used to manage what FlowFuse teams a user has access to. To enable this option, select the `Manage roles using group assertions` in the SSO configuration. The following configuration options should then be set: - `Group Assertion Name` - this is used to identify the group membership information in the response sent by the Identity Provider. It defaults to `ff-roles` but can be customised if the Identify Provider requires it. - `Team Scope`- this determines what teams can be managed using this configuration. There are two options: - `Apply to all teams` - this will allow the SAML groups to manage all teams on the platform. This is suitable for a self-hosted installation of FlowFuse with a single SSO configuration for all users on the platform. - `Apply to selected teams` - this will restrict what teams can be managed to the provided list. This is suitable for shared-tenancy platforms with multiple SSO configurations for different groups of users, such as FlowFuse Cloud. When this option is selected, an additional option is available - `Allow users to be in other teams`. This will allow users who sign-in via this SSO configuration to be members of teams not in the list above. Their membership of those teams will not be managed by the SSO groups. If that option is disabled, then the user will be removed from any teams not in the list above. ### SAML Groups configuration A user's team membership is managed by what groups they are in. When the user logs in, the SAML provider must be configured to provide a list of groups they are a member of as a SAML assertion. The group name is used to identify a team, using its slug property, and the user's role in the team. The name must take the form `ff--`. For example, the group `ff-development-owner` will container the owners of the team `development`. The valid roles for a user in a team are: - `owner` - `member` - `viewer` - `dashboard` *Note*: this uses the team slug property to identify the team. This has been chosen to simplify managing the groups in the SAML Provider - rather than using the team's id. However, a team's slug can be changed by a team owner. Doing so will break the link between the group and the team membership - so should only be done with care. An optional prefix and suffix can be include in the group name to support SAML providers that have existing naming policies. The SSO configuration can be configured with the lengths of these values so they will be stripped off before the group name is validated. For example, if an organisation requires all groups to begin with `acme-org-`, a prefix length of `9` can be set and the group `acme-org-ff-development-owner` will be handled as `ff-development-owner`. ### Application Level Groups In addition to being able to add Users to Teams using groups, Role overrides for specific Applications within those groups can also be controlled. The User must have a membership of the Team the Application belongs to for an Application override to take effect. If the User is not a member of that Team, the override is ignored. Groups for Application overrides use a similar pattern to Team Groups and use the same prefix and suffix length modifiers. They take the form `ff-[]-`, where `` can be the Application name or id and `` must be one of the valid roles listed above, but can also be `none` to remove access from an Application within the Team. Any unrecognised role is ignored. For example, given a Team called `development` and an Application called `test`, owner-level access to the Team would be granted by membership of a group named `ff-development-owner`, and an override to `viewer` for the Application `test` would be granted by `ff-development[test]-viewer`. If multiple groups grant different roles for the same Application, the highest role is applied. Application overrides applied this way are managed by the SSO provider: they cannot be edited in the FlowFuse UI, and if the corresponding group is removed the override is cleared the next time the User logs in. ## Managing Admin users The SSO Configuration can be configured to manage the admin users of the platform by enabling the `Manage Admin roles using group assertions` option. Once enabled, the name of a group can be provided that will be used to identify whether a user is an admin or not. \**Note:* the platform will refuse to remove the admin flag from a user if they are the only admin on the platform. It is *strongly* recommended to have an admin user on the system that is not managed via SSO to ensure continued access in case of any issues with the SSO provider. ## Direct SSO Login For Self Hosted users there is an option in the Admin Settings to enable buttons on the login page for each active SAML SSO provider. These buttons will redirect to the SSO provider rather than requiring users to enter and email address in the username field to select the correct provider. ## Forcing All Users to Use SSO For self-hosted installations that need to ensure no user can bypass SSO, there is an option in **Admin Settings > Settings > SSO > Force all logins for non-admin users via a single SAML SSO provider**. ![SSO settings page showing the option to force all non-admin users to log in via a single SAML SSO provider](https://flowfuse.com/docs/admin/sso/images/force-sso.png)*SSO settings page showing the option to force all non-admin users to log in via a single SAML SSO provider* When this option is enabled: - All users are redirected to the configured SSO provider at login, regardless of their email domain - The email and password login form is no longer presented as a fallback option - Admin users can bypass SSO by accessing `/admin` routes This is intended for organisations running a single identity provider across the entire platform, where per-domain SSO configuration is not sufficient to cover all users. ## Providers The following is a non-exhaustive list of the providers that are known to work with FlowFuse SAML SSO. - [Microsoft Entra](https://flowfuse.com/#microsoft-entra) - [Google Workspace](https://flowfuse.com/#google-workspace) - [OneLogin](https://flowfuse.com/#onelogin) - [Okta](https://flowfuse.com/#okta) - [Keycloak](https://flowfuse.com/#keycloak) ### Microsoft Entra Microsoft provide a guide for creating a custom SAML Application [here](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal){rel=""nofollow""}. The following tables map the Entra terminology to the FlowFuse settings. | FlowFuse Setting | Entra Setting | | -------------------------------------- | -------------------------------------------- | | `ACS URL` | `Reply URL (Assertion Consumer Service URL)` | | `Identity Provider Issuer ID / URL` | `Microsoft Entra Identifier` | | `Identity Provider Single Sign-On URL` | `Login URL` | | `X.509 Certificate Public Key` | `Certificate (Base64)` | Follow these steps to properly configure SAML SSO for Microsoft Entra: 1. In FlowFuse: 1. Create a draft SSO configuration in FlowFuse with the appropriate email domain 2. In Entra: 1. Create a SAML application - use the guide linked above for more information 2. Copy the following values from the FlowFuse SSO configuration into the corresponding Entra configuration: 1. Set `Reply URL` to the value of `ACS URL` 2. Set `Identifier (Entity ID)` to the value of `Entity ID/Issuer` 3. Within the `SAML Signing Certificate` configuration, the `Signing Option` must be set to `Sign SAML response and assertion`. 4. The `Unique User Identifier (Name ID)` claim must be configured to return the value of the `user.mail` source attribute. 5. Download the `Federation Metadata XML` file from the `SAML Certificates` section of the Entra application. 3. In FlowFuse: 1. From the metadata XML file, copy the follow properties into the FlowFuse SSO configuration: 1. Set `Identity Provider Single Sign-On URL` to the value of the `Location` attribute of the `` tag. This should look like `https://login.microsoftonline.com//saml2`. 2. Set `Identity Provider Issuer ID / URL` to the value of the `entityID` attribute of the `` tag. This should look like `https://sts.windows.net//` 3. Set `X.509 Certificate Public Key` to the value of the `` tag. This does *not* need to have the `-----BEGIN CERTIFICATE-----/-----END CERTIFICATE-----` wrapper. #### Group Membership Configuration By default, when enabled, Entra will share group assertions under the name `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups` and provides the groups as a list of object ids. Either the `Group Assertion Name` should be set to this name, or Entra configured to use a custom assertion name that matches the FlowFuse SSO Configuration value. Entra must also be configured to return group names rather than object ids. ### Google Workspace Google provide a guide for creating a custom SAML Application [here](https://support.google.com/a/answer/6087519?hl=en){rel=""nofollow""}. Google Workspace only supports HTTPS-based SSO URLs. You cannot use it when developing locally using `http://localhost:3000`. The following table maps the Google Workspace terminology to the FlowFuse settings. | FlowFuse Setting | Google Workspace Setting | | -------------------------------------- | ------------------------ | | `Identity Provider Single Sign-On URL` | `SSO URL` | | `Identity Provider Issuer ID / URL` | `Entity ID` | | `X.509 Certificate Public Key` | `Certificate` | Within the `Service provider details` configuration, the `Signed response` option must be enabled. ### OneLogin Follow [this guide](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f){rel=""nofollow""} to create a `OneLogin SAML Test Connector`. | FlowFuse Setting | OneLogin Setting | | -------------------------------------- | -------------------------- | | `Identity Provider Single Sign-On URL` | `SAML 2.0 Endpoint (HTTP)` | | `Identity Provider Issuer ID / URL` | `Issuer URL` | | `X.509 Certificate Public Key` | `X.509 Certificate` | ### Okta Within your Okta Admin dashboard, browse the App Integration catalog and add a new instance of the `SAML Service Provider` integration. On the Sign-On Options section, ensure SAML 2.0 is selected. Below that section you will see a notice saying: > SAML 2.0 in not configured until you complete the setup instructions. Click the 'View setup instructions' button to open the page in a new window. Follow the instructions on that - copying the `Identity Provider Issuer`, `Identity Provider HTTP POST URL` and `Identity Provider Certificate` values into the FlowFuse SSO configuration. Back on the Okta SAML Application configuration page, under the `Advanced Sign-on Settings` section enter the `Assertion Consumer Service URL` and `Service Provider Entity Id` from the FlowFuse SSO configuration. Under `Credential Details` section, change the Application username format to `Email`. #### Group Membership Configuration To configure Okta to return Group assertions, edit the Settings of the SAML Service Provider's SAML 2.0 configuration. Expand the 'Attributes' section and add a `Group Attribute Statement`. The name must match the `Group Assertion Name` in the FlowFuse SSO configuration (default: `ff-roles`). You can optionally add a filter of `ff-` so that it only returns groups used by FlowFuse. ### Keycloak Within your Keycloak Admin Console, create a new Client with the following settings: Under the General Settings: - Set `Client type` to `SAML` - Set the `Client ID` to the `Entity ID / Issuer` value from the FlowFuse SSO configuration. Under the Login Settings: - Set `Valid redirect URIs` to the `ACS URL` value from the FlowFuse SSO configuration. Once created and you are shown the full client configuration, make the following additional changes: - Set `Name ID format` to `email` - Under the 'Keys' tab, turn off `Client signature required` Save the changes. Next, select the 'Download adapter config' option under the 'Action' dropdown menu. Select the 'Mod Auth Mellon files' format and click Download. This will download a zip file. Extract the zip and open the `idp-metadata.xml` file in a text editor. The final task is to copy some of the contents of the XML file into the FlowFuse SSO configuration. - Copy the value of the `entityID` attribute into the `Identity Provider Issuer ID / URL` property - Find one of the `md:SingleSignOnService` tags and copy the value of its `Location` attribute into the `Identity Provider Single Sign-On URL` property - Copy the contents of the `ds:X509Certificate` tag into the `X.509 Certificate Public Key` property #### Group Membership Configuration In Keycloak and the Realm setup with FlowFuse as a client: - Create a new "Client Scope" - Give it a name and ensure the "Protocol" is `SAML` - After saving the scope, select the "Mappers" tab - "Add mapper" and pick "By configuration" - Select "Group list" from the options - Give it a name and set "Group attribute name" to `ff-roles` (this must match the value configured in FlowFuse, default 'ff-roles') - Enable 'Single Group Attribute' - Ensure that "Full group path" is unchecked - Save and return to the "Clients" list and select your FlowFuse Client created earlier - Under "Client scopes", use the "Add client scope" button to add the new scope # Usage Telemetry The platform shares anonymous usage information with us at FlowFuse. This helps us understand how the platform is being used, what areas need improvement and how we should prioritise future work. Ultimately, it helps us produce a better platform for all its users. We do not collect: - Any personally identifiable information. We do store a secure hash of the sending IP address - but the plain value is never stored - Any specific details of the flows running on the platform. ## Core Telemetry ### Configuring Telemetry By default, usage telemetry is enabled on the platform. The administrator can opt-out of sharing information as part of the initial setup, or through the Admin Settings section of the platform UI. It is also possible to disable in the `flowforge.yml` configuration file. This overrides whatever option is set in the Admin Settings UI. **IMPORTANT: Licensed installations cannot disable telemetry** ```yaml telemetry: enabled: false ``` ### Collected Data The following pieces of information are included in the telemetry sent back to us: ```json { "instanceId": "5db51f99-c6fb-4340-9c19-78adce58cc1b", "os": { "type": "Darwin", "release": "20.5.0", "arch": "x64" }, "env": { "nodejs": "v16.19.1", "flowforge": "1.0.0" }, "platform": { "counts": { "users": 6, "teams": 5, "projects": 4, "devices": 9, "projectSnapshots": 16, "projectTemplates": 7, "projectStacks": 2, "libraryEntries": 0, "sharedLibraryEntries": 2 }, "config": { "driver": "localfs", "broker": { "enabled": true }, "fileStore": { "enabled": true }, "email": { "enabled": false } }, "license": { "id": "4c105579-782b-4d53-af62-cf7fa69f6b43", "type": "DEV" } } } ``` | Property | Description | | ----------------------------------- | -------------------------------------------------------------------------------------------- | | `instanceId` | A unique identifier for the FlowFuse instance. | | `os` | Information about the operating system | | `env` | Node.js and FlowFuse versions | | `platform.counts` | A snapshot of the number of users, teams, projects (instances), etc, in use on the platform. | | `platform.config.driver` | Which backend driver is being used | | `platform.config.broker.enabled` | A flag indicating whether the the internal comms broker is enabled | | `platform.config.fileStore.enabled` | A flag indicating whether the file store is enabled | | `platform.config.email.enabled` | A flag indicating whether email is enabled | | `platform.license.id` | The ID of the license loaded | | `platform.license.type` | The type of the license | When the data is collected, we also store the timestamp the data was received and a **hash** of the sending IP address - we do not store the plain value. ### Schedule For the core tracking, the platform will send the telemetry data: - 30 seconds after the platform starts up (but only if the platform has already been initialised) - Once every 24 hours at a time randomly picked when the platform starts The data is sent via an HTTP Post to `https://ping.flowforge.com`. ## Frontend Telemetry The FlowFuse UI can be configured to track usage to help understand how users are navigating the pages. It supports integration with two different services: - [PostHog](https://posthog.com/){rel=""nofollow""} *(recommended)*: You will require your own API key to pass into the `yml`, which will begin the logging of user interactions. - [Sentry](https://sentry.io/){rel=""nofollow""} *(recommended)*: You will need to specify your Sentry DSN for the frontend and back-end - [Plausible](https://plausible.io/){rel=""nofollow""}: *(deprecated since 0.9 and will be removed in the future)*: You can setup your own account, and pass the relevant domain to the `yml` in the telemetry configuration. As this option is deprecated, details of how to configure are no longer provided. ### Configuring Telemetry | Option | Description | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `telemetry.enabled` | Enables the anonymous usage telemetry of the platform. Default: `true` | | `telemetry.backend.sentry.dsn` | The API key provided to you from your own sentry account. Default: `null` | | `telemetry.frontend.posthog.apikey` | The API key provided to you from your own PostHog account. Default: `null` | | `telemetry.frontend.posthog.capture_pageview` | FlowFuse is designed as to provide custom posthog `$pageview` events that provide more detail on navigation than the default, and suit a single page application better. As such, we recommend setting this to false in order to prevent duplicate `pageleave`/`pageview` events firing. Default: `true` | | `telemetry.frontend.sentry.dsn` | The API key provided to you from your own sentry account. Default: `null` | | `telemetry.frontend.sentry.production_mode` | Should this instance be treated as production (lower session count recorded). Default: `false` | ```yaml telemetry: enabled: true frontend: posthog: apikey: capture_pageview: false sentry: dsn: production_mode: true backend: sentry: dsn: prometheus: enabled: true ``` Sentry reads the environment variable `SENTRY_ENV`, falling back to `NODE_ENV` to set the environment for both frontend and backend. #### Telemetry During Build Configure .env with the auth token, org, and project name for the frontend project. ```yaml # Used for BUILD time sentry reporting SENTRY_AUTH_TOKEN= SENTRY_ORG= SENTRY_PROJECT= ``` # User management ## User registration Depending on where FlowFuse is installed, users should or should not be allowed to sign up for the service. User registration can be configured in the admin panel. Go to "Admin Settings" > "Settings" and select or deselect "Allow new users to register on the login screen". ## Creating new users To add new users to the platform go to "Admin Settings" > "Users" and click the "New User" button. Fill out the form, and provide the new user with their password. To require users to change their password the next time they log in, use the "Expire Password" option on the "Edit User" dialog. ## Deleting a user Users can only be removed if they are not the sole owners of any teams. As such they must either delete their teams first or ensure their teams have alternative owners. They can either do this themselves or an Admin do it for them. They can then be removed via the "Edit User" dialog in the Admin view. # FlowFuse Platform API The platform provides a REST API that makes it possible to create integrations and custom workflows. The API comes with an OpenAPI 3.0 Specification that can be viewed [here](https://app.flowfuse.com/api/){rel=""nofollow""}, or on any FlowFuse instance on the path `/api/`. ### Accessing the API To make use of the API you will need a valid Access Token. Tokens can be generated for a user under the Security section of the User Settings page. ![Tokens Settings Page](https://flowfuse.com/docs/api/images/tokens.png) Tokens can be set to have a limited life or unlimited and can be revoked by deleting the token from the list. Tokens with an expiry date will be deleted once they reach that date. Be aware that the token value will only be displayed once, at creation time, there is no way to recover the token after this point. Currently, all routes require a valid token to be included in the request. The tokens are passed using the `Authorization` header as a `Bearer` token. For example, the following will get a list of the token owner's teams: ```text curl -H "Authorization: Bearer ffpat_d4vZlLhCN8muyFUi6UsquLj47H2aTDkDpvxBUf5Ea" \ https://app.flowforge.com/api/v1/user/teams ``` When sending data to the API, requests must set the `Content-Type` header to `application/json`. For example, the following will update the name of the team with an id of `mNYLkklLAG`: ```text curl -X PUT \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ffpat_d4vZlLhCN8muyFUi6UsquLj47H2aTDkDpvxBUf5Ea" \ -d '{"name": "My Development Team"}' \ https://app.flowforge.com/api/v1/teams/mNYLkklLAG ``` # Hardware apps **Hardware apps — start here** 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). ::guide-tabs :::guide-tab{label="Packaged App"} ::::flow-diagram --- edges: - from: snap to: inst label: deploy · sealed accent: slate nodes: - id: snap label: Pipeline snapshot sub: sealed · built once accent: indigo - id: inst label: Remote Instance sub: identical accent: indigo many: true --- :::: A sealed product that ships on a piece of hardware and is identical everywhere — buy it, it runs on its device, nothing to configure. **Use it when** — The app ships with a known partner device and the data it reads is fixed by that hardware. **How it works** — Built and promoted through a pipeline (dev → staging → prod), then deployed to a Remote Instance as a snapshot. Everything is baked in; only fixed env vars vary at deploy. **Major components** - **Pipeline snapshot** — the app built once, promoted to the device - **Remote Instance (edge device)** — FlowFuse-managed Node-RED running the sealed app - **Team Broker (MQTT)** — carries the app's events to subscribers - **FlowFuse Tables** — stores the rows the app writes **Where config & data live** - **Config** — baked into the snapshot; only fixed env vars at deploy, nothing per-site. - **Data** — events to the Team Broker, records to FlowFuse Tables. ::: :::guide-tab{label="Configurable App"} ::::flow-diagram --- edges: - from: snap to: inst label: same build accent: slate - from: cfg to: inst label: loads its own config accent: red dashed: true legend: - line: slate label: same build - line: red dashed: true label: loads its own config nodes: - id: snap label: Pipeline snapshot sub: same build accent: indigo col: 1 row: 1 - id: cfg label: Per-site config sub: tags · broker · site accent: slate col: 1 row: 2 - id: inst label: Remote Instance sub: per site accent: indigo many: true col: 2 row: 1 --- :::: The same shelf product, plus a few knobs — tag names, broker address, site name — that differ per site and live on the Remote Instance. **Use it when** — The flows are the same everywhere but the values they use differ per install and may change over time. **How it works** — Same pipeline delivery to a Remote Instance; the runtime loads a per-site config from a file on the device. Back that file up to the database, so a device swap restores the config — the device is the source of truth, the DB is the safety net. **Major components** - **Pipeline snapshot** — the same build promoted to every device - **Remote Instance (edge device)** — runs the app and loads its own per-site config - **Per-site config (tags · broker · site)** — a file on the device, backed up to the DB - **Team Broker / FlowFuse Tables** — event egress + records **Where config & data live** - **Config** — a per-site file on the device (tags, broker address, site name), backed up to FlowFuse Tables so a swap restores it. - **Data** — Team Broker + FlowFuse Tables. ::: :::guide-tab{label="Edge Building Block"} ::::flow-diagram --- edges: - from: eq to: inst label: reads - from: inst to: broker label: publishes accent: slate nodes: - id: eq label: Equipment sub: signals / PLC accent: slate - id: inst label: Remote Instance sub: edge building block accent: indigo - id: broker label: Team Broker sub: MQTT accent: indigo --- :::: Not a finished app — one hardware-facing block plus example flows, running on a Remote Instance. You assemble everything upstream of it yourself. **Use it when** — The hardware-facing piece is reusable, but everything before it differs so much per site that no finished app would fit. **How it works** — Blocks are published as subflows to the Team Library with an example flow. Consumers drop them onto a Remote Instance and wire them up. **Major components** - **Equipment (signals / PLC)** — the source hardware the block reads - **Remote Instance (edge device)** — runs the edge building block at the line - **Team Broker (MQTT)** — publishes the normalized data upstream - **Team Library** — where the block is published as an installable subflow **Where config & data live** - **Config** — lives in the consuming flow you build around the block (env / context). - **Data** — Team Broker (normalized, upstream). ::: :: # App delivery methods **App delivery methods — start here** 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. ::guide-tabs :::guide-tab{label="Whole app"} **Snapshots & pipelines** — promote a complete, versioned project through dev → staging → prod to every place that runs it. ::::flow-diagram --- edges: - from: golden to: fleet accent: indigo label: snapshot · pipeline nodes: - id: golden label: Dev instance sub: the golden one you build & test accent: indigo - id: fleet label: Instances sub: every place it runs accent: slate many: true --- :::: Take the whole app — every flow, setting and dependency — as a versioned snapshot, then promote that one controlled build through pipeline stages to every place that should run it. **Use it when** — You're shipping a complete application and every site should run the same, controlled version. **How it works** — A pipeline promotes a snapshot dev → staging → production; each target is parameterised by its own env vars, so one controlled build serves every site. **Major components** - **Snapshot** — the whole app, frozen as one versioned build - **Pipeline** — promotes that snapshot through dev → staging → prod - **Dev instance** — where you build and test the project - **Remote / Hosted Instances** — the fleet each snapshot rolls out to **Where config & data live** depends on the kind of app you're shipping — a hardware app tied to a device, or a software app on the platform. See [Hardware apps →](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/) and [Software apps →](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/). **More phases when you need them** — a pipeline isn't limited to two stages. Add the phases your process needs — an extra staging tier, an approval gate, per-region rollouts — each one a controlled promotion of the same golden build: ::::flow-diagram --- edges: - dev>stage - stage>prod nodes: - id: dev label: Dev sub: golden build accent: indigo - id: stage label: Staging - id: prod label: Production sub: every place it runs accent: slate many: true --- :::: ::: :::guide-tab{label="Pieces"} **Subflow export** — publish one piece as a package the team installs, like a shared library. ::::flow-diagram --- edges: - from: sub to: node label: export as - from: node to: inst accent: red dashed: true label: install - from: inst to: bom label: version-tracked legend: - line: red dashed: true label: install - line: neutral label: version-tracked nodes: - id: sub label: Subflow sub: reusable block accent: indigo - id: node label: Custom node sub: installable package accent: slate - id: inst label: Instances sub: install & run the piece accent: indigo many: true - id: bom label: Bill of Materials sub: which version each runs accent: slate --- :::: Package a single piece of a flow — a block of logic or UI — as a reusable subflow, export it as a **custom node** other apps install, and pull it in instead of copying code between projects. **Use it when** — A part of an app should be reused across many apps and upgraded in one place — a shared library, not a whole application. **How it works** — Export the subflow as a **custom node** — an installable package apps pull in like any library dependency. Apps install it and the Bill of Materials tracks every version in use. Share an **example flow** in the Team Library to show how to wire it up. **Major components** - **Subflow** — the one reusable piece you package - **Custom node** — the installable package your subflow is exported to - **Team Library** — example flows the team shares (a custom node can ship with one to show its use) - **Instances** — the apps that install and run the piece - **Bill of Materials** — tracks which version each app runs **Where config & data live** - **Config** — the subflow's instance properties / env where it's installed. - **Distribution** — export once as a custom node; apps install and upgrade from it, like a library. ::: :: ::callout{icon="i-lucide-triangle-alert"} **Dev and prod in the same team?** Every instance on a team reaches the same shared resources — the Team Broker, FlowFuse Tables, project links, any external Postgres. So a dev instance can read and write the very data prod depends on. **Name and namespace resources per environment** so test data and real data never mix: separate broker topic prefixes, table or schema names, and project-link targets, driven by each instance's env vars. [See the Data plane →](https://flowfuse.com/docs/application-guide/data-plane/) :: ::callout{icon="i-lucide-git-branch"} Dev and prod on **separate servers** — dev in IT or the cloud, prod in OT or air-gapped? A **GitHub bridge** carries the same versioned code across the boundary. That's an architecture decision. [See Architectures →](https://flowfuse.com/docs/application-guide/architectures/) :: # Software apps **Software apps — start here** 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). ::guide-tabs :::guide-tab{label="Packaged App"} ::::flow-diagram --- edges: - from: broker to: instance label: subscribes - from: instance to: tables label: writes nodes: - id: broker label: Team Broker sub: MQTT accent: teal - id: instance label: Hosted Instance sub: headless · no UI accent: indigo - id: tables label: FlowFuse Tables sub: writes rows accent: green --- :::: A headless, self-contained job that runs the same everywhere — an MQTT-to-DB connector, a pipeline, a scheduled task. No UI. **Use it when** — A self-contained job with no screen and no per-site settings: connectors, pipelines, scheduled work. **How it works** — Built and promoted through a pipeline; runs headless on a Hosted (or Remote) Instance. Everything is baked into the snapshot; only fixed env vars vary at deploy. **Major components** - **Team Broker (MQTT)** — the event stream the app subscribes to - **Hosted Instance** — FlowFuse-managed Node-RED running the headless app - **Packaged App (no UI)** — the logic itself, no dashboard - **FlowFuse Tables** — where the app writes its rows **Where config & data live** - **Config** — baked into the snapshot; only fixed env vars at deploy. - **Data** — reads the Team Broker, writes FlowFuse Tables. ::: :::guide-tab{label="Data-Driven App"} ::::flow-diagram --- edges: - from: users to: instance label: opens dir: both - from: instance to: tables label: reads · writes dir: both nodes: - id: users label: Users sub: browser accent: slate - id: instance label: Hosted Instance sub: data-driven app accent: indigo - id: tables label: FlowFuse Tables sub: records accent: green --- :::: A user-facing app on a Hosted Instance — a time clock, an asset manager — whose content is driven by data. It needs a backend and a data source to be complete. **Use it when** — Apps whose displayed settings or records change between deployments and grow over time: time clocks, registries, asset managers. **How it works** — Same pipeline delivery to a Hosted Instance; the runtime loads its data from FlowFuse Tables or the Team Broker, served by a backend behind the screen. **Major components** - **Users (browser)** — the people using the app's dashboard - **Hosted Instance** — runs the data-driven app and serves its UI - **FlowFuse Tables** — the records the app reads and writes **Where config & data live** - **Config** — app settings and records live in FlowFuse Tables (or context), editable without redeploying. - **Data** — FlowFuse Tables for records, the Team Broker for live values. ::: :::guide-tab{label="Shared Building Block"} ::::flow-diagram --- edges: - from: block to: library label: publish - from: library to: instances label: embedded in accent: red dashed: true legend: - line: red dashed: true label: embedded in nodes: - id: block label: Shared Building Block sub: reusable subflows accent: indigo - id: library label: Team Library sub: catalogue accent: slate - id: instances label: Hosted Instances sub: embed & upgrade together accent: indigo many: true --- :::: A reusable piece of UI or logic that other apps embed — not an app itself. Think a common dashboard surface many Hosted Instances present through. **Use it when** — Many apps should share one piece of UI or logic and upgrade it in lockstep. **How it works** — Published as subflows to the Team Library with an example flow; updating the subflow updates every Hosted Instance that adopts the new version. **Major components** - **Shared Building Block (reusable subflows)** — the block authored once - **Team Library** — the catalogue it's published to - **Hosted Instances** — the apps that embed it and upgrade together **Where config & data live** - **Config** — via the subflow's instance properties / env where it's embedded. - **Data** — none of its own; it embeds into the host app's data. ::: :: # IIoT architectures ::arch-diagram --- edges: - from: sa to: ria - from: sb to: rib label: reads - from: sc to: ric - from: ria to: broker dashed: true accent: teal - from: rib to: broker label: publish dashed: true accent: teal - from: ric to: broker dashed: true accent: teal - from: broker to: central label: subscribe dashed: true accent: teal groups: - id: central_z label: Central system · one live view of every node accent: indigo nodes: - central - broker - id: sites_z label: Distributed sites · a Remote Instance + a few sensors, each a small job accent: red nodes: - ria - rib - ric - sa - sb - sc legend: - swatch: indigo label: Central - swatch: red label: Edge sites - line: teal dashed: true label: MQTT / UNS - line: neutral label: sensor wire nodes: - id: central label: Central app sub: one live view of every node accent: indigo col: 2 row: 1 - id: broker label: Team Broker sub: MQTT · UNS accent: teal col: 2 row: 2 - id: ria label: Remote Instance sub: Site A accent: slate col: 1 row: 3 - id: rib label: Remote Instance sub: Site B accent: slate col: 2 row: 3 - id: ric label: Remote Instance sub: Site C accent: slate col: 3 row: 3 - id: sa label: Sensors sub: pH · flow · level accent: neutral col: 1 row: 4 - id: sb label: Sensors sub: pH · flow · level accent: neutral col: 2 row: 4 - id: sc label: Sensors sub: pH · flow · level accent: neutral col: 3 row: 4 --- :: The IIoT shape: many distributed Remote Instances, each reading just a few sensors and doing one small job, publish to a central Team Broker. One central app subscribes and sees every node at once. Each node is small — the value is the large-scale live picture they add up to. Think water-quality monitoring across dozens of pump stations. **Use it when** — Lots of small, spread-out measurement points that only pay off when aggregated into one live view. # Architectures Every FlowFuse deployment is the same building blocks — instances, broker, data, edge — arranged for where it runs. Read any diagram as a vertical stack, then pick the world you're designing for. ::callout{icon="i-lucide-arrow-right"} **[OT architectures →](https://flowfuse.com/docs/application-guide/architectures/ot/)** — Near the equipment — edge deployments with the server in IT or in an OT/DMZ, hardware-saving consolidation, and air-gapped sites. :: ::callout{icon="i-lucide-arrow-right"} **[IT architectures →](https://flowfuse.com/docs/application-guide/architectures/it/)** — Hosting and governing — on-prem, your cloud per-site, hosting choice at scale, enterprise governance, and secure data exposure. :: ::callout{icon="i-lucide-arrow-right"} **[IIoT architectures →](https://flowfuse.com/docs/application-guide/architectures/iiot/)** — The live data backbone — a Unified Namespace where edge publishes once and many subscribe, across every site. :: ## Separating dev from prod A modifier for any of the three worlds above. When your development server is a different server from production — dev in IT or the cloud, prod down in OT or behind a tighter network boundary — a GitHub bridge carries the same versioned code across the boundary. (A truly air-gapped site can't pull from GitHub; there, code crosses by offline snapshot import instead.) ### GitHub bridge **Modifier · works across OT, IT & IIoT.** A pipeline pushes your dev work up to a repo; each site's instance pulls it back down. One versioned source of truth — with review, history and rollback in Git — and dev kept safely off the production deployment servers. ::arch-diagram --- edges: - from: dev to: github label: pipeline push dashed: true accent: red - from: github to: siteA label: pull dashed: true accent: red - from: github to: siteB dashed: true accent: red - from: github to: siteC dashed: true accent: red groups: - label: Dev · one build nodes: - dev - label: Sites · prod instances (OT & IT) nodes: - siteA - siteB - siteC legend: - line: red dashed: true label: push / pull nodes: - id: dev label: Dev instance sub: develop once accent: indigo col: 2 row: 1 - id: github label: GitHub sub: versioned source of truth accent: slate col: 2 row: 2 - id: siteA label: Instance sub: Site A · OT accent: indigo col: 1 row: 3 - id: siteB label: Instance sub: Site B · IT accent: indigo col: 2 row: 3 - id: siteC label: Instance sub: Site C · OT accent: indigo col: 3 row: 3 --- :: ::callout{icon="i-lucide-arrow-right"} **[App delivery methods →](https://flowfuse.com/docs/application-guide/app-delivery-methods/)** — Once code is on a server, it ships via snapshots or subflows. :: # IT architectures ::guide-tabs :::guide-tab{label="On-prem IT"} ::::arch-diagram --- edges: - from: users to: plat label: access - from: plat to: i1 - from: plat to: i2 label: hosts - from: plat to: i3 groups: - id: dc label: On-prem IT data center · self-managed accent: green nodes: - plat - i1 - i2 - i3 legend: - swatch: green label: IT zone nodes: - id: users label: IT users sub: dashboards & tools accent: slate col: 2 row: 1 - id: plat label: FlowFuse platform sub: on your own servers accent: indigo col: 2 row: 2 - id: i1 label: Hosted Instance sub: app accent: indigo col: 1 row: 3 - id: i2 label: Hosted Instance sub: app accent: indigo col: 2 row: 3 - id: i3 label: Hosted Instance sub: app accent: indigo col: 3 row: 3 --- :::: The whole FlowFuse platform runs on the company's own servers in their IT data center. It hosts the apps as Hosted Instances and serves them to IT users. Nothing leaves the building unless you choose to connect it. **Use it when** — IT wants to own and run the platform entirely in-house, on their own infrastructure. ::: :::guide-tab{label="Cloud + per-site"} ::::arch-diagram --- edges: - from: plat to: a - from: plat to: b label: deploys - from: plat to: c groups: - id: cloud label: Cloud · your own AWS account accent: blue nodes: - plat - id: sites label: Sites · IT layer — one Remote Instance each accent: green nodes: - a - b - c legend: - swatch: blue label: Cloud - swatch: green label: IT zone nodes: - id: plat label: FlowFuse platform sub: your cloud account accent: indigo col: 2 row: 1 - id: a label: Remote Instance sub: Site A · IT layer accent: slate col: 1 row: 2 - id: b label: Remote Instance sub: Site B · IT layer accent: slate col: 2 row: 2 - id: c label: Remote Instance sub: Site C · IT layer accent: slate col: 3 row: 2 --- :::: The FlowFuse platform runs in the company's own cloud account (e.g. AWS) and deploys and manages a Remote Instance in each site's IT layer via the Device Agent. The cloud platform governs and deploys; each site's instance runs locally and keeps working on its own even if the link drops. **Use it when** — The platform lives in your cloud, but each site needs its own instance in its IT layer. ::: :::guide-tab{label="Scaled-out · hosting choice"} ::::arch-diagram --- edges: [] groups: - id: sites label: Sites · one independent FlowFuse server each, host it where you want accent: green nodes: - s1 - s2 - s3 legend: - swatch: green label: Site nodes: - id: s1 label: FlowFuse server sub: Site A · on-prem accent: indigo col: 1 row: 1 - id: s2 label: FlowFuse server sub: Site B · on-prem accent: indigo col: 2 row: 1 - id: s3 label: FlowFuse server sub: Site C · in the cloud accent: indigo col: 3 row: 1 --- :::: Scale out by running a **FlowFuse server at each site** — host each one where it fits, on-prem or in the cloud. Each site's server is fully independent: its own platform, run and governed on its own. There's no central server above them. **Use it when** — Every site wants its own full, self-contained FlowFuse server, hosted wherever suits it, with nothing central above it. ::: :::guide-tab{label="Enterprise governance"} ::::arch-diagram --- edges: - from: central to: s1 - from: central to: s2 label: dev once · share code down - from: central to: s3 groups: - id: ent label: Corporate · company-wide apps accent: indigo nodes: - central - id: servers label: Sites · apps that run locally accent: green nodes: - s1 - s2 - s3 legend: - swatch: green label: Site server nodes: - id: central label: FlowFuse sub: corporate apps accent: indigo col: 2 row: 1 - id: s1 label: FlowFuse server sub: Site A · local apps accent: indigo col: 1 row: 2 - id: s2 label: FlowFuse server sub: Site B · local apps accent: indigo col: 2 row: 2 - id: s3 label: FlowFuse server sub: Site C · local apps accent: indigo col: 3 row: 2 --- :::: Split where apps live: company-wide apps run on a central corporate FlowFuse, while apps specific to a site run locally on that site's own FlowFuse server. You can still develop in one place and **share code down** to the sites — but because these are separate servers, that travels over the **GitHub bridge or a snapshot export**, not a Pipeline (a Pipeline only promotes within a single platform). **Use it when** — Some apps belong to the whole company and some are site-specific, and you want to build centrally but let each site run its own local apps. ::: :: # OT architectures ::guide-tabs :::guide-tab{label="Edge · server in IT"} ::::arch-diagram --- edges: - from: r1 to: server accent: slate - from: r2 to: server label: manages across the boundary accent: slate - from: r3 to: server accent: slate - from: r1 to: p1 - from: r2 to: p2 label: reads - from: r3 to: p3 groups: - id: it label: IT infrastructure accent: green nodes: - server - id: ot label: OT environment accent: red nodes: - r1 - r2 - r3 - p1 - p2 - p3 legend: - swatch: green label: IT zone - swatch: red label: OT zone - line: slate label: Authenticated - line: neutral label: Local wire nodes: - id: server label: FlowFuse server sub: in IT infrastructure accent: indigo col: 2 row: 1 - id: r1 label: Remote Instance sub: on OT equipment accent: slate col: 1 row: 2 - id: r2 label: Remote Instance sub: on OT equipment accent: slate col: 2 row: 2 - id: r3 label: Remote Instance sub: on OT equipment accent: slate col: 3 row: 2 - id: p1 label: PLCs sub: Line A col: 1 row: 3 - id: p2 label: PLCs sub: Line B col: 2 row: 3 - id: p3 label: PLCs sub: Line C col: 3 row: 3 --- :::: The FlowFuse server lives up in the IT infrastructure; the Remote Instances live down in the OT environment on the equipment. The server deploys to and manages them across the IT/OT boundary, while each Remote Instance keeps running locally if the link drops. **Use it when** — IT owns and hosts the platform, but execution must sit next to the machines in OT. ::: :::guide-tab{label="Edge · server in OT / DMZ"} ::::arch-diagram --- edges: - from: itri to: corp accent: slate - from: itri to: server label: controlled uplink accent: slate - from: r1 to: server accent: slate - from: r2 to: server accent: slate - from: r3 to: server accent: slate - from: r1 to: p1 - from: r2 to: p2 - from: r3 to: p3 groups: - id: it label: IT · corporate network accent: green nodes: - corp - itri - id: dmz label: DMZ · firewall-segregated accent: neutral nodes: - server - id: ot label: OT network accent: red nodes: - r1 - r2 - r3 - p1 - p2 - p3 legend: - swatch: green label: IT zone - swatch: neutral label: DMZ - swatch: red label: OT zone - line: slate label: Authenticated - line: neutral label: Local wire nodes: - id: corp label: Corporate systems sub: MES / ERP / dashboards accent: neutral col: 2 row: 1 - id: itri label: Remote Instance sub: in IT accent: slate col: 2 row: 2 - id: server label: FlowFuse server sub: in the DMZ accent: indigo col: 2 row: 3 - id: r1 label: Remote Instance sub: IPC · Area 1 accent: slate col: 1 row: 4 - id: r2 label: Remote Instance sub: IPC · Area 2 accent: slate col: 2 row: 4 - id: r3 label: Remote Instance sub: embedded · Area 3 accent: slate col: 3 row: 4 - id: p1 label: PLCs col: 1 row: 5 - id: p2 label: PLCs col: 2 row: 5 - id: p3 label: PLCs col: 3 row: 5 --- :::: The FlowFuse server sits inside the plant, firewall-segregated in a DMZ. It reaches corporate systems through a controlled uplink to a Remote Instance up in the IT network, and manages Remote Instances on IPCs and embedded hardware in the OT network below. Nothing reaches OT except through the firewalls. **Use it when** — Security policy keeps the platform inside the plant boundary, exposed only through a DMZ. ::: :::guide-tab{label="Air-gapped"} ::::arch-diagram --- edges: - from: net to: server label: blocked dir: none dashed: true accent: red - from: server to: inst label: deploys accent: slate - from: inst to: plc label: reads groups: - id: ot label: OT network · no internet accent: red nodes: - server - inst - plc legend: - swatch: red label: OT zone - line: slate label: Authenticated - line: neutral label: Local wire - line: red dashed: true label: Blocked nodes: - id: net label: Internet accent: neutral col: 1 row: 1 - id: server label: FlowFuse sub: self-managed · on-site accent: indigo col: 1 row: 2 - id: inst label: Instances sub: site apps accent: indigo col: 1 row: 3 - id: plc label: PLCs sub: equipment col: 1 row: 4 --- :::: The DMZ pattern taken to its extreme: a self-managed FlowFuse runs on a server inside an isolated OT network with no internet at all. It manages that site's instances and devices entirely within the OT boundary — nothing goes in or out. **Use it when** — Site security policy forbids any internet traffic in or out of the OT network. ::: :::guide-tab{label="Edge · hardware-saving"} ::::arch-diagram --- edges: - from: server to: h1 accent: slate - from: server to: h2 label: runs accent: slate - from: server to: h3 accent: slate - from: h1 to: p1 - from: h2 to: p2 label: talks to equipment - from: h3 to: p3 groups: - id: ot label: OT · on-site, close to the equipment accent: red nodes: - server - h1 - h2 - h3 - p1 - p2 - p3 legend: - swatch: red label: OT zone - line: slate label: Authenticated - line: neutral label: Local wire nodes: - id: server label: FlowFuse server sub: on-site · near the line accent: indigo col: 2 row: 1 - id: h1 label: Hosted Instance sub: does Line A's work accent: indigo col: 1 row: 2 - id: h2 label: Hosted Instance sub: does Line B's work accent: indigo col: 2 row: 2 - id: h3 label: Hosted Instance sub: does Line C's work accent: indigo col: 3 row: 2 - id: p1 label: PLCs sub: Line A col: 1 row: 3 - id: p2 label: PLCs sub: Line B col: 2 row: 3 - id: p3 label: PLCs sub: Line C col: 3 row: 3 --- :::: Instead of a Remote Instance on every device, deploy one FlowFuse server close to the line and run several Hosted Instances on it — each doing the work an edge device would have done, talking to its equipment directly. Fewer physical boxes to buy and maintain, same separation of concerns. **Use it when** — You want the edge workloads consolidated onto nearby server hardware to cut device count. ::: :: # Data plane **Data plane — start here** 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. ::guide-tabs :::guide-tab{label="Relational"} **Built in** — ships with every FlowFuse server install; exposed to every instance. ::::flow-diagram --- edges: - from: instances to: tables label: query & update accent: slate groups: - label: Built into every FlowFuse server install accent: green nodes: - tables legend: - line: slate label: Authenticated · every instance reaches it nodes: - id: instances label: Instances sub: App A · B · C accent: indigo many: true - id: tables label: FlowFuse Tables sub: relational accent: green --- :::: A place for records that relate to each other — assets, config, users, orders — that you look up, join and update in place. It's FlowFuse Tables, built into every FlowFuse server install and exposed to every instance on the team. **Use it when** — The data has structure and relationships, and apps across the team should read and write the same store. **How it works** — FlowFuse Tables (managed PostgreSQL) via the Query node; because it ships with the server, any instance on the team reaches it natively over an authenticated connection. **In FlowFuse** - **FlowFuse Tables** — managed PostgreSQL, built into every FlowFuse install - **Query node** — read, join and update from any instance - **Exposed to the whole team automatically** — nothing to stand up - **Also any external Postgres** — same node **Good to know** - **Watch out** — not for high-rate timestamped streams; use the time-series target for those. ::: :::guide-tab{label="Broker / UNS"} **Built in** — ships with every FlowFuse server install; publish once, many subscribe. ::::arch-diagram --- edges: - from: pub to: broker label: publish accent: teal dashed: true - from: broker to: dashboard label: subscribe accent: teal dashed: true - from: broker to: historian accent: teal dashed: true - from: broker to: other accent: teal dashed: true groups: - label: Built into every FlowFuse install accent: teal nodes: - broker legend: - line: teal dashed: true label: MQTT · publish once, many subscribe nodes: - id: dashboard label: Dashboard sub: subscribes accent: blue col: 1 row: 1 - id: historian label: Historian sub: subscribes accent: green col: 2 row: 1 - id: other label: Other app sub: subscribes accent: slate col: 3 row: 1 - id: broker label: Team Broker sub: built in · UNS accent: teal col: 2 row: 2 - id: pub label: Instance sub: publishes accent: indigo col: 2 row: 3 --- :::: A real-time bus, not storage: one instance publishes to a topic, any number subscribe. It's the Team Broker, built into every FlowFuse server install — the backbone of a Unified Namespace. **Use it when** — Live data needs to reach many consumers at once, decoupled, as it happens. **How it works** — The built-in Team Broker with publish / subscribe nodes; because it ships with the server, every instance on the team can publish and subscribe over MQTT. Pair with Tables when you also need to keep history. **In FlowFuse** - **Team Broker** — built into every FlowFuse install, no separate product to stand up - publish / subscribe nodes - **Topic structure** — your Unified Namespace - Exposed to the whole team; pair with Tables for history **Good to know** - **Watch out** — it carries data, it doesn't store it; write to Tables too if you need history. ::: :::guide-tab{label="Time-series"} **Bring your own** — external today; FlowFuse has no built-in time-series DB. ::::flow-diagram --- edges: - from: db to: hosted label: Postgres wire accent: slate - from: hosted to: fleet label: Project Link accent: red dashed: true groups: - label: External · where the readings live accent: slate nodes: - db legend: - line: slate label: Postgres wire - line: red dashed: true label: Project Link · target is a Hosted Instance nodes: - id: db label: Time-series DB sub: Timescale / QuestDB accent: slate - id: hosted label: Hosted Instance sub: connects & fronts it accent: indigo - id: fleet label: Instances sub: query by time accent: slate many: true --- :::: A store built for a steady stream of timestamped readings — sensor data, telemetry, trends — written fast and queried by time. FlowFuse has no built-in time-series database, so you run one and expose it to the fleet. **Use it when** — The data is a continuous stream of timestamped values, written at high rate and queried by time window. **How it works** — Run TimescaleDB, QuestDB or InfluxDB where you want. A Hosted Instance connects to it — Timescale and Quest speak the Postgres wire, so the Query node connects exactly like Tables; InfluxDB connects through its own nodes — and fronts it; other instances reach it over Project Link, which always calls a Hosted Instance, with no inbound ports. **In FlowFuse** - **External** — FlowFuse has no built-in time-series DB today - **TimescaleDB / QuestDB** — speak the Postgres wire; Query node connects like Tables - **Hosted Instance** — connects to it and fronts it for the fleet - **Project Link** — reaches it with no inbound ports (targets a Hosted Instance) **Good to know** - **Watch out** — not part of FlowFuse; you run and expose it. Pair with the Team Broker for live + history. ::: :::guide-tab{label="Bring your own"} **Bring your own** — expose any other store or service to the fleet over Project Link. ::::flow-diagram --- edges: - from: store to: hosted label: connects accent: slate - from: hosted to: fleet label: Project Link accent: red dashed: true groups: - label: Wherever it lives · you run it accent: slate nodes: - store legend: - line: slate label: connects - line: red dashed: true label: Project Link · target is a Hosted Instance nodes: - id: store label: Your store / service sub: SQL · ML · gateway accent: slate - id: hosted label: Hosted Instance sub: connects & fronts it accent: indigo - id: fleet label: Instances sub: queries it accent: slate many: true --- :::: Any other store or service FlowFuse doesn't provide — an existing SQL database, an ML model, a site gateway. You run it where it already lives and expose it to the fleet of managed instances over Project Link, with no inbound ports. **Use it when** — You need to reach a store or service that isn't built in and isn't a time-series DB — an existing database, a model, a gateway. **How it works** — A Hosted Instance connects to the store or service and fronts it; other instances reach it over Project Link — which always targets a Hosted Instance — as a secure API / MCP endpoint, with no inbound ports and no copy into a warehouse. **In FlowFuse** - **Hosted Instance** — connects to the store / service and fronts it - **Project Link** — calls a Hosted Instance (only Hosted Instances are callable targets) - Any SQL database, ML model or gateway - Exposed as a secure API / MCP endpoint, no inbound ports **Good to know** - **Good for** — keeping data and services where they already live and exposing them securely to the fleet. FlowFuse doesn't care what the target is. ::: :: ::callout{icon="i-lucide-git-branch"} **Single service?** Calling one external endpoint from a flow — an HTTP request or webhook to one system — is a Node-RED decision, not a platform data target. [Node-RED guide →](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/) :: # Foundations The foundation to build on: what FlowFuse is, its core pieces, and how code is shared across teams. **FlowFuse is an application platform for building, deploying and managing industrial applications on Node-RED** — across IT, OT and IIoT, from the edge to the cloud, governed from one place. ## The big picture ::arch-diagram --- edges: - from: users to: platform label: access - from: agent to: platform label: managed by groups: - id: platform label: FlowFuse Platform — runs and connects your instances accent: indigo nodes: - hosted - dash - broker - tables - id: agent label: Device Agent — bridges platform to the edge accent: red nodes: - remote nodes: - id: users label: Users sub: operators & teams accent: slate many: true span: 2 col: 2 row: 1 - id: hosted label: Hosted Instance sub: one or many · cloud or your server accent: indigo many: true col: 1 row: 2 - id: dash label: Dashboard sub: live operator UI accent: blue col: 2 row: 2 - id: broker label: Team Broker sub: MQTT message bus accent: teal col: 3 row: 2 - id: tables label: FlowFuse Tables sub: shared SQL database accent: green col: 4 row: 2 - id: remote label: Remote Instance sub: one per device, across sites accent: slate many: true span: 2 col: 2 row: 3 --- :: ## The core pieces - **Single platform** — Manage, secure, and govern everything from one place. - **Instances** — Node-RED runtimes. A **Hosted Instance** runs on FlowFuse-managed infrastructure (cloud or your own server); a **Remote Instance** runs on your own edge hardware via the Device Agent. Same runtime either way — it connects to whatever the job needs (hardware, data, cloud) and can serve its own Dashboard. [What an instance connects to →](https://flowfuse.com/docs/node-red-guide/foundations/) - **Team Broker** — A shared message bus that ties data together across sites. - **Database** — One shared operational data store. - **Dashboards** — Operator-facing UIs for the people who run it. - **Edge & device management** — Deploy and manage across many devices, lines, and plants. ::callout{icon="i-lucide-square-stack"} **Remote Instance** — A Remote Instance lives in both worlds: edge execution down in OT, or an on-prem worker under an IT/cloud platform. :: ::callout{icon="i-lucide-book-open"} **In the FlowFuse docs** — that's the mental model in plain language. For the full glossary — every FlowFuse piece and term (Applications, Instances, Snapshots, Pipelines, Team Broker, Tables, Devices and more) — see the [FlowFuse Concepts documentation](https://flowfuse.com/docs/user/concepts/). :: ## How code gets shared Two ways code moves in FlowFuse: promote a whole app through environments, or compose an app from shared parts. ::callout{icon="i-lucide-arrow-right"} **[App delivery methods →](https://flowfuse.com/docs/application-guide/app-delivery-methods/)** — whole app or reusable pieces: how a build reaches every place that should run it. :: # Building FlowFuse applications Turn an app idea into FlowFuse pieces you can name and say in one sentence. ::callout{icon="i-lucide-flag"} **New to FlowFuse? Start with the [Foundations →](https://flowfuse.com/docs/application-guide/foundations/)** — what FlowFuse is and its core pieces, the grounding for everything in this guide. :: ## Apps ### [App delivery methods](https://flowfuse.com/docs/application-guide/app-delivery-methods/) - **[Whole app](https://flowfuse.com/docs/application-guide/app-delivery-methods/)** — A complete, versioned project promoted through dev, staging and prod — via snapshots & pipelines. - **[Pieces](https://flowfuse.com/docs/application-guide/app-delivery-methods/)** — A reusable piece packaged to the Team Library, installed like a shared library — via subflow export. ### [Hardware apps](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/) - **[Packaged App](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/)** — Sealed product on a Remote Instance, identical on every device. - **[Configurable App](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/)** — Same build on a Remote Instance, tuned by a per-site config file that lives on the device. - **[Edge Building Block](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/)** — A reusable edge block you wire into your own upstream flows. ### [Software apps](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/) - **[Packaged App](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/)** — Headless job on a Hosted or Remote Instance, no screen. - **[Data-Driven App](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/)** — User-facing app on a Hosted Instance, backed by data. - **[Shared Building Block](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/)** — Reusable UI or logic many Hosted Instances embed. ## Architectures ### [Data plane — how data is handled](https://flowfuse.com/docs/application-guide/data-plane/) - **[Built in — Tables](https://flowfuse.com/docs/application-guide/data-plane/)** — Relational records — built into every FlowFuse install, exposed to every instance. - **[Built in — Broker / UNS](https://flowfuse.com/docs/application-guide/data-plane/)** — Team Broker — built into every FlowFuse install; publish once, many subscribe. - **[Time-series](https://flowfuse.com/docs/application-guide/data-plane/)** — External today — no built-in TSDB; run Timescale/Quest and expose it to the fleet. - **[Bring your own](https://flowfuse.com/docs/application-guide/data-plane/)** — Expose any other store or service (SQL, ML, gateway) over Project Link. ### Execution plane — where it runs - **[OT architectures](https://flowfuse.com/docs/application-guide/architectures/ot/)** — Near the equipment — edge with the server in IT or an OT/DMZ, hardware-saving consolidation, and air-gapped sites. - **[IT architectures](https://flowfuse.com/docs/application-guide/architectures/it/)** — Hosting & governing — on-prem, your cloud per-site, hosting choice at scale, and enterprise governance. - **[IIoT architectures](https://flowfuse.com/docs/application-guide/architectures/iiot/)** — The live data backbone — a Unified Namespace where edge publishes once and many subscribe, across every site. ## Examples - **[Worked example](https://flowfuse.com/docs/application-guide/worked-examples/oee/)** — OEE, end to end — the full chain from edge to broker to cloud to history. ::callout{icon="i-lucide-arrow-right"} **Ready to build one?** This guide covers the decisions. [Using FlowFuse](https://flowfuse.com/docs/user/) covers the steps that carry them out — creating instances, running pipelines, registering remote instances. :: # Worked examples A **use case** is a problem to solve — a defined pain, stated in plain business terms: *"track OEE across three lines,"* *"warn the team before a tank runs dry."* The **solution** is an app — and a use case is rarely a single one. Most take **one or more apps working together**. Each worked example takes a real use case and works it end to end — the same way a FlowFuse **Proof of Value** does: name the use case, break it into the apps that solve it, pin down how each app is delivered and what shape it takes, then draw the whole architecture. A solution can be delivered different ways — **built entirely by FlowFuse, built together with your team, or built by you** from these examples. The breakdown is the same either way. ## From use case to apps Every example runs the same four moves: 1. **Name the use case** — the pain in plain business terms, and what "good" looks like. 2. **Break it into apps** — list each app the use case needs and say, in one sentence, what it does. Most use cases are more than one app. 3. **Pin down each app** — for every app, name its **delivery method** (a [whole app](https://flowfuse.com/docs/application-guide/app-delivery-methods/) or a reusable [piece](https://flowfuse.com/docs/application-guide/app-delivery-methods/)) and its **app pattern** ([hardware](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/) or [software](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/) app), where it runs, and why that shape. 4. **Draw the architecture** — place the apps, the [data plane](https://flowfuse.com/docs/application-guide/data-plane/) and the broker into one picture that shows how it ties together, and state the whole [architecture](https://flowfuse.com/docs/application-guide/architectures/) in a sentence. ## The examples ::callout{icon="i-lucide-arrow-right"} **[OEE, end to end →](https://flowfuse.com/docs/application-guide/worked-examples/oee/)** — one use case, two apps and two shared services, from edge to broker to cloud to history. :: More worked examples will land here — each one a use case, broken into its apps and drawn out end to end. # OEE, end to end ## The use case **Track OEE across three production lines.** OEE (Overall Equipment Effectiveness) tells you how much good product a line makes versus its full potential — one live number per line, plus history for trends. **What "good" looks like** — every line shows a live OEE figure the team trusts, and any line's OEE can be charted back over weeks to spot drift. It's not one app. OEE takes **two apps** — one at the edge, one in the cloud — joined by **two shared services** (a broker and a history store). ## The apps ### 1 · OEE - Edge Aggregator **What it does** — Reads the machine signals on a line and publishes the line's state. - **Delivery method** — [whole app](https://flowfuse.com/docs/application-guide/app-delivery-methods/): promoted as a snapshot through a pipeline to a Remote Instance on every line. - **App pattern** — [hardware app · Configurable App](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/): the same build everywhere, with each line's PLC tag names loaded as per-site config. - **Runs on** — a Remote Instance, one per line. - **Why this shape** — the same flows run on every line, right next to the equipment, and keep working if the link drops — but each line's PLC tags differ, so the values are configured per install. Build it once; roll the same version to the whole fleet and point each at its own tags. - **See the flow** — [how OEE - Edge Aggregator is built in Node-RED →](https://flowfuse.com/docs/node-red-guide/worked-examples/oee-edge-aggregator/) ### 2 · OEE - Central Dashboard **What it does** — Subscribes to the line states, computes availability, performance and quality, and presents the live OEE dashboard. - **Delivery method** — [whole app](https://flowfuse.com/docs/application-guide/app-delivery-methods/): promoted dev → staging → prod as one versioned build. - **App pattern** — [software app · Data-Driven App](https://flowfuse.com/docs/application-guide/app-delivery-methods/software-apps/). - **Runs on** — a Hosted Instance in the cloud. - **Why this shape** — it's a user-facing app driven by live data, with no hardware of its own; one instance serves the whole plant. - **See the flow** — [how OEE - Central Dashboard is built in Node-RED →](https://flowfuse.com/docs/node-red-guide/worked-examples/oee-central-dashboard/) ### Shared services - **Team Broker (MQTT · UNS)** — carries line state from edge to cloud. The edge publishes to a topic; the dashboard subscribes. Neither references the other. - **External time-series DB (Timescale / QuestDB)** — FlowFuse has no built-in time-series store, so history goes to an external DB over the Postgres wire protocol — a second egress, because the data is a timestamped stream. ## The full architecture ::arch-diagram --- edges: - from: lineA to: broker dashed: true accent: teal - from: lineB to: broker label: publish state dashed: true accent: teal - from: lineC to: broker dashed: true accent: teal - from: broker to: hosted label: subscribe dashed: true accent: teal - from: hosted to: tsdb label: writes history · Postgres wire accent: slate groups: - id: cloud label: Cloud · platform — the app + history accent: blue nodes: - hosted - tsdb - id: edge label: OT · edge — one Remote Instance per line, all the same Edge Aggregator accent: red nodes: - lineA - lineB - lineC legend: - swatch: red label: OT edge - swatch: blue label: Cloud - line: teal dashed: true label: MQTT / UNS - line: slate label: Postgres wire nodes: - id: hosted label: Hosted Instance sub: OEE · Central Dashboard accent: indigo col: 2 row: 1 - id: tsdb label: Time-series DB sub: external · history accent: slate col: 3 row: 1 - id: broker label: Team Broker sub: MQTT · UNS accent: teal col: 2 row: 2 - id: lineA label: Remote Instance sub: Line A · Edge Aggregator accent: slate col: 1 row: 3 - id: lineB label: Remote Instance sub: Line B · Edge Aggregator accent: slate col: 2 row: 3 - id: lineC label: Remote Instance sub: Line C · Edge Aggregator accent: slate col: 3 row: 3 --- :: ::callout{icon="i-lucide-quote"} **The architecture, in one sentence** — OEE is the **Edge Aggregator** (a hardware Configurable App, on a Remote Instance per line) publishing machine state over the Team Broker to the **Central Dashboard** (a software Data-Driven App, on a Hosted Instance), which computes and displays OEE and writes history to an external time-series DB. :: # Billing ## Payment Methods We will accept payments via credit or debit card only using Stripe as our payment provider. All payments are processed in US Dollars. ## Team Billing Each team has its own billing subscription that includes charges for the Node-RED instances and Devices owned by the team. ## Billing Cycle If you've signed up to FlowFuse Cloud without a yearly contract agreed upon with our sales team, your FlowFuse teams are billed monthly on the anniversary of the team creation. You will receive one bill for each team. Hosted and Remote Instances which are added beyond the included amount are pro-rated charges on the current billing cycle and invoiced at the end of the cycle. ## Removing Instances When an instance is deleted your account will receive pro-rated credit for the time remaining in the billing cycle. ## Suspended Instances Suspended Node-RED instances have no running editor, nor a runtime. You are not charged for suspended instances. When an instance is suspended your account will receive pro-rated credit for the time remaining in the billing cycle. When an instance is restarted it will be charged for the remaining time in the billing cycle. ## Managing Billing Details Click on "Billing" followed by "Stripe Customer Portal" to get a summary of the team's current subscription. You'll be redirected to a Stripe customer portal where you can update customer details as: The credit card on file and the billing information. ## Failed Payments If your payment fails for any reason you will receive a notification to the billing email address, you may need to login and update the card on file. Stripe will retry the payment several times over a number of days. If the card repeatedly fails your Node-RED instances will be suspended and a banner will be displayed at the top of the page for all users. An admin will need to update your card details to be able to create new instances or restart them. ## Cancelling your subscription To cancel your subscription you can either delete or suspend your team; both options are available under the Team Settings page. Deleting the team will remove all of the team's instances and devices - they cannot be recovered after being deleted. Suspending the team will stop all of your team's instances and devices and cancel your subscription so no further charges are made. You will not be able to do anything more with the team whilst it is suspended. You can unsuspend the team in the future by setting up a new payment subscription. When deleting the team, if you have outstanding credit you can request a refund via a [support ticket](https://flowfuse.com/support/), include the Team ID in your email. # FlowFuse Cloud FlowFuse Cloud is a hosted service allowing users to sign-up and start creating Node-RED instances without having to install and manage their own instance of FlowFuse. The [Concepts](https://flowfuse.com/docs/user/concepts) remain the same, but we run the platform for you. ## 30-day Free Trial When users sign-up to FlowFuse Cloud they get a 30-day free trial of the platform. This is a great way to start using FlowFuse and discover a lot of the value it provides. Users can end their trial by heading to the Billing page of their team and setting up their payment information. This includes the option to pick which plan you want to upgrade the team to. Otherwise, at the end of the 30-day trial period, any instances created in the team will be suspended. This means they will no longer be running and the editor will not be accessible. Users will need to add their Billing details at which point they will be able to restart their suspended Node-RED instances. We will email users about their trial when it is nearing the end to ensure they know what is happening. ## Billing Customers are billed at the team level for each Node-RED instance they create. This is a recurring monthly charge. See the [Billing](https://flowfuse.com/docs/cloud/billing) page for more detailed answers about billing. ## Support Premium customers can get support by [filing a ticket](https://flowfuse.com/support). We offer support for the FlowFuse application and your account, any issues relating to Node-RED such as your flows or a 3rd party node should be raised in the [community forum](https://community.FlowFuse.com){rel=""nofollow""}. ### Requesting a new verification email When a user signs up for FlowFuse Cloud an email will be sent to verify it. If this email doesn't get delivered one can be resend by signing in to FlowFuse and click the button to resend it. ## Team Types FlowFuse Cloud offers a Trial and an Enterprise Team Type, aimed at different sorts of users ### Enterprise Enterprise includes: - HA for Instances - SSO - Better Support SLA - MQTT Broker Includes 10 instances and 20 MQTT Clients in the base price ### Changing Team Type You can change Team Type by selecting the "Team Settings" option from the left hand menu, then clicking on the "Change Team Type" button ![Change Team Type](https://flowfuse.com/docs/cloud/images/change-team-type.png) From here you will be presented with a choice of Team Types. You will not be able to downgrade to a lower Team Type if you already have more resources than allowed at that level. Please Suspend or Delete any no longer required Instances or Devices. ![Available Team Types](https://flowfuse.com/docs/cloud/images/availble-team-types.png) ## Node-RED on FlowFuse Cloud FlowFuse currently offers Node-RED 4.x, 3.x and 2.x to customers. When creating a new instance a [stack](https://flowfuse.com/docs/user/concepts#stack) is chosen, which later can be [upgraded to a later version](https://flowfuse.com/docs/user/changestack). Each Node-RED can install custom modules as advertised in the [Flow Library](https://flows.nodered.org){rel=""nofollow""}. Note that some modules have dependencies on system libraries or other components that are not available within the FlowFuse container images we use. Those modules cannot be used within FlowFuse Cloud. If you have a particular requirement, please do [contact us](https://flowfuse.com/contact-us/){rel=""nofollow""} so we can discuss what options may be available. ## Cloud Instance Sizes The different sizes of Cloud Instances relate to how much memory and CPU is made available to them. Memory tends to be the main limiting factor for a Node-RED instance. The following table shows what is currently allocated by instance size. | Size | Memory (RAM) | | ------ | ------------ | | Small | 256MB | | Medium | 768MB | | Large | 3840MB | Medium and Large instance types require the Enterprise tier. ## Use of the File System FlowFuse Cloud hosted instances have access to a persistent file-system that will retain the files stored on it across restarts of the instance. A quota limit is applied to how much data can be stored, based on the Team type. Enterprise teams have a file storage quota of 100GB per instance. Files can be manually uploaded to an instance using the [Static Asset Service](https://flowfuse.com/blog/2024/08/flowfuse-2-8-release/#static-assets-service){rel=""nofollow""}. ## Node-RED Context Node-RED Context can be used to store small pieces of application state within the runtime. By default, this is stored in memory only. FlowFuse Cloud provides an optional context store that can be used to persist the data. The amount of data that can be stored in context is determined by the Team type. Enterprise teams have a context store quota of 1GB per instance. ## Network Connections ### HTTP(S) & Websockets Node-RED exposes an HTTPS interface on port 443 with each instance having its own hostname (`example.flowfuse.cloud`). Plain HTTP requests to port 80 will receive a redirect to HTTPS on port 443. You MUST connect using the hostname not the IP address to reach your Node-RED instance. Websocket connections over SSL (`wss:`) are also supported. The payload size per request is limited to 5MB, which is the Node-RED default. When a request exceeds this limit, the whole request is rejected with a `413 Payload Too Large` error. ### TCP and UDP The default TCP and UDP nodes have been removed from the Node-RED palette. This is because it is not possible to route these sorts of connections to the container running Node-RED inside the FlowFuse Cloud platform. ### MQTT MQTT Connections to an external broker using the standard MQTT nodes will work fine as the connection is initiated by Node-RED. FlowFuse provides an MQTT broker for Enterprise Node-RED instances. See the following section. Also the Project Nodes can be used to easily pass messages between Node-RED instances running in the platform. #### Enterprise Team Broker Enterprise teams come with their own MQTT broker. You can provision clients from the broker tab in the left hand menu. Enterprise level Teams can register up to 20 clients as part of their plan. The ability to purchase additional packs of clients will come in a near future release. The broker is available on `broker.flowfuse.cloud` and supports the following connection types: - MQTT on port `1883` - MQTT over TLS on port `8883` - MQTT over secure WebSockets on port `443` When creating clients you can specify a username, but it will prepended to the the Team's id e.g. `alice` will become `alice@32E4NEO5pY`. Clients must also use the username as the MQTT Client ID in order to connect. ![Create Broker Client](https://flowfuse.com/docs/cloud/images/create-broker-client.png) e.g. ```text mosquitto_sub -u "alice@32E4NEO5pY" -i "alice@32E4NEO5pY" -P "password" -h broker.flowfuse.cloud -t "#" ``` Or in Node-RED as follows ![Node-RED MQTT Client Connection](https://flowfuse.com/docs/cloud/images/node-red-mqtt-connection.png) ![Node-RED MQTT Client Security](https://flowfuse.com/docs/cloud/images/node-red-mqtt-security.png) ### IP Addresses Outbound connections from FlowFuse will always come from the IP address `63.33.85.112`. This can make access to a remote database or corporate network possible where those systems are protected by IP address filtering firewalls. All incoming connections MUST use the hostname and not an IP address. ## Data Security FlowFuse Cloud is hosted on Amazon Web Services. The following statements apply to data handling within the platform: - The underlying database use the industry standard AES-256 encryption algorithm to encrypt the stored data. - Persistent storage offered to Node-RED instances uses AES-256 encryption algorithm to encrypt data and metadata at rest. - The network load balancer uses the latest recommended AWS Network Security policy. This enforces TLS1.2 as a minimum. ## Single-Sign On FlowFuse supports configuring both SAML and LDAP based Single Sign-On for particular email domains. This can be configured on request for FlowFuse Cloud by submitting a support request via our [Contact Us](https://flowfuse.com/contact-us/){rel=""nofollow""} page. For SAML providers, you must have the ability to configure a SAML endpoint on your Identity Provider, and have the authority to configure SSO for your email domain. We have currently validated our SSO support with the following Identity Providers: - Microsoft Entra - Google Workspace - OneLogin - Keycloak If you are using a different Identity Provider, please still get in touch, and we can evaluate what will be required to enable it. ## Custom Hostnames FlowFuse Cloud can support custom hostnames for instances in Enterprise teams. This allows you to point your own subdomain, such as `dashboard.example.com` at one of your instances. See [Custom Hostnames](https://flowfuse.com/docs/user/custom-hostnames) for more information. ## Removing your account Before you can delete your account, teams you own must either be deleted or have at least 1 other owner. Once this is done, you can remove your account by going to the "User Settings" page and clicking the "Delete Account" button. See also: [cancelling your subscription](https://flowfuse.com/docs/cloud/billing#cancelling-your-subscription). # Adding Template Settings Within FlowFuse, each Node-RED instance is created from a Template. The Template defines a set of preconfigured options for the instance. This includes runtime settings - values that you would normally expect to set in your Node-RED settings.js file. The Template also defines which of those options can be customised by individual instances. This guide explains how to add a new Node-RED runtime option to the Template object so that it can be customised and passed through to the underlying Node-RED settings.js file. This is reasonably straightforward for simple boolean/string/numeric types. For other types (objects/arrays) it gets more complicated and we don't currently have good examples to follow. For the 'simple' cases, the steps are: 1. Update the Frontend 1. Pick a name for the setting, add it to the list of known settings and any validation logic that is needed 2. Add it to the appropriate Template section 2. Update the runtime 1. Add it to the known list of settings and any additional validation logic 3. Update the Launcher 1. Update the template used to generate settings.js with the new property ## 1. Updating the Frontend There are a set of views in the frontend used to present and edit templates. They get used in two different ways: 1. When creating/editing a template. All options are available and there is a dropdown that lets the user set the policy on the setting (which controls whether an instance is allowed to override the setting) 2. When editing instance settings. All options are shown, but only lets the user modify those that the template policy allows to be changed. This reuse of the views saves a lot of code duplication, at the cost of some complication in implementing it. ### 1.1 - Pick a name for the setting The name should try to match its corresponding property in the Node-RED settings.js file. Some thought should be made as to organisation of the properties. 1. Edit `frontend/src/pages/admin/Template/utils.js` 2. Add the new property name to the `templateFields` and `defaultTemplateValues` objects. Notice the names are flat strings with `_` used as a hierarchy separator... that will make more sense if you're looking at the file. 3. If the value needs any sort of validation, add it to `templateValidators` in the same file. ### 1.2 - Add it to the appropriate Template section Currently there are: - Editor - Palette - Environment - a special case that is unlikely to get other options added These each lives in their own file under `frontend/src/pages/admin/Template/sections/`. Pick an existing setting that most closely matches the setting you want to add (ie checkbox or text input), and copy its entry to the appropriate place. Make sure you update *all* of the references of the copied property name to your new property name. ## 2. Updating the Runtime ### 2.1 - Adding it to the known list 1. Edit `forge/db/controllers/ProjectTemplate.js` to add the new property to the list of known settings. 2. In that same file, update the `validateSettings` function with any additional validation or data cleansing needed. ## 3. Update nr-launcher In the `flowforge-nr-launcher` repo... 1. Edit `lib/runtimeSettings.js` to include the new setting in the generate settings.js file. Note that you must handle the case where the new setting is not present - either by applying a sensible default, or omitting the value. # HTTP API of the FlowFuse platform ## API documentation All public API routes should include a `schema` as part of their definition. This serves a number of purposes: - It will be included in the auto-generated OpenAPI 3.0 spec - Fastify will validate requests include any required properties - Fastify will ensure the response object matches the defined schema Some general guidance: - Ensure the routes have an appropriate tag set - this determines where in the Swagger UI it gets displayed. - Ensure the tag is listed in `forge/routes/api-docs.js` so it appears in the right place - We define view schemas under `forge/db/views/*` alongside the code that generates the view. Keep the naming consistent with other views. - Learn from the existing schemas - be consistent in style. References: - [Fastify Validation and Serialization](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization){rel=""nofollow""} - [OpenAPI 3.0 spec](https://swagger.io/specification/){rel=""nofollow""} ## Object Ids Most database models have a primary key of an auto-incrementing integer stored in the `id` column. These ids are internal properties of the models and should *not* be exposed via the API. This is because they can be guessed and leak information about how many instances of any particular model exist. All model instances have an auto-generated `hashid` property that is an encoded version of the `id` property. This property should be used on the API but aliased as the `id` property. Each database model has a pair of helper functions to encode and decode hashids to/from the true `id` value: ```js const encodedHashid = app.db.models.User.encodeHashid(123); const objectId = app.db.models.User.decodeHashid("edjEbo2K1w") ``` ## API Path design ### Admin routes All admin-only routes exist under: ```txt /api/v1/admin/... ``` ### Logged-in user routes All routes relating to the logged-in user exist under: ```txt /api/v1/user/... ``` ### Object collection routes API routes that are related to objects in collections follow the pattern: ```txt /api/v1///... ``` ## Implementing routes All API routes exist under `forge/routes/api` in the repository, grouped into files based on the entity/functionality the api is related to. All requests will have already been validated to ensure they are being made with a valid session for a logged in user, or an access token. If there is a valid session, `request.session.User` will be the requesting user. ### Opening a route to anonymous users In rare cases, a route needs to be accessible to anonymous users. To by-pass the built-in preHandler, you can set `allowAnonymous` on the routes `config` object: ```js app.get('/', { config: { allowAnonymous: true } }, async (request, reply) => { ... }) ``` ### Team routes Routes under `forge/routes/api/team.js` that relate to a specific team, must use `:teamId` as the placeholder in the route. A request preHandler will use that to ensure the requesting user has permission to access the instance. The following properties will then be available on the `request`: - `request.team` - the team - `request.teamMembership` - the requesting user's role on the team ### Project routes **Note:** In FlowForge 1.5 we started to replace the Project concept with that of Application and Instance. No changes have been made to the underlying APIs - that will be evaluated as part of 1.6. Routes under `forge/routes/api/project.js` that relate to a specific instance, must use `:projectId` as the placeholder in the route. A request preHandler will use that to ensure the requesting user has permission to access the instance. The following properties will then be available on the `request`: - `request.project` - the instance (previously called "project") - `request.teamMembership` - the requesting user's role on the team that owns the instance ### User/role permissions Permissions are defined in `forge/routes/auth/permissions.js`. Each permission specifies the role a user must have to have that permission. If a route requires a particular permission, it can use `app.needsPermission` to generate a preHandler function that will ensure any requesting user has that permission. For example, to add a user to a team, the requesting user needs to have `"team:user:add"`: ```js app.post('/', { preHandler: app.needsPermission("team:user:add") }, async (request, reply) => { // This is defined under forge/routes/api/teamMembers.js - and is mounted // under the path `/api/v1/teams/:teamId/members/` // Due to the team route preHandler, this means `request.teamMembership` // will be defined and will be used by the `needsPermission` handler }); ``` ## Error formats If a route needs to return an error it should respond with a payload in the format: ```js { code: 'error_code', error: 'Human-readable message' } ``` The `code` property should be a well-defined string that can be used to programmatically identify the error without relying on the human-readable message. There is a set of predefined codes that should be used where appropriate: - `unauthorized` - `invalid_request` - `unexpected_error` If the error is related to an invalid option/parameter/object selection, then the code should be: - `invalid_` For example: `invalid_project_name`. ## Pagination & Search All routes that return collections of things must use pagination to allow for future growth, and provide a way to search the collection. ### Pagination We use a cursor-based approach to our pagination. A cursor is a pointer to an entity in the collection and provides the starting point for what should be returned. - End-points accept `cursor` and `limit` parameters - If `cursor` is not provided, it returns from the beginning of the collection. For some end-points, this will mean returning the most recent entries - the `/api/v1/projects/:id/logs` for example. - Each end-point should have a sensible default for the `limit` parameter The response object for paginated end-points should have the format: ```js { "meta": { "next_cursor": "16416724188790000", }, "": [ ], "count": 123 } ``` The `meta` property contains information to help the client navigate the collection. - `next_cursor` - if set, provides the cursor to use to get the next page of results - `previous_cursor` - if set, provides the cursor to use to get the previous page of results. Note that not all end-points need to be navigable in both directions so may never return `previous_cursor`. The `` property should be called the appropriate plural form of what is being returned, such as `projects`. The `count` property is optional and indicates the total number of objects in the collection. End-points *should* include it if the number is known and is of material use. It is *not* returned by end-points used to query logs etc as the total count is a constantly changing value. #### Cursor design A cursor should be able to point directly at an entity in the collection. It could be the entity hashid, or a timestamp if it is a time-series collection such as the logs. The cursor should *not* be the internal `id` of the entity in the collection - as we do not expose those ids on the api, use the `hashid` value as the external `id`. If an end-point supports navigating the collection in reverse (by returning `previous_cursor`), the cursor should be prefixed with `-` to indicate the query should be in the opposite direction to the collection's natural order. ### Search & Filtering Search is done as simple case-insensitive text-based queries against string columns in the database model. This is a crude but effective way to implement search but may need a more comprehensive approach in the future. The search value is provided via the `query` query parameter. ```text /api/v1/example?query=something ``` Filtering can be used to limit the results based on the values of specific columns. The Filter value is provided via additional query parameters. ```text /api/v1/example?eventName=foo ``` ### Implementing pagination & search Two utility functions are provided to help implement pagination and search. #### `app.getPaginationOptions` This returns an object of pagination options for a given request, with any default values automatically applied: ```js const paginationOptions = app.getPaginationOptions(request, {limit: 1000}) // paginationOptions.limit = how many results to return // paginationOptions.cursor = starting cursor // paginationOptions.query = search string to use // paginationOptions.* = any remaining query parameters ``` #### `buildPaginationSearchClause` This takes the pagination options along with model-specific configuration options to build a suitable `where` clause that can be passed to the database model's `getAll` function. ```text const where = buildPaginationSearchClause(params, whereClause, searchColumns, filterMap) ``` - `params` - the pagination object returned by `app.getPaginationOptions` for a given request - `whereClause` - (optional) an object containing any additional query clauses that should be applied - `searchColumns` - (optional) an array of fully-qualified column names that should be searched when `query=` is included in query - `filterMap` - (option) a map of filter name to fully-qualified column name that are valid filter parameters With the filtering option, if a particular filter parameter is specified more than once in the query string, the generated query will apply an Or between the values. For example, lets consider the imaginary `Thing` model has a `name`, `description` and `type`. The following will mean: - any `query=` parameter will be searched for in the `name` and `description` columns - any `type=` parameter will filter on the `type` column. ```js const { buildPaginationSearchClause } = require('../utils') ... getAll: async (pagination = {}, where = {}) => { // Ensure a sensible default limit for this particular type of thing const limit = parseInt(pagination.limit) || 1000 // Decode the cursor from hashid to database id if (pagination.cursor) { pagination.cursor = M.Thing.decodeHashid(pagination.cursor) } // Build the full where query using the buildPaginationSearchClause utility. // We pass in the list of columns that should be searched against where = buildPaginationSearchClause( pagination, where, ['Thing.name', 'Thing.description'], { type: 'Thing.type' } ) // Run the query const { count, rows } = await this.findAndCountAll({ where, order: [['id', 'ASC']], limit }) // Return the results with the additional metadata return { meta: { next_cursor: rows.length === limit ? rows[rows.length - 1].hashid : undefined }, count, things: rows } } ``` # FlowFuse Architecture A FlowFuse install is made up of 2 main components - The Management Application - The Node-RED instances These can be deployed in one of 2 ways - On a single machine :br![LocalFS Architecture](https://flowfuse.com/docs/contribute/images/ff-localfs.png) - Using a Container Orchestration platform (Kubernetes/Docker Compose) :br![Container Architecture](https://flowfuse.com/docs/contribute/images/ff-containers.png) ## FlowFuse Management Application This provides the interface for managing the objects in the platform. It also provides a collection of APIs to support the Node-RED instances once started. A key component is the Container API driver, this is the part that actually creates/destroys Node-RED instances and keeps track of what should be running and restarts if needed. ### Container Drivers Node-RED instances are started by the FlowFuse Management Application via one of the following Container Drivers. Documentation for the Container Driver API will be available in the [API](https://flowfuse.com/docs/api) section. #### Localfs This driver runs Node-RED as separate processes on the same machine as the FlowFuse Management Application. Each instance gets its own `userDir` and a dedicated TCP/IP port to listen to. State is stored in a local SQLite database There is no automatic Ingres automation provided by this driver. #### Kubernetes This driver runs Node-RED in separate containers and each instance is accessed by a dedicated hostname via an HTTP Ingres proxy. The state is stored in a provided PostgreSQL database. Node-RED containers are segregated into their own namespace (currently hardcoded to `flowforge`) The driver uses the [@kubernetes/client-node](https://www.npmjs.com/package/@kubernetes/client-node){rel=""nofollow""} to interact with the cluster. The driver will create the required Service and Ingres Kubernetes resources to expose each instance via whatever Ingress Controller the underlying Kubernetes cluster provides. #### Docker-Compose This driver runs Node-RED in separate containers and each instance is accessed by a dedicated hostname via an HTTP Ingres proxy. The state is stored in a provided PostgreSQL database. The driver uses the [dockerode](https://www.npmjs.com/package/dockerode){rel=""nofollow""} to interact with the cluster. The driver will add the required Environment variables to each Node-RED container to work with the [jwilder/nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy){rel=""nofollow""} NGINX proxy. ## FlowFuse Instances A FlowFuse Node-RED Instance is made up of 2 processes - The FlowFuse Launcher - A Node-RED instance ![Project Architecture](https://flowfuse.com/docs/contribute/images/ff-project-arch.png) ### FlowFuse Launcher This is a small application that handles downloading the Instance specific settings, building a `settings.js` from those settings and then starting the Node-RED instance. The launcher presents an HTTP API (it defaults to the Node-RED port + 1000) that allows the FlowFuse Management Application to start/stop/restart the Node-RED instance as well as query its current state and retrieve the console logs. The launcher can be found [here](https://github.com/FlowFuse/nr-launcher){rel=""nofollow""} Within the launcher are some custom plugins that are loaded by Node-RED: #### nr-storage This plugin is used to save flows, settings, sessions, and library entries back to the FlowFuse Management Application. #### nr-auth This plugin is used to authenticate users trying to access the Node-RED Editor, it refers back to the FlowFuse Management Application to ensure only members of the team that owns the instance can log in. This plugin uses the Node-RED [Authentication API](https://nodered.org/docs/user-guide/runtime/securing-node-red#custom-user-authentication){rel=""nofollow""} #### nr-audit-logger This plugin sends Node-RED Audit events (e.g. user log in and flow deployment events) back to the to the FlowFuse Management Application to allow a reliable audit of what actions have taken place in the instance. This plugin uses the Node-RED [Logging API](https://nodered.org/docs/user-guide/runtime/logging){rel=""nofollow""} ## Component Overview ```mermaid erDiagram USER ||--o{ NGINX : Requests NGINX { Protocol HTTP-TLS Port default-443 } FORGE-APP { Protocol HTTP-TLS Port default-3000 } POSTGRESQL { Protocol tcp-tls Port default-5432 } MOSQUITTO { Protocol HTTP-TLS-WSS-MQTT Port default-1883 Port websocket-1884 } FLOWFORGE-FILE-SERVER { Protocol HTTP-TLS Port default-3001 } NGINX }o--o| NODE-RED : routes NGINX }o--o{ FORGE-APP: routes NGINX ||--|| MOSQUITTO: mqtt-ws FORGE-APP ||--|{ POSTGRESQL: query FORGE-APP ||--|| NODE-RED: "flow update" NODE-RED }o--o{ FLOWFORGE-FILE-SERVER: "Blob store" FLOWFORGE-FILE-SERVER ||--|| FORGE-APP: "Authenticate" NODE-RED { Protocol HTTP-TLS Port default-1880 } NODE-RED }o--|| MOSQUITTO: mqtt FORGE-APP }o--|| MOSQUITTO: mqtt NODE-RED-DEVICES { Port default-1880 Protocol User-Defined } NODE-RED-DEVICES }o--|| NGINX: mqtt-ws USER ||--|| NODE-RED-DEVICES: Requests ``` # Creating Debug Stack Containers Sometimes we want to be able to run some debug code within a stack running in our staging test environment. For example, changes to the `nr-launcher` component or any of the other components that run within the stack. This guide shows a simple way to do that without having to rebuild the container from scratch each time. This will require: 1. Docker 2. A container registry you can push images to. For example, a free DockerHub account where you can push images ### Creating a debug container Pick an existing container image to use as your starting point. For example, `flowfuse/node-red:2.5.0-4.0.x` contains `nr-launcher@2.5.0` and the latest Node-RED 4.x release. **Note**: our staging environments requires `arm64` based containers. The following instructions work for Macs (with M1/M2) - additional steps may be needed for other OS; please contribute them if you know them. Use the following command to open a shell into the container: ```text docker run -it --entrypoint /bin/bash flowfuse/node-red:2.5.0-4.0.x ``` The prompt will then look like this: ```text e8dcd669ea4c:/usr/src/flowforge-nr-launcher$ ``` Take a note of the `e8dcd669ea4c` - this is the id of the container instance you have created. The following directories are probably of interest: - `/usr/src/flowforge-nr-launcher` - contains the `nr-launcher` code and its dependencies - `/usr/src/node-red` - contains the `node-red` code Using `vi`, you can edit the files to make the changes you want and when you're done, exit the shell. Having made the changes, use `docker commit` to create a new container image. The command requires the id of the container you've just edited and the tag for the container. It also needs to restore the `entrypoint` configuration back to the default. ```text docker commit \ --change='ENTRYPOINT ["./node_modules/.bin/flowfuse-node-red", "-p", "2880", "-n", "/usr/src/node-red"]' \ e8dcd669ea4c \ knolleary/ff-debug:debug-1 ``` Finally, you can push the new container to your container registry. ```text docker push knolleary/ff-debug:debug-1 ``` ### Using the container in FlowFuse Debug containers should *only* be used in pre-staging/staging environments. Do *not* add to production. Once pushed, you can create a custom stack in the FlowFuse admin section and give it the location of your container. ### Iterating If you find you want to add some more debug, repeat the process, however use your existing image as the starting point: ```text docker run -it --entrypoint /bin/bash knolleary/ff-debug:debug-1 ``` Be sure to increment the number in the image name (`debug-2`) when you commit and push the new container. ### Tidying up Remember to delete your instances/stacks once you're done, as well as all of the local docker images and containers that have been created along the way. # Database Migrations Any changes made to the database models must include migrations that can modify the database state from one state to another. Whilst we use Sequelize as our ORM layer, we do not use the migration tooling it provides - we have our own. ## Creating migrations ### Filename A migration is provided as JavaScript in the directory `forge/db/migrations`. Its file name must follow the pattern: ```text YYYYMMDD-nn-description.js ``` - `YYYYMMDD` - the date the migration is added - `nn` - a two digit number - `description` - a name for the migration For example `20220204-01-add-billing.js`. This ensures the migrations have a natural order to be applied. The `nn` part of the name allows multiple migrations to be added on the same day but is kept in the right order. ### Structure The migration code should use the following layout: ```js module.exports = { up: async (context) => { // Apply the migration }, down: async (context) => { // Remove the migration } } ``` The `up` function applies to the migration. This can be to create new tables, add columns to existing ones - whatever is needed. The `down` function reverses the migration. It should restore the database back to how it was prior to the migration. The `context` argument is an instance of [Sequelize.QueryInterface](https://sequelize.org/docs/v6/other-topics/query-interface/){rel=""nofollow""} that can be used to perform operations on the database. ## Applying migrations Migrations are applied automatically at the start of the FlowFuse application. Down migrations are not yet supported. ## Considerations when writing migrations Whilst migrations give us the ability to make changes to the database, they must be used with great care. A failing migration will prevent the platform from starting and may require manual intervention. Everything should be done to avoid that from happening. Certain types of migration need particular guidance and care over. ### Adding constraints If a migration is adding a new constraint to an existing table, you need to consider very carefully what impact that could have on an existing system with real data. For example, adding a new 'unique' constraint where you cannot guarantee that constraint hasn't already been broken. What strategy will you use to guard against that or to help recover from it? What additional testing is needed for the migration to verify its behavior in those situations? The preferred method to add a new unique constraint is by adding a new index to the database. This is because SQLite doesn't provide a way to alter columns that doesn't involve dropping the whole table and triggering any cascade triggers. # Working with Feature Flags When adding features to the platform it is sometimes a requirement to be able to restrict the feature to licensed platforms, and furthermore to certain types of team on the platform. Most typically this will be a feature that should only be available to the `Enterprise` tier on FlowFuse Cloud. This is a quick guide for how to add a feature flag - both at the platform-wide level and against individual `TeamTypes`. Feature flag names should use `camelCase`. ### Add a platform-wide feature flag All licensable features should set a platform-wide feature flag to indicate the feature is available. ```text app.config.features.register('featureFlagName', true) ``` [Here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/forge/ee/lib/ha/index.js#L6){rel=""nofollow""} is an example of how the HA feature sets its platform-wide flag. ### Add a team type feature flag The feature flag is set by an admin user via [`TeamTypeEditDialog.vue`](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/frontend/src/pages/admin/TeamTypes/dialogs/TeamTypeEditDialog.vue){rel=""nofollow""}. 1. Add an entry to the existing list of feature flags [here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/frontend/src/pages/admin/TeamTypes/dialogs/TeamTypeEditDialog.vue#L73-L85){rel=""nofollow""}. 2. Add a check to ensure the right default value is applied [here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/frontend/src/pages/admin/TeamTypes/dialogs/TeamTypeEditDialog.vue#L172-L174){rel=""nofollow""}. Any new feature should default to `false` so it can then be selectively enabled. ### Using the feature flags - runtime side Platform-wide feature flags can be checked using: ```text const isFeatureEnabledOnPlatform = app.config.features.enabled('featureFlagName') ``` `TeamType` feature flags can be checked using the `getFeatureProperty` function of the `TeamType` model: ```text // myTeamType is an instance of `TeamType` const isFeatureEnabledForTeamType = myTeamType.getFeatureProperty('featureFlagName', false) ``` The first arg is the name of the feature flag, the second arg is the default value if the feature flag is otherwise unset. As mentioned above, any new feature should default to `false`. [Here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/forge/ee/routes/sharedLibrary/index.js#L22){rel=""nofollow""} is an example of this in action. ### Using the feature flag - frontend In the frontend, platform-wide feature flags can be checked against the `features` property of the `account` store. TeamType feature flags can be checked against `team.type.properties.features.featureFlagName`. [Here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/frontend/src/pages/application/DeviceGroups.vue#L135-L146){rel=""nofollow""} is an example of how we combine these two things: ```text computed: { ...mapState('account', ['features']), featureEnabledForTeam () { return !!this.team.type.properties.features?.deviceGroups }, featureEnabledForPlatform () { return this.features.deviceGroups }, featureEnabled () { return this.featureEnabledForTeam && this.featureEnabledForPlatform } }, ``` This allows the UI to distinguish between a feature being unavailable because the platform is not licensed for it, and a feature being unavailable for the current team type. This allows different messages to be displayed with the most appropriate call to action. The `EmptyState` component has support for this - see [here](https://github.com/FlowFuse/flowfuse/blob/0335c9056019ff9987d97f3ad3f18675de1c2422/frontend/src/pages/application/DeviceGroups.vue#L29){rel=""nofollow""} for how that is applied. ### Using the FEATURE\_CONFIGS composable The recommended approach for frontend feature checks is to use the `FEATURE_CONFIGS` array in `frontend/src/composables/FeatureChecks.ts`. Each entry defines a feature with platform and/or team level checks, and the `buildFeatureChecks` function produces computed properties that are available via the `featuresCheck` getter on the `account-settings` store. Features can be **opt-in** (disabled by default, must be explicitly enabled per team type) or **opt-out** (enabled by default, must be explicitly disabled). Opt-out features use `optOut: true` in their config entry. Features can also declare dependencies on other features using `dependsOn`, `dependsOnPlatform`, and `dependsOnTeam`. For example, all AI sub-features depend on the `ai` flag: ```text { output: 'isExpertAssistantFeatureEnabled', platformKey: 'expertAssistant', teamKey: 'expertAssistant', optOut: true, dependsOnPlatform: 'ai', dependsOnTeam: 'ai', dependsOnTeamOptOut: true } ``` See the JSDoc on the `FEATURE_CONFIGS` array in `FeatureChecks.ts` for full documentation of all available options. # Contributing to FlowFuse This guide will help you get setup to contribute to the FlowFuse project. The core of the FlowFuse platform is available under the Apache-2.0 license and we welcome contributions from the community. ### Software Requirements This guide assumes you have a working development environment including: - Node.js 18/20 - Platform build tools - Linux: `apt-get install build-essential` - MacOS: `xcode-select --install` - Windows: installed as part of the official node.js installer - ☑️ Automatically install the necessary tools must be checked - Git ### Project Repositories There are a number of repositories under the [FlowFuse GitHub organisation](https://github.com/FlowFuse){rel=""nofollow""} that make up the platform. | Repository | Description | | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [FlowFuse](https://github.com/FlowFuse/flowfuse){rel=""nofollow""} | This is the core of the platform. | | [installer](https://github.com/FlowFuse/installer){rel=""nofollow""} | The installer for the platform | | [driver-localfs](https://github.com/FlowFuse/driver-localfs){rel=""nofollow""} | The LocalFS driver. This deploys instances to the local system. | | [driver-docker](https://github.com/FlowFuse/driver-docker){rel=""nofollow""} | The Docker driver. This deploys instances as containers in a Docker-managed environment. | | [driver-k8s](https://github.com/FlowFuse/driver-k8s){rel=""nofollow""} | The Kubernetes driver. This deploys instances as containers in a Kubernetes-managed environment. | | [nr-launcher](https://github.com/FlowFuse/nr-launcher){rel=""nofollow""} | The launcher application is used to start and monitor an individual instance of Node-RED in the FlowFuse platform. This includes a number of Node-RED plugins used to integrate with the FlowFuse platform. | ### Setting Up A Development Environment With the project split across multiple repositories, setting up a development environment manually takes quite a lot of steps to ensure everything is checked out and configured properly. To make it easier, you can use the [FlowFuse Development Environment](https://github.com/FlowFuse/dev-env){rel=""nofollow""} project to get set up. The following steps will get your development environment setup in no time: ```bash git clone https://github.com/FlowFuse/dev-env.git cd dev-env npm install npm run init ``` This clones all of the main project repositories, installs their dependencies and builds the repositories that need it. All of the repositories are cloned under the `packages` directory: ```txt dev-env └── packages ├── device-agent ├── docker-compose ├── driver-docker ├── driver-k8s ├── driver-localfs ├── file-server ├── flowfuse ├── helm ├── installer ├── nr-file-nodes ├── nr-launcher └── nr-project-nodes ``` More details on using the FlowForge Development Environment are available in its [documentation](https://github.com/FlowFuse/dev-env){rel=""nofollow""}. ### FlowFuse Code Structure The `FlowFuse/flowfuse` repository is the core of the platform and where you'll likely want to begin. ```txt . ├── bin ├── config - build config files ├── docs ├── etc - FlowFuse platform configuration files ├── forge - Platform core code │ ├── config │ ├── containers │ ├── db │ ├── ee │ ├── lib │ ├── licensing │ ├── monitor │ ├── postoffice │ ├── routes │ └── settings ├── frontend - Frontend code │ ├── dist - build output - created by `npm run build` │ ├── public - static assets │ └── src - vue src │ ├── api │ ├── components │ ├── pages │ ├── routes │ └── store ├── test - tests for FlowFuse └── var - where the database and localfs project directories are created ``` ## Development Setup 1. [Create a Stack](https://flowfuse.com/#create-a-stack) 2. [Running FlowFuse](https://flowfuse.com/#running-flowfuse) 3. [Configuring FlowFuse](https://flowfuse.com/#configuring-flowfuse) 4. [Mocking email](https://flowfuse.com/#mocking-email) 5. [Testing](https://flowfuse.com/#testing) 6. [VSCode Tips](https://flowfuse.com/#vscode-tips) 7. [Team Broker](https://flowfuse.com/docs/contribute/team-broker) ### Create a Stack You will need to setup the version(s) of Node-RED you want to use in your stacks. From the `flowfuse` directory run ```bash npm run install-stack --vers=3.1.9 ``` Where `3.1.9` is the version of Node-RED you want to use in the stack. #### Working with Local Nodes If you want to test local, in-development nodes in FlowFuse, you can create a dedicated stack for this purpose. ```bash npm run install-stack --vers=3.1.9 ``` Navigate to the stacks directory: ```bash cd /var/stacks ``` Rename the directory to something more appropriate: ```bash mv "3.1.9" "3.1.9-local" ``` Install your local repository directly into the stack: ```bash cd 3.1.9-local npm install /path/to/your/nodes-repo ``` With your stack created from the terminal, you can now add it via the FlowFuse Admin UI - see [Managing Stacks](https://flowfuse.com/docs/admin/introduction/#create-new-stack). ### Running FlowFuse A number of `npm` tasks are defined in the `package.json` file of this repository. To get started from the `flowfuse` directory use: ```bash npm run serve ``` This does a couple of things in parallel: - Starts the core FlowFuse application and watches the source code for any changes - triggering a restart if needed. - Builds the frontend application using WebPack and watches for any changes - triggering a rebuild as needed. When running like this, the `NODE_ENV` environment variable gets set to `development`. *Note*: if you have not used the [FlowFuse Development Environment](https://flowfuse.com/#setting-up-a-development-environment), then you will need to run `npm run build` to build the platform before you can use `npm run serve`. ### Configuring FlowFuse When running in development mode, the core app will use `etc/flowforge.yml` for its configuration. As you may want to have a local configuration that you don't want to commit back to git, you can create a file called `etc/flowforge.local.yml` and it will use that instead. That filename is set to be ignored by git so it won't be accidentally committed. ### Mocking email If you are developing locally and need to enable external email sending, you can either: - Setup a local test SMTP server. For example, the Nodemailer project provides a useful app that does the job: {rel=""nofollow""} - Alternatively, set the `email.debug` option to `true` in your configuration file and the app will print all emails to its log. ### Configuring billing If you need to develop features covered by the Billing EE feature, you will need to configure the platform with a set of valid Stripe API keys and an EE license. The *development-only* EE licence is provided in `flowfuse/forge/licensing/index.js`. This licence is not valid for production usage. For FlowForge Inc. employees the configuration is provided in 1Password as 'Stripe Testing Configuration'. ```yaml license: *** billing: stripe: key: *** wh_secret: *** team_price: *** team_product: *** project_price: *** project_product: *** device_price: *** device_product: *** deviceCost: 10 new_customer_free_credit: 1000 teams: starter: price: *** product: *** userCost: 0 ``` You will also need to install the [Stripe CLI](https://stripe.com/docs/cli/){rel=""nofollow""} to handle webhook callbacks properly. Install the CLI following their documentation, then run the following command, with the API key using the value of `billing.stripe.key` from above. ```bash stripe listen --forward-to localhost:3000/ee/billing/callback --api-key *** ``` Note that due to the way Stripe works, you will receive events for *all* activity in the configured Stripe account. That means if someone else is actively developing with billing enabled on the same account, you will see their events arrive. #### Free Trials Free trials are implemented as a Stripe Credit that is applied when a FlowFuse user creates their first team and completes billing sign up. To enable trials, set the `billing.stripe.new_customer_free_credit` value to a credit amount in cents. For a totally free trial, this amount should match the cost of the Stripe product for the project type to be trialed to be trialed. The Stripe webhook forwarder must be running as the credit is handled as part of the webhook handling. ### Testing Our testing philosophy follows the principle of: > Write tests. Not too many. Mostly integration [1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""} We create both unit tests and system level tests. The former is suitable for well-contained components that need to provide a stable api and behavior to the rest of the code base. The latter is for testing the external behavior of the platform as a whole with as little internal mocking as possible. We use code coverage reporting as *one* aspect of assessing our testing coverage. We do not treat 100% coverage as an imperative goal - that can often lead to busy work writing tests that don't provide any real value in understanding the overall quality of the system. Unit tests should provide sufficient coverage to give us confidence that a component's behavior does not unexpectedly change. #### Running tests To run the tests for the project, you can use the following npm tasks: - `npm run test` - runs the whole test suite, covering code linting, unit and systems tests. - `npm run lint` - runs the linting tests - `npm run test:unit` - runs the unit tests - `npm run test:system` - runs the system tests - `npm run test:docs` - checks the validity of links in the documentation ##### Testing against PostgreSQL By default, the tests use an in-memory SQLite database to test against. This is the most self-contained way of testing the platform. But it is also necessary to test against PostgreSQL. To enable the use of PostgreSQL in the tests: 1. Ensure you have an instance of PostgreSQL running locally. For example, via docker: ```bash docker run -it -p 5432:5432 --name ff-postgres -e POSTGRES_PASSWORD=secret postgres:14 ``` 2. Enable PostgreSQL mode by setting the following environment variable: ```bash export FF_TEST_DB_POSTGRES=true ``` :brThe database connection can be set using the following env vars (default values shown) ```bash export FF_TEST_DB_POSTGRES_HOST=localhost export FF_TEST_DB_POSTGRES_PORT=5432 export FF_TEST_DB_POSTGRES_USER=postgres export FF_TEST_DB_POSTGRES_PASSWORD=secret export FF_TEST_DB_POSTGRES_DATABASE=flowforge_test ``` #### Reporting code coverage The `test:*` tasks have corresponding code coverage tasks. These tasks run the tests using `nyc` to generate code coverage information. - `npm run cover` - runs the whole test suite (excluding linting) with code coverage enabled and generates a report (via the `cover:report` task) - `npm run cover:unit` - runs the unit tests with code coverage enabled. It does *not* generate the report. - `npm run cover:system` - runs the system tests with code coverage enabled. It does *not* generate the report. - `npm run cover:report` - generates a report of the code coverage. This is printed to the console and generates a browsable HTML copy under `coverage/index.html` ### VSCode Tips To step debug in VSCode 1. Open `launch.json` config and enter the JavaScript below 2. Choose `Start-Watch` from the "Run and Debug" menu 3. Press ▶️ or :kbd[F5] to start debugging There are 2 other "Run and Debug" entries in the menu... - "Attach by Process ID" - this will allow you to attach to a launched driver - "Debug Current Test" - this will enable you to step debug a test (starts debugging the currently open test file) #### ```json { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "command": "npm run start-watch", "name": "Start-Watch", "request": "launch", "type": "node-terminal", "env": { "NODE_ENV": "development" } }, { "name": "Attach by Process ID", "processId": "${command:PickProcess}", "request": "attach", "skipFiles": [ "/**" ], "type": "node" }, { "type": "node", "request": "launch", "name": "Debug Current Test", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--no-warnings", "-u", "bdd",// set to bdd, not tdd "--timeout", "999999", "--colors", "${file}" ], "env": { "NODE_ENV": "development" }, "internalConsoleOptions": "openOnSessionStart" } ] } ``` ## Footnotes 1. {rel=""nofollow""} [↩](https://flowfuse.com/#user-content-fnref-1){.data-footnote-backref ariaLabel="Back to reference 1" dataFootnoteBackref=""} # Local Install This guide is for setting up FlowFuse on a single machine, ideal for smaller deployments, evaluations, or for contributors who want to gain a basic understanding of the FlowFuse platform and its features. **Note: Local installation does not support HTTPS** ## Prerequisites ### Operating System The install script has been tested against the following operating systems: - Raspbian/Raspberry Pi OS versions Buster/Bullseye [1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""} - Debian Buster/Bullseye - Fedora 40 - Ubuntu 20.04 - CentOS 8/RHEL 8/Amazon Linux 2 - MacOS Big Sur & Monterey on Intel & Apple M processors - Windows 10 & 11 ### Node.js FlowFuse requires ***Node.js v24***. #### Linux The install script will check to see if it can find a suitable version of Node.js. If not, it will offer to install it for you. It will also ensure you have the appropriate build tools installed that are often needed by Node.js modules to build native components. #### Windows/MacOS If the install script cannot find a suitable version of Node.js, it will exit. You will need to manually install it before proceeding. Information about how to do this can be found on the Node.js website here: {rel=""nofollow""} You will also need to install the appropriate build tools. - **Windows**: the standard Node.js installer will offer to do that for you. - **MacOS**: you will need the `XCode Command Line Tools`to be installed. This can be done by running the following command: ```bash xcode-select --install ``` ## Installing FlowFuse 1. Create a directory to be the base of your FlowFuse install. For example: `/opt/flowforge` or `c:\flowforge`:br For Linux/MacOS: ```bash sudo mkdir /opt/flowforge sudo chown $USER /opt/flowforge ``` :brFor Windows: ```bash mkdir c:\flowforge ``` 2. Download the latest [Installer zip file](https://github.com/FlowFuse/installer/releases/latest/download/flowforge-installer.zip){rel=""nofollow""} into a temporary location. 3. Unzip the downloaded zip file and copy its contents to the FlowForge directory ### For Linux/MacOS: :br*Assumes `/tmp/` is the directory where you downloaded `flowforge-installer.zip`* ```bash cd /tmp/ unzip flowforge-installer.zip cp -R flowforge-installer/* /opt/flowforge ``` ### For Windows: :br*Assumes `c:\temp` is the directory where you downloaded `flowforge-installer.zip`* ```bash cd c:\temp tar -xf flowforge-installer.zip xcopy /E /I flowforge-installer c:\flowforge ``` 4. Run the installer and follow the prompts :br For Linux/MacOS: ```bash cd /opt/flowforge ./install.sh ``` :brFor Windows: ```bash cd c:\flowforge install.bat ``` ### Installing as a service (optional) On Linux, the installer will ask if you want to run FlowFuse as a service. This will mean it starts automatically whenever you restart your device. If you select this option, it will ask if you want to run the service as the current user, or create a new `flowforge` user. If you choose to create the user, it will also change the ownership of the FlowFuse directory to that user. ## Configuring FlowFuse The default FlowFuse configuration is provided in the file `flowforge.yml` - Linux/MacOS: `/opt/flowforge/etc/flowforge.yml` - Windows: `c:\flowforge\etc\flowforge.yml` The default configuration file already contains everything you need to get started with FlowFuse. It will allow you to access FlowFuse and the Node-RED instances you create, from the same server running the platform. If you want to allow access from other devices on the network, you must edit the configuration file and change the `host` setting to `0.0.0.0` and change `base_url` to contain the IP address of the server. NOTE: We do not support changing the `host` and `base_url` values once you have created an instance. For more information on all of the options available, see the [configuration guide](https://flowfuse.com/docs/install/configuration). ## Running FlowFuse To run it manually, you can use: - Linux/MacOS: ```bash /opt/flowforge/bin/flowforge.sh ``` - Windows: ```bash c:\flowforge\bin\flowforge.bat ``` Or to run as a service: - Linux ```bash service flowforge start ``` ## First Run Setup Once FlowFuse is started, you will be ready to perform the first run setup. Follow [this guide](https://flowfuse.com/docs/install/first-run) to continue. ## Setting up Mosquitto (optional) The platform depends on the [Mosquitto MQTT Broker](https://mosquitto.org/){rel=""nofollow""} to provide real-time messaging between devices and the platform. This is currently an *optional* component - the platform will work without the broker, but some features will not be available (e.g Access to Remote Instance Editor and Remote Instance logs requires the MQTT Broker). We do **not** support sharing a broker with other non-FlowFuse applications. If you already have mosquitto installed and running, you will need to run a second instance dedicated to FlowFuse. You can either follow the manual install steps, which involve building the authentication plugin from scratch, or make use of the [Docker install](https://flowfuse.com/#docker-install). ### Manual install **Note**: if you are running on Windows, you will need to follow the [Docker install](https://flowfuse.com/#docker-install) instructions below due to a limitation of the authentication plugin we use. Follow the appropriate [install instructions](https://mosquitto.org/download/){rel=""nofollow""} for your operating system. Once installed, you can download pre-built binaries for Linux platforms from [here](https://github.com/iegomez/mosquitto-go-auth/releases/latest){rel=""nofollow""} and then jump to step 4 below. On MacOS you will need to build and install the authentication plugin. 1. Clone the plugin repository ```bash git clone https://github.com/iegomez/mosquitto-go-auth.git ``` 2. Follow the instructions on [building the plugin](https://github.com/iegomez/mosquitto-go-auth#building-the-plugin){rel=""nofollow""} 3. This should result in a file called `go-auth.so` being generated 4. Run mosquitto with the configuration file found in the `broker` directory :br You will need to customise the values to match your local configuration: - `auth_plugin` - set to the path of the `go-auth.so` file built in the previous step - `listener 1883/1884` - if you already have mosquitto running locally, you'll need to change these ports to something else. - `auth_opt_http_host` / `auth_opt_http_port` - if you plan to run the platform on a different port, change these settings to match. ```bash mosquitto -c broker/mosquitto.conf ``` ### Docker Install Instead of installing and building mosquitto and the authentication plugin from source, you can use a pre-built docker image that provides everything needed. 1. First pull the latest version of the pre-built container ```bash docker pull iegomez/mosquitto-go-auth ``` 2. A default mosquitto.conf file can be found in the `broker` directory. :br You will need to customise the values to match your local configuration: - `auth_opt_http_host` value to match the IP address of either the docker0 interface or the external IP address of the machine running the FlowFuse platform - `auth_opt_http_port` if you have changed the port the FlowFuse platform is running on - `auth_plugin` should be changed to `auth_plugin /mosquitto/go-auth.so` 3. Start the container with the following command ```bash docker run -d -v /opt/flowforge/broker/mosquitto.conf:/etc/mosquitto/mosquitto.conf -p 1883:1883 -p 1884:1884 --name flowforge-broker iegomez/mosquitto-go-auth ``` :brThis will map the `1883`/`1884` ports to the host machine so they can be accessed outside of the container. If you already have an MQTT broker running on port 1883, then you'll need to modify the `-p` options to use a different set of ports. For example: `-p 9883:1883 -p 9884:1884`. ## File Server By default the FlowFuse File Server component is disabled as it is a licensed feature. If you provide a license you can start the File Server with the following command: ```bash sudo service flowforge-file start ``` You can then uncomment the following section in the `/opt/flowforge/etc/flowforge.yml` file ```yaml ################################################# # File Server config # ################################################# fileStore: url: http://localhost:3001 ``` ## Upgrade If upgrading from 1.x.y to 2.x.y then you may need to upgrade from NodeJS v16 to NodeJS v18. Please ensure you do this before the following steps. To upgrade to the latest release you can follow these steps. Replace `x.y.z` with the version you are upgrading to. 1. Stop FlowFuse `sudo service flowfuse stop` [2](https://flowfuse.com/#user-content-fn-2){#user-content-fnref-2 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""} 2. Change into the `app`directory - `cd /opt/flowforge/app` (Linux/MacOS) - `cd c:\flowforge\app` (Windows) 3. NPM install the desired version - `sudo -u flowforge npm install @flowfuse/flowfuse@x.y.z` (Linux/MacOS) [3](https://flowfuse.com/#user-content-fn-3){#user-content-fnref-3 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""} - `npm install @flowfuse/flowfuse@x.y.z` (Windows) 4. Check the release notes for any additional steps needed to upgrade the particular version 5. Restart FlowFuse `sudo service flowfuse start` [2](https://flowfuse.com/#user-content-fn-2){#user-content-fnref-2-2 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""} If you are running as your normal user you can drop the `sudo -u flowfuse` and just run `npm install @flowfuse/flowfuse@x.y.z` ## Uninstall To uninstall, stop all the running processes and then delete the `/opt/flowfuse` or `c:\flowfuse\` directory. ## Footnotes 1. Arm6 devices, such as the original Raspberry Pi Zero and Zero W are not supported. [↩](https://flowfuse.com/#user-content-fnref-1){.data-footnote-backref ariaLabel="Back to reference 1" dataFootnoteBackref=""} 2. Assumes you are running FlowFuse as a Linux service. [↩](https://flowfuse.com/#user-content-fnref-2){.data-footnote-backref ariaLabel="Back to reference 2" dataFootnoteBackref=""} [↩2](https://flowfuse.com/#user-content-fnref-2-2){.data-footnote-backref ariaLabel="Back to reference 2-2" dataFootnoteBackref=""} 3. Assumes you are running FlowFuse as the `flowfuse` user as created by the installer [↩](https://flowfuse.com/#user-content-fnref-3){.data-footnote-backref ariaLabel="Back to reference 3" dataFootnoteBackref=""} # Local Stacks A Stack defines a set of platform configuration options that will get applied to each Node-RED instance when it is created. For the Local deployment model, this covers two things: - `memory` - the value to apply (in MB) to the Node.js `max-old-space-size` option. This defines the point where Node.js will start freeing unused memory. It is not a hard limit - Node-RED's memory usage will not be capped - but this is useful when running on a memory constrained device such as a Raspberry Pi. Recommended minimum `256`. - `nodered` - the version number of Node-RED to use. This should match the value used in the steps following. The FlowFuse installer will create a default stack using the latest stable release of Node-RED. The stacks are stored under `/opt/flowforge/var/stacks` or `c:\flowforge\var\stacks` on Windows. ### Creating a Stack When a new version of Node-RED is released, it can be added to your FlowFuse platform by creating a new stack. For a local install there are two steps required: 1. Install a new Node-RED version :br In the FlowFuse Home directory, run the provided install script. You must provide the full Node-RED version number, eg `3.0.2`, or use `latest` to install the most recent stable version. :br Linux/Mac: ```bash cd /opt/flowforge ./bin/ff-install-stack.sh 3.0.2 ``` :brWindows ```bash cd c:\flowforge bin\ff-install-stack.bat 3.0.2 ``` 2. Add the Stack into the FlowFuse Admin UI - see [Managing Stacks](https://flowfuse.com/docs/admin/introduction/#managing-stacks). ### Development Only If you are developing FlowFuse having checked it out from GitHub then you can run the following command in the repository root to install a stack: ```bash npm run install-stack --vers=3.0.2 ``` # Team Broker configuration The FlowFuse Team Broker makes use of an EMQX instance. ## Requirement - Docker ## Configuration files Create a directory where the configuration for your broker will live. Create the following three files, with their respective content, in that directory: cluster.hocon ```text authentication = [ { backend = http body { clientId = "${clientid}" password = "${password}" username = "${username}" } connect_timeout = "15s" enable = true enable_pipelining = 100 headers { content-type = "application/json" } mechanism = password_based method = post pool_size = 8 request_timeout = "5s" ssl { ciphers = [] depth = 10 enable = false hibernate_after = "5s" log_level = notice reuse_sessions = true secure_renegotiate = true verify = verify_peer versions = [ "tlsv1.3", "tlsv1.2" ] } url = "http://host.docker.internal:3000/api/comms/v2/auth" }, { backend = built_in_database bootstrap_file = "${EMQX_ETC_DIR}/auth-built-in-db-bootstrap.csv" bootstrap_type = plain enable = true mechanism = password_based password_hash_algorithm {name = plain, salt_position = disable} user_id_type = username } ] authorization { cache { enable = true excludes = [] max_size = 32 ttl = "1m" } deny_action = ignore no_match = allow sources = [ { body { action = "${action}" topic = "${topic}" username = "${username}" } connect_timeout = "15s" enable = true enable_pipelining = 100 headers { content-type = "application/json" } method = post pool_size = 8 request_timeout = "30s" ssl { ciphers = [] depth = 10 enable = false hibernate_after = "5s" log_level = notice reuse_sessions = true secure_renegotiate = true verify = verify_peer versions = [ "tlsv1.3", "tlsv1.2" ] } type = http url = "http://host.docker.internal:3000/api/comms/v2/acls" }, { enable = false path = "data/authz/acl.conf" type = file } ] } listeners { tcp { default { acceptors = 16 access_rules = [ "allow all" ] bind = "0.0.0.0:1883" enable = true enable_authn = true max_conn_rate = infinity max_connections = infinity mountpoint = "${client_attrs.team}" proxy_protocol = false proxy_protocol_timeout = "3s" tcp_options { active_n = 100 backlog = 1024 buffer = "4KB" high_watermark = "1MB" keepalive = none nodelay = true reuseaddr = true send_timeout = "15s" send_timeout_close = true } zone = default } } ws { default { acceptors = 16 access_rules = [ "allow all" ] bind = "0.0.0.0:8083" enable = true enable_authn = true max_conn_rate = infinity max_connections = infinity mountpoint = "${client_attrs.team}" proxy_protocol = false proxy_protocol_timeout = "3s" tcp_options { active_n = 100 backlog = 1024 buffer = "4KB" high_watermark = "1MB" keepalive = none nodelay = true reuseaddr = true send_timeout = "15s" send_timeout_close = true } websocket { allow_origin_absence = true check_origin_enable = false check_origins = "http://localhost:18083, http://127.0.0.1:18083" compress = false deflate_opts { client_context_takeover = takeover client_max_window_bits = 15 mem_level = 8 server_context_takeover = takeover server_max_window_bits = 15 strategy = default } fail_if_no_subprotocol = true idle_timeout = "7200s" max_frame_size = infinity mqtt_path = "/" mqtt_piggyback = multiple proxy_address_header = "x-forwarded-for" proxy_port_header = "x-forwarded-port" supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" validate_utf8 = true } zone = default } } } dashboard { default_password = topSecret } api_key { bootstrap_file = "/mounted/config/api-keys" } ``` acl.conf ```text {allow, {username, {re, "^dashboard$"}}, subscribe, ["$SYS/#"]}. {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. {deny, all, subscribe, ["$SYS/#"]}. {allow, all}. ``` api-keys ```text flowforge:verySecret:administrator ``` ## Starting The following docker command should be run in the directory the configuration files were stored. ```text docker run -d --rm \ -v $(pwd)/cluster.hocon:/opt/emqx/data/configs/cluster.hocon \ -v $(pwd)/api-keys:/mounted/config/api-keys \ -v $(pwd)/acl.conf:/opt/emqx/data/authz/acl.conf \ --add-host=host.docker.internal:host-gateway \ -p 1883:1883 -p 8083:8083 -p 18083:18083 --name emqx emqx/emqx:5.8.0 ``` ## Configuring FlowFuse Make sure the `broker` section of the `flowfuse.yml` is updated as follows ```text broker: url: mqtt://[::1]:1883 public_url: ws://:8083 teamBroker: enabled: true ``` ## Access to the EQMX Dashboard You can log into the EMQX Dashboard at `http://locahost:18083` Username: `admin` Password: `topSecret` # Enabling the device editor ```mermaid sequenceDiagram User->>FrontEnd: Clicks 'open editor' against device FrontEnd->>+Forge: PUT /api/v1/devices/:id/editor { tunnel: 'enable' } Forge->Forge: Generates Forge--)Device: Publishes command to establish connection with Device--)Forge: WS Connect /api/v1/devices/:id/editor/comms/:token Forge->>-FrontEnd: Returns session identifier FrontEnd->>FrontEnd: Opens /device//editor/ FrontEnd-->+Forge: Sends requests to /device//editor/** Forge--)+Device: Request proxied over WebSocket Device-->>Editor: Performs request on local Node-RED Editor-->>Device: Returns response Device-->>-Forge: Streams response back Forge-->>-FrontEnd: Streams response back User->>FrontEnd: User navigates away FrontEnd-->Forge: Node-RED WebSocket closes Note over Forge: if no active WebSockets for this device Forge--)Device: Close WebSocket ``` # Workflows A collection of sequence diagrams for key parts of the FlowFuse platform - [Login](https://flowfuse.com/docs/contribute/workflows/login) - [User Sign Up](https://flowfuse.com/docs/contribute/workflows/signup) - [Password Reset](https://flowfuse.com/docs/contribute/workflows/password-reset) - [Invite External User](https://flowfuse.com/docs/contribute/workflows/invite-external-user) - [Team Create](https://flowfuse.com/docs/contribute/workflows/team-create) - [Project Create](https://flowfuse.com/docs/contribute/workflows/project-create) - [Project States](https://flowfuse.com/docs/contribute/workflows/project-states) # Invite External User Flow ```mermaid sequenceDiagram autonumber participant UserEmail participant InvitedUser participant TeamOwner participant UI participant Runtime participant DB Note over TeamOwner: TeamOwner wants to invite an external user to a team TeamOwner->>UI: Opens Add Team Members dialog TeamOwner->>UI: Enters User email, clicks okay UI->>+Runtime: POST /api/v1/teams/:teamId/invitations Runtime->>DB: Create Invitation Runtime->>UserEmail: Send email containing link to /account/create?email={email}` Runtime->>DB: Update audit log Runtime-->>-UI: { status: 'okay' } Note over TeamOwner: TeamOwner role complete UserEmail-->>InvitedUser: Email received InvitedUser->>+UI: Opens /account/create?email={email} UI->>UI: Prefills email field of sign-up page InvitedUser->>UI: Enters details on sign-up page InvitedUser->>UI: Clicks Sign-up UI->>+Runtime: POST /account/register Runtime->>Runtime: Checks an invite exists for this email Note over InvitedUser,Runtime: Standard sign-up flow continues ``` - See also [User Sign Up](https://flowfuse.com/docs/contribute/workflows/signup) # User Login Flows This represents the login flow as of FlowFuse 1.2, that incorporates optional SSO ```mermaid sequenceDiagram participant U as User participant B as Browser participant RT as ForgeApp participant DB as Database participant IDP as IdentifyProvider U->>B: Enters username/email on sign-up page U->>B: Clicks login B->>RT: POST /account/login (username=XYZ) RT->DB: Checks username/email against list of SSO registered domains alt Email not SSO enabled RT->>B: 403:{ code: 'password_required' } B->>U: Shows password box U->>B: Enters password U->>B: Clicks login B->>RT: POST /account/login (username/password) RT->>DB: Validates username/password RT->>B: 200:{} end alt Email SSO enabled alt Username provided or Password provided RT->>B: 403:{ code:'sso_required', error:'Please login with your email' } B->>U: Prompts user to enter email not username end alt Email provided RT->>B: 403:{ code:'sso_required', redirect:'/account/login?u=' } B->>RT: GET /account/login?u= RT->>RT: passport.authenticate RT->>IDP: SAML exchange IDP->>IDP: User authentication IDP->>RT: POST RT->>DB: Verify Email against users alt Valid User RT->>DB: create session RT->>B: redirect to / else Unknown User RT->>B: redirect to / end end end ``` # Reset Password Flow ```mermaid sequenceDiagram autonumber participant UserEmail participant User participant UI participant Runtime participant DB User->>UI: Access the login page UI->>UI: Displays 'forgot pw' if `user:reset-password`=true User->>UI: Clicks 'forgot password' User->>UI: Enters their email address, clicks submit UI->>+Runtime: POST /account/forgot_password { email: } Runtime->>DB: Get user DB->>Runtime: User Runtime->>DB: Generate AccessToken { scope: 'password:reset' } Runtime->>UserEmail: Send email containing reset link Runtime-->>-UI: { status: 'okay' } UserEmail-->>User: Email received User->>UI: Opens /account/change-password/{token} User->>UI: Enters new details, clicks submit UI->>+Runtime: POST /account/reset_password/:token { password } Runtime->>DB: Validate {token} is a valid password reset token Runtime->>DB: Get the user associated with token Runtime->>DB: Change users password Runtime->>DB: Delete the token Runtime->>-UI: {status: 'okay' } UI->UI: Prompt user to login ``` # Sequence For Project Creation ```mermaid sequenceDiagram autonumber participant User participant Ui participant Runtime participant ContainerDriver participant DB participant Stripe User->>Ui: Clicks Create Project User->>Ui: Enters Project Name Ui->>Runtime: POST /projects alt billing enabled Runtime->DB: Team has Subscription DB->>Runtime: Subscription alt Valid Subscription Runtime->>Stripe: Add Project to Subscription Runtime->>ContainerDriver: create() Runtime->>ContainerDriver: start() Runtime->>Ui: { status: "okay" } Ui->>Ui: Show Project Overview alt success Stripe->>Runtime: POST /ee/billing/callback else failure Stripe->>Runtime: POST /ee/billing/callback Runtime->>ContainerDriver: stop() Runtime->>ContainerDriver: disable() Runtime->>User: email message end else no Subscription Runtime->>Ui: Failed to create project, no Billing info end else no billing Runtime->>Ui: { status: "okay" } Ui->>Ui: Show Project Overview end ``` # States - **Starting (first time)** The nr-laucher process is starting up. On container based systems this is done by creating a container - **Loading** nr-launcher pulling instance settings - **Installing** Once the nr-launcher process has started and downloaded the Instance settings it will npm install any nodes in the settings -> palette section. - **Starting (second time)** Once any nodes are installed the Node-RED process will be started, this state ends once the process can will respond to HTTP request - **Running** The normal state for an Instance - **Restarting** If the "Restart" action is triggered the nr-launcher will restart just the NR process inside the container. It will pull the latest settings data from the Forge app. A restart will also be triggered if the NR process fails to respond to 3 HTTP health checks in a row. Health checks run ever 7 seconds by default. - **Suspending** driver has been asked to suspend the Instance - **Suspended** When an instance is Suspended the Node-RED process is stopped and the nr-launcher shutdown, the container is then shutdown and removed on container based platforms - **Safe** Node-RED can be started in Safe Mode, this starts the Editor but does not run the flows. This is to allow a user to edit the flow to fix a problem. The flows are started when the flows are deployed. This is triggered if the NR process restarts more than 5 times with a run time of less than 30 seconds between each restart. - **Stopped** If the NR process continues to crash while in Safe Mode then it will be placed in to a Stopped state. The nr-launcher is still running but the NR process is not. # 2.3.0 ```mermaid stateDiagram-v2 direction TB InstanceCreated: Instance Created state "nr-launcher" as nrLauncher { direction TB LoadingSettings NPM NodeRED Safe: Safe Mode Stopped LoadingSettings --> NPM: loading NPM --> NPM: installing NPM --> NodeRED: starting NodeRED --> NodeRED: restarting NodeRED --> NodeRED: running NodeRED --> Safe: safe Safe --> Stopped: crashed } InstanceDeleted: Instance Deleted InstanceSuspended: Instance Suspended [*] --> InstanceCreated InstanceCreated --> nrLauncher : starting nrLauncher --> InstanceDeleted nrLauncher --> InstanceSuspended: suspending InstanceSuspended --> nrLauncher: starting InstanceSuspended --> InstanceSuspended: suspended InstanceDeleted --> [*] ``` # User Sign Up Flow ```mermaid sequenceDiagram autonumber participant UE as UserEmail participant US as User participant UI participant RT as Runtime participant DB US->>UI: Enters details on sign-up page US->>UI: Clicks Sign-up UI->>+RT: POST /account/register RT->>DB: Create User RT->>RT: Generate Email Verification Token par Runtime to UserEmail RT->>UE: Send email containing verification code and Runtime to UI RT-->>-UI: { status: 'okay' } and session created end UI->>UI: Show 'Check your email' page UE-->>US: Email received US->>+UI: Enters verification code US->>UI: Click "Continue" button UI->>+RT: POST /account/verify/token { token } RT->>RT: Checks {token} is for the logged in user RT->>DB: User.email_verified=true loop for each pending invite RT->>DB: Add user to team RT->>DB: Delete invite RT->>DB: Update audit log end RT-->>-US: Redirect '/' ``` # Sequence For Team Creation ```mermaid sequenceDiagram autonumber participant User participant Ui participant Runtime participant ContainerDriver participant DB participant Stripe User->>Ui: Clicks Create Team User->>Ui: Enters Team Name alt billing enabled Ui->>Runtime: POST /api/v1/teams Runtime->>DB: Create Team Runtime->>Stripe: checkout.create.session Stripe->>Runtime: Session ID Runtime->>Ui: { billingURL: "https://stripe..." } Ui->>Stripe: Redirect User->>Stripe: Enters Credit Card info alt complete Stripe->>Ui: Redirect to Ui Ui->>Ui: Show Team Overview Stripe->>Runtime: POST /ee/billing/callback Runtime->>DB: Create Subscription else abort Stripe->>Ui: Message Ui->>Runtime: DELETE /api/v1/teams/{id} end else: no billing Ui->>Runtime: POST /teams Runtime->>DB: Create Team Runtime->>Ui: { status: "okay"} Ui->>Ui: Show Team Overview end ``` # Node-RED Safe Mode When a Node-RED instance is unresponsive, for example due to an infinite loop, it can be put into Safe Mode. 1. Edit the instance's [Environment Variables](https://flowfuse.com/docs/user/envvar) 2. Add a variable called `NODE_RED_ENABLE_SAFE_MODE` to `true`. 3. Save the changes then suspend/restart the instance. When starting up in Safe Mode, Node-RED will provide access to the editor without starting the flows. You can log in to the editor, make any necessary changes and then deploy to restart the flows. Once recovered you should delete the `NODE_RED_ENABLE_SAFE_MODE` environment variable to prevent it entering Safe Mode the next time it is restarted. # Deploying Flows to the Device Agent Before you're able to deploy your flows to your Remote Instance, you will have needed to have completed these steps: 1. [Install the Device Agent on the Device](https://flowfuse.com/docs/device-agent/install/overview) - installs Node-RED and other requirements in order to communicate with FlowFuse. 2. [Register the Remote Instance with FlowFuse](https://flowfuse.com/docs/device-agent/register) - this step will have provided you with a `device.yml` file to move to your Remote Instance. 3. [Run the Device Agent](https://flowfuse.com/docs/device-agent/running) - starts the Device Agent on the Remote Instance. ## Deploying a Node-RED Snapshot to the Remote Instance From a Hosted Instance To deploy a Node-RED Snapshot to the Remote Instance: 1. [Create a snapshot](https://flowfuse.com/docs/user/snapshots#create-a-snapshot) - a point-in-time backup of the Node-RED flows and configuration. 2. [Mark that snapshot](https://flowfuse.com/docs/user/snapshots#setting-a-device-target-snapshot) as the **Remote Instance Target** snapshot. This model allows you to develop your flows in FlowFuse and only push it out to the registered Remote Instances when you're happy with what you've created. ## Starting Node-RED on the Remote Instance without deploying a snapshot A Remote Instance can be assigned to an application without a snapshot being deployed to it. In this mode, the Remote Instance will start Node-RED with a default set of flows that can be edited on the Remote Instance see [Editing the Node-RED flows on a Remote Instance that is assigned to an application](https://flowfuse.com/#editing-the-node-red-flows-on-a-remote-instance-that-is-assigned-to-an-application) below ## Editing the Node-RED flows on a Remote Instance that is assigned to an instance When running in the default of Fleet Mode, the device agent does not allow local access to the Node-RED editor. This ensures the Remote Instance is running the deployed snapshot without modification. When running on FlowFuse Cloud, or a premium licensed FlowFuse instance (with the [MQTT broker enabled](https://flowfuse.com/docs/contribute/local/#setting-up-mosquitto-optional){rel=""nofollow""}) the Remote Instance can be placed in Developer Mode that enables remote access to the editor. This can then be used to develop the flows directly on the Remote Instance and a new snapshot generated from the Remote Instance that can be deployed to other Remote Instances in the application. Whilst in Developer Mode the Remote Instance will not receive new updates from the platform when new snapshots are deployed. **Accessing the Editor** 1. Once developer mode is enabled, click the **Enable** button next to the 'Editor Access' option 2. When the editor is available, the Editor button in the header will become active and will take you to the device editor. **Creating a Remote Instance Snapshot** To create an instance snapshot from the Remote Instance use the **Create Snapshot** button in the Developer Mode options panel. You will be prompted to give the snapshot a name and description. See [Snapshots](https://flowfuse.com/docs/user/snapshots) for more information about working with snapshots. ## Editing the Node-RED flows on a Remote Instance that is assigned to an application Access to the editor is only available when: - The Remote Instance is in Developer Mode - When running on FlowFuse Cloud, or a premium licensed FlowFuse instance (with the [MQTT broker enabled](https://flowfuse.com/docs/contribute/local/#setting-up-mosquitto-optional){rel=""nofollow""} the Remote Instance can be placed in Developer Mode that enables remote access to the editor. - Local access to the editor can be enabled by defining a Username & Password in the Device Settings -> Security and enabling "Allow offline access" ![Device Allow Offline Access Settings](https://flowfuse.com/docs/device-agent/images/device-local-access.png){dataZoomable=""}*Device Allow Offline Access Settings* Whilst in Developer Mode the Remote Instance will not receive new updates from the platform. **Enabling Developer Mode** 1. Go to your team's **Remote Instances** page. 2. Select the Remote Instance you want to edit by clicking its name. 3. Click the "Developer Mode" button to enable developer mode. 4. Once enabled, Developer Mode options are available under the tab labelled "Developer Mode" on the Remote Instance page. **Accessing the Editor** 1. Once developer mode is enabled, click the **Enable** button next to the 'Editor Access' option 2. When the editor is available, the Editor button in the header will become active and will take you to the Remote Instance editor. **Creating a Remote Instance Snapshot** To create a snapshot from an application owned Remote Instance use the **Create Snapshot** button in the Developer Mode options panel. You will be prompted to give the snapshot a name and description. See [Snapshots](https://flowfuse.com/docs/user/snapshots) for more information about working with snapshots. **Auto Remote Instance Snapshots** For Remote Instances that are assigned to an application, the platform will automatically create a snapshot of the Remote Instance when it detects flows modified. This snapshot will be created with the name "Auto Snapshot - yyyy-mm-dd hh\:mm-ss". Only the last 10 auto snapshots are kept, others are deleted on a first in first out basis. **Custom Node Catalogues** For Remote Instances that want to make use of custom node catalogues, these can be configured under the Remote Instance settings page on the Palette tab **.npmrc file** Likewise for Remote Instances that need to be provided with a custom `.npmrc` file to allow access to a custom npm registry or to provide an access token this can also be set on the Remote Instance settings Palette tab ### Important Notes - Remote access to the editor requires Device Agent v0.8.0 or later. - The Web UI requires Device Agent v0.9.0 or later. - Assigning a Remote Instance to an application requires Device Agent v1.11.0 and FlowFuse v1.11.0 or later. - Snapshots of Remote Instances assigned to an application are supported in FlowFuse V1.12.0 or later. - Deploying a snapshot from a different Hosted Instance or Remote Instance to an application owned Remote Instance is supported in FlowFuse V1.13.0 or later. - When a Remote Instance is assigned to a Hosted Instance: - It must first have a snapshot applied before editor access is possible. - Disabling Developer Mode and returning to Fleet Mode will cause the Remote Instance to check in with the platform. If the Remote Instance flows have changed, it will be reloaded with the current target snapshot assigned to that Remote Instance, causing any changes made in Developer Mode to be overwritten. Therefore, it is recommended to create a snapshot of the changes before disabling Developer Mode. - When a Remote Instance is assigned to an application: - It will start with a set of default flows. - The Remote Instance will not receive any updates from the platform while in Developer Mode. - The Remote Instance must be online and connected to the platform to enable "Editor Access". - To minimise server and Remote Instance resources, it is recommended to disable "Editor Access" when not actively developing flows on a Remote Instance. - Auto snapshots were introduced in FlowFuse V2.1. - Auto snapshots are only supported for Remote Instance assigned to an application. - If an auto snapshot is set as the target snapshot for a Remote Instance or assigned to a pipeline stage, it will not be auto cleaned up meaning it is possible to have more than 10 auto snapshots. # FlowFuse Device Agent Installer ## What is the FlowFuse Device Agent Installer? The FlowFuse Device Agent Installer is a CLI tool for the FlowFuse Device Agent that automatically sets up Node.js runtime, installs the device agent package, and configures it as a system service. Additionally, it provides a simple interface for keeping the device agent up to date. The FlowFuse Device Agent Installer is the easiest way to get the FlowFuse Device Agent up and running on your remote device. ## Requirements - Linux, macOS, or Windows - Internet connection for downloading dependencies - Administrator/root privileges for system service installation ### Networking requirements Please see [Networking requirements](https://flowfuse.com/docs/device-agent/install/overview#networking-requirements). ## Use the FlowFuse Device Agent Installer to install or update the FlowFuse Device Agent ### One-line install For the fastest one-line install experience, see the [Quick Start guide](https://flowfuse.com/docs/device-agent/quickstart). ### Manual install If you prefer to install the FlowFuse Device Agent manually with the Installer, you can follow these steps: #### 1. Download the installer script: ##### Linux/macOS ```bash /bin/bash -c "$(curl -fsSL https://flowfuse.github.io/device-agent/get.sh)" ``` ##### Windows ```bash powershell -c "irm https://flowfuse.github.io/device-agent/get.ps1|iex" ``` #### 2. Install the Device Agent using One Time Code ##### Linux/MacOS ```bash ./flowfuse-device-agent-installer --otc ``` ##### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash flowfuse-device-agent-installer.exe --otc ``` ### Other installation options #### Install without One Time Code You can also install the FlowFuse Device Agent without a One Time Code by providing the `device.yml` content during interactive installation. To do this, run the installer without the `--otc` flag. ##### Linux/MacOS ```bash ./flowfuse-device-agent-installer ``` ##### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1-2 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash flowfuse-device-agent-installer.exe ``` #### Install in custom directory There is a possibility to install the Device Agent in a custom directory by using the `--dir` option. For example: ##### Linux/MacOS ```bash ./flowfuse-device-agent-installer --dir /path/to/custom/dir ``` ##### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1-3 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash flowfuse-device-agent-installer.exe --dir C:\path\to\custom\dir ``` #### Install on custom port You can configure a custom port using the `--port` flag. The service name includes the port, for example `flowfuse-device-agent-1882` for `--port 1882`. ##### Linux/MacOS ```bash ./flowfuse-device-agent-installer --port 1882 ``` ##### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1-4 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash flowfuse-device-agent-installer.exe --port 1882 ``` ## Updating components ### Node.js runtime To update bundled Node.js runtime, specify the `--update-nodejs` flag with the desired version: ```bash ./flowfuse-device-agent-installer --update-nodejs --nodejs-version 20.19.1 ``` Specifying `--update-nodejs` without a version will pick the default version defined in the installer. From the Device Agent v4 release, the installer defaults to Node.js 22. ### Device Agent To update the Device Agent package, use the `--update-agent` flag, optionally specifying the version: ```bash ./flowfuse-device-agent-installer --update-agent --agent-version 3.3.2 ``` Specifying `--update-agent` without the `--agent-version` flag will update to the latest available version. ## Troubleshooting ### Managing the Device Agent service Services are named per-port, for example `flowfuse-device-agent-1880`. On macOS, the launchd label is `com.flowfuse.device-agent-1880`. #### Linux (systemd) ```bash sudo systemctl start flowfuse-device-agent- sudo systemctl stop flowfuse-device-agent- sudo systemctl restart flowfuse-device-agent- ``` #### Linux (SysVinit) ```bash sudo service flowfuse-device-agent- start sudo service flowfuse-device-agent- stop sudo service flowfuse-device-agent- restart ``` #### Linux (OpenRC) ```bash sudo rc-service flowfuse-device-agent- start sudo rc-service flowfuse-device-agent- stop sudo rc-service flowfuse-device-agent- restart ``` #### macOS (launchd) ```bash sudo launchctl start com.flowfuse.device-agent- sudo launchctl stop com.flowfuse.device-agent- sudo launchctl kickstart -k system/com.flowfuse.device-agent- ``` #### Windows (Service Control) ```bash sc.exe start flowfuse-device-agent- sc.exe stop flowfuse-device-agent- ``` ### Check the Device Agent service status You can check the status of the Device Agent service to verify if it is running correctly or to diagnose any issues. The status command provides information about the current state of the service, including whether it is active, inactive, or failed. #### Linux (systemd) ```bash sudo systemctl status flowfuse-device-agent- ``` #### Linux (SysVinit) ```bash sudo service flowfuse-device-agent- status ``` #### Linux (OpenRC) ```bash sudo rc-service flowfuse-device-agent- status ``` #### macOS (launchd) ```bash sudo launchctl print system/com.flowfuse.device-agent- ``` #### Windows (Service Control) ```bash sc.exe query flowfuse-device-agent- ``` ### Viewing Device Agent log files Adjust the path if custom directory has been specified during installation. #### Linux/macOS: ```bash tail -f /opt/flowfuse-device/logs/flowfuse-device-agent.log ``` #### Linux (systemd): ```bash journalctl -f -u 'flowfuse-device-agent-' ``` #### Windows: ```powershell Get-Content -Path 'C:\opt\flowfuse-device\flowfuse-device-agent.log' -Wait ``` ### Error: Disk space check failed > [ERROR] Disk space check failed: insufficient disk space in temporary directory (/tmp): need at least 500.0 MB, available 490.4 MB ##### Cause: The `Disk space check failed` error indicates that the installer has detected insufficient disk space in the temporary directory. The FlowFuse Device Agent Installer requires a minimum of 500MB of free disk space in the temporary directory to ensure proper installation. This error might also appear if there is not enough space on the disk partition where the Device Agent is being installed. Make sure that the target installation directory has at least 500MB of free space available. [Adjust installation directory](https://flowfuse.com/docs/device-agent/install/device-agent-installer/#install-in-custom-directory) accordingly. ##### Solution: To fix this issue, you can try to free up some disk space by deleting unnecessary files or moving them to another location. Alternatively, you can specify a different temporary directory with sufficient space by setting proper environmental variable before running the installer. **On Linux/macOS**, set the `TMPDIR` environment variable: ```bash export TMPDIR=/path/to/existing/directory/with/sufficient/space ``` **On Windows**, you can set the `TEMP` or `TMP` environment variable: ```powershell Set TMP="C:\path\to\existing\directory\with\sufficient\space" ``` Retry installation after making these adjustments. ## Further reading For more detailed technical information about the FlowFuse Device Agent, like list of supported parameters or how to contribute, please refer to the [documentation](https://github.com/FlowFuse/device-agent/blob/main/installer/README.md){rel=""nofollow""}. ## Footnotes 1. Run `powershell -Command "Start-Process 'cmd' -Verb RunAs"` to launch an elevated command prompt window (e.g. as an admin user) [↩](https://flowfuse.com/#user-content-fnref-1){.data-footnote-backref ariaLabel="Back to reference 1" dataFootnoteBackref=""} [↩2](https://flowfuse.com/#user-content-fnref-1-2){.data-footnote-backref ariaLabel="Back to reference 1-2" dataFootnoteBackref=""} [↩3](https://flowfuse.com/#user-content-fnref-1-3){.data-footnote-backref ariaLabel="Back to reference 1-3" dataFootnoteBackref=""} [↩4](https://flowfuse.com/#user-content-fnref-1-4){.data-footnote-backref ariaLabel="Back to reference 1-4" dataFootnoteBackref=""} # Docker Install Run the Device Agent in a container. Bind-mount your `device.yml` and expose the editor port. ## Prerequisites - Docker or Docker Compose - A `device.yml` configuration from [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register) ## Docker run ```bash docker run \ --mount type=bind,src=/path/to/device.yml,target=/opt/flowfuse-device/device.yml \ -p 1880:1880 \ flowfuse/device-agent:latest ``` ### Time zone Set the container time zone using the `TZ` environment variable: ```bash docker run \ -e TZ=Europe/London \ --mount type=bind,src=/path/to/device.yml,target=/opt/flowfuse-device/device.yml \ -p 1880:1880 \ flowfuse/device-agent:latest ``` ## Docker Compose ```yaml version: '3.9' services: device: image: flowfuse/device-agent:latest ports: - "1880:1880" volumes: - /path/to/device.yml:/opt/flowfuse-device/device.yml environment: - TZ=UTC ``` ## Running as a non-root user From Device Agent v4, the container no longer runs as `root`. It runs as the unprivileged `flowfuse` user (UID `2000` / GID `2000`), following least-privilege security practices. This affects bind-mounted directories: the directory the agent uses for its state must be writable by UID/GID `2000`, otherwise the agent will fail to start with a permissions error. Before upgrading an existing container to v4, update the ownership of any mounted directory so the `flowfuse` user can access it: ```bash sudo chown -R 2000:2000 /path/to/config/dir ``` Alternatively, run the container as a user of your choosing with the `--user` flag: ```bash docker run \ --user 1000:1000 \ --mount type=bind,src=/path/to/device.yml,target=/opt/flowfuse-device/device.yml \ -p 1880:1880 \ flowfuse/device-agent:latest ``` ## Verify Once running and assigned, access the Node-RED editor at `http://:1880`. ## Notes - Device Agent 4.x defaults to Node.js 22; the `latest` tag now uses Node.js 22. Node.js 20 reached end-of-life in April 2026. - Device Agent 3.x uses Node.js 18 in the base image. To stay on a specific line, use a fixed tag instead of `latest`. - For 2.x, use a fixed tag like `2.8.0` instead of `latest`. - Ensure outbound TCP 443 to `app.flowfuse.com` and `mqtt.flowfuse.cloud` and access to `https://registry.npmjs.com` unless using a module cache. See [Running with no access to npmjs.org](https://flowfuse.com/docs/device-agent/running#running-with-no-access-to-npmjsorg). # Kubernetes Install ## When to Use Each Option Running the Device Agent in Kubernetes is appropriate when devices are containerized or managed as part of a Kubernetes-based edge or infrastructure platform. Choose your deployment pattern based on how you manage device identity: - **Fixed Configuration**:br Use when the device already exists in FlowFuse and you have a `device.yml` with its credentials. One deployment maps to one device identity. - **Automatic Provisioning**:br Use when devices should register themselves at startup using a Provisioning Token. Each instance requires writable persistent storage. Any deployment on Kubernetes is going to be specific to the environment and requirements of the solution. The following examples show two common patterns for running the FlowFuse Device Agent on Kubernetes: - Fixed configuration using a static `device.yml` - Automatic provisioning using a FlowFuse Provisioning Token Choose the approach that matches how you manage device lifecycle and credentials. ## Fixed Configuration If you have an existing `device.yml` file containing a set of Device Agent credentials. ```bash kubectl create secret generic device-one-secret --from-file=device.yml=./device.yml ``` The following manifest will create a Deployment and Service for a device using the supplied Secret as its credentials ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: device-one labels: app: device-one spec: replicas: 1 # there can only be one replica as there is one configuration revisionHistoryLimit: 10 selector: matchLabels: app: device-one template: metadata: labels: app: device-one spec: containers: - name: device-one image: flowfuse/device-agent:latest ports: - containerPort: 1880 volumeMounts: - name: config mountPath: "/opt/flowfuse-device/device.yml" subPath: "device.yml" readOnly: true resources: limits: cpu: 1000m memory: 256Mi requests: cpu: 500m memory: 128Mi volumes: - name: config secret: secretName: device-one-secret --- apiVersion: v1 kind: Service metadata: name: device-one-service spec: selector: app: device-one ports: - protocol: TCP port: 1880 targetPort: 1880 ``` ## Automatic Provisioning Using a FlowFuse Provisioning Token to automatically configure a new Device Agent on deployment. Because the Device Agent will need to re-write the `device.yml` file it can no longer be stored in a Secret and a PersistentVolume must be used for each instance of the Device Agent. A Secret is used to hold the initial `device.yml` which contains the provisioning token. ```bash kubectl create secret generic device-provisioning-secret --from-file=device.yml=./device.yml ``` The following manifest will create a Deployment, Service and PVC for a device using the supplied Secret as the source of the Provisioning token. The PVC will be used to store the updated `device.yml` and the Node-RED nodes installed by the Remote Instance. ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: device-one labels: app: device-one spec: replicas: 1 # to scale to more than one instance you should modify this to use a StatefulSet revisionHistoryLimit: 10 selector: matchLabels: app: device-one template: metadata: labels: app: device-one spec: initContainers: # on first run copies the device.yml from Secret to PVC volume - name: config-copy image: busybox:latest command: - "/bin/sh" - "-c" - "if [ ! -f /opt/flowfuse-device/device.yml ]; then cp /tmp/device.yml /opt/flowfuse-device/device.yml; fi" volumeMounts: - name: config mountPath: "/opt/flowfuse-device" - name: initial-config mountPath: "/tmp/device.yml" subPath: "device.yml" readOnly: true containers: - name: device-one image: flowfuse/device-agent:latest ports: - containerPort: 1880 volumeMounts: - name: config mountPath: "/opt/flowfuse-device" resources: limits: cpu: 1000m memory: 256Mi requests: cpu: 500m memory: 128Mi volumes: - name: initial-config secret: secretName: device-provisioning-secret - name: config persistentVolumeClaim: claimName: device-one-pvc --- apiVersion: v1 kind: Service metadata: name: device-one-service spec: selector: app: device-one ports: - protocol: TCP port: 1880 targetPort: 1880 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: device-one-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi ``` # Manual Install (NPM) Use this method if you want direct control over the Node.js runtime and filesystem. ## Prerequisites - Node.js 18 or later installed (Node.js 22 recommended; supported versions are 18, 20, 22, and 24). Node.js 20 reached end-of-life in April 2026. - Linux, macOS, or Windows - Outbound network access to FlowFuse platform and the NPM registry ### Networking requirements Please see [Networking requirements](https://flowfuse.com/docs/device-agent/install/overview#networking-requirements). ## Install the Device Agent The Device Agent is published to npm as [@flowfuse/device-agent](https://www.npmjs.com/package/@flowfuse/device-agent){rel=""nofollow""}. ### Linux/macOS ```bash sudo npm install -g @flowfuse/device-agent ``` ### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash npm install -g @flowfuse/device-agent ``` ## Working directory By default the agent uses `/opt/flowfuse-device` (Linux/macOS) or `c:\opt\flowfuse-device` (Windows) as its working directory. Override with `-d/--dir` if needed. Ensure the directory exists and is writable by the service user. ### Linux/macOS ```bash sudo mkdir -p /opt/flowfuse-device sudo chown -R $USER /opt/flowfuse-device ``` ### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1-2 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash mkdir c:\opt\flowfuse-device icacls c:\opt\flowfuse-device /grant "user":F /T ``` Where "user" is the service account that will run the device agent (ideally, not an admin account). ## Configuration Place a `device.yml` in the working directory. See [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register) for obtaining the configuration via Quick Connect or provisioning. ## Listen Port Node-RED listens on port `1880` by default. Change with `-p/--port`: ```bash flowfuse-device-agent --port 1881 ``` ## Start on system boot Use the provided systemd service file on Linux to run the agent as a service. 1. Download the service file: ```bash curl -L https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/flowfuse-device.service -o flowfuse-device.service ``` 2. Adjust `User`, `Group`, and `WorkingDirectory` if needed. 3. Update `ExecStart` to include a custom port if required. 4. Move the file into place and enable the service: ```bash sudo mv flowfuse-device.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable flowfuse-device sudo systemctl start flowfuse-device ``` On Windows or macOS, consider using the [Installer](https://flowfuse.com/docs/device-agent/install/device-agent-installer) to set up services automatically. ## Verify Start the agent from a terminal to confirm it runs: ```bash flowfuse-device-agent -v ``` Once assigned, access the Node-RED editor at `http://:1880`. ## Upgrading the agent With Device Agent 1.13+, the package moved from the `@flowforge` scope to `@flowfuse`: - npm: `@flowforge/flowforge-device-agent` ➜ `@flowfuse/device-agent` ### Linux/macOS ```bash sudo npm install -g @flowfuse/device-agent@latest ``` ### Windows (run elevated[1](https://flowfuse.com/#user-content-fn-1){#user-content-fnref-1-3 ariaDescribedBy=""footnote-label"" dataFootnoteRef=""}) ```bash npm install -g @flowfuse/device-agent@latest ``` If you must stay on 2.x, use `@2.x`. Device Agent 3.x requires Node.js 18+. Device Agent 4.x defaults to Node.js 22 and supports Node.js 18, 20, 22, and 24. ## Footnotes 1. Run `powershell -Command "Start-Process 'cmd' -Verb RunAs"` to launch an elevated command prompt (e.g. as an admin user) [↩](https://flowfuse.com/#user-content-fnref-1){.data-footnote-backref ariaLabel="Back to reference 1" dataFootnoteBackref=""} [↩2](https://flowfuse.com/#user-content-fnref-1-2){.data-footnote-backref ariaLabel="Back to reference 1-2" dataFootnoteBackref=""} [↩3](https://flowfuse.com/#user-content-fnref-1-3){.data-footnote-backref ariaLabel="Back to reference 1-3" dataFootnoteBackref=""} # Installing Device Agent ## Choose your install path **Recommended for most users:** Use the Device Agent Installer (Quick Start), the fastest way to deploy with minimal configuration. Power users can choose Manual (using `npm`), Docker, or Kubernetes deployments. ### Quick install Run the one-line installer on your device. It installs Node.js, registers the device on your FlowFuse platform, and configures it to run as a local service. **Linux / macOS** ```bash /bin/bash -c "$(curl -fsSL https://flowfuse.github.io/device-agent/get.sh)" && ./flowfuse-device-agent-installer ``` **Windows** — run in an elevated PowerShell terminal: ```powershell Set-Location $env:USERPROFILE; powershell -c "irm https://flowfuse.github.io/device-agent/get.ps1 | iex"; .\flowfuse-device-agent-installer.exe ``` See the [Quick Start guide](https://flowfuse.com/docs/device-agent/quickstart) for the full walkthrough, or the [Installer reference](https://flowfuse.com/docs/device-agent/install/device-agent-installer) for all options and service management. - Recommended: Use the Device Agent Installer - Fastest way to get started with a one-line command in the [Quick Start guide](https://flowfuse.com/docs/device-agent/quickstart) - Full options and service management in the [Installer reference](https://flowfuse.com/docs/device-agent/install/device-agent-installer) - Alternative: Manual install (using `npm`) - Install the npm package, set working directory, configure, and run as a service. See [Manual install](https://flowfuse.com/docs/device-agent/install/manual) - Alternative: Docker / Docker Compose - Run the agent in a container; bind-mount the configuration. See [Docker install](https://flowfuse.com/docs/device-agent/install/docker) - Alternative: Kubernetes - Deploy the agent in a Kubernetes cluster. See [Kubernetes install](https://flowfuse.com/docs/device-agent/install/kubernetes) ## Prerequisites - Node.js 18 or later (for Manual install and for running locally). Supported versions are 18, 20, 22, and 24. Device Agent v4 (and its installer and official Docker image) defaults to Node.js 22 — this is the recommended runtime. Note that Node.js 20 reached end-of-life in April 2026. - Supported OS: Linux, macOS, Windows, or Docker container - Networking: allow outbound access on 443 to: - `app.flowfuse.com` - `mqtt.flowfuse.cloud` - Access to npm registry when snapshots are installed: {rel=""nofollow""} Note: The Device Agent downloads the required Node-RED version and any nodes specified by the assigned snapshot. Ensure firewall/proxy permits access to the npm registry or see [Running with no access to npmjs.org](https://flowfuse.com/docs/device-agent/running#running-with-no-access-to-npmjsorg). ### Networking requirements If you're working behind a firewall, and need to configure it to allow the Device Agent to connect to FlowFuse and the npm registry, see the following: Allow outbound TCP 443 to: - `app.flowfuse.com` - `mqtt.flowfuse.cloud` - `registry.flowfuse.cloud` - `registry.flowfuse.com` Ensure access to npm registry to download Node-RED and nodes: - `https://registry.npmjs.com` For offline environments, see [Running with no access to npmjs.org](https://flowfuse.com/docs/device-agent/running#running-with-no-access-to-npmjsorg). ## Verify the installation After installing by any method: 1. Ensure a working directory exists (default is `/opt/flowfuse-device` or `c:\opt\flowfuse-device`). 2. Run the agent and it will help you get it registered, or provide a device configuration you have already created (via Quick Connect, provisioning, or manual `device.yml`). See [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register). 3. Start the agent (service or CLI) and open `http://:1880` when assigned and running. ## What’s next - Follow the [Quick Start guide](https://flowfuse.com/docs/device-agent/quickstart) to add and connect a Remote Instance - Learn [how to run and configure the agent](https://flowfuse.com/docs/device-agent/running) - Use [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines) to deploy flows # FlowFuse Device Agent ![The installer running in a terminal, walking through sign-up and connection to FlowFuse](https://flowfuse.com/docs/device-agent/images/device-agent-install.gif){dataZoomable=""} ### Overview FlowFuse Device Agent allows you to remotely manage Node-RED instances running on your hardware, for example, devices on your factory floor. The Agent creates a secure connection between the FlowFuse Platform and your devices, enabling you to manage and deploy your applications. ### Get Started You can install the Device Agent on any hardware capable of running Node.js / Node-RED. The [Quick Start Guide](https://flowfuse.com/docs/device-agent/quickstart) will get the Device Agent installed and connected to FlowFuse in under 5 minutes. For more detailed information on installing and running the Device Agent, the full [installation guide](https://flowfuse.com/docs/device-agent/install/overview) has you covered. # Quick Start Guide: Device Agent This guide will walk you through the process of adding a device to FlowFuse, connecting it to the platform, and deploying your Node-RED flows remotely. FlowFuse's Device Agent helps unlock the power of your devices by allowing you to manage and deploy Node-RED flows running on those devices securely and remotely. ## Before you begin You will need: - A terminal or command prompt on the device you wish to install the Agent on - A FlowFuse platform account; either on [FlowFuse Cloud](https://app.flowfuse.com){rel=""nofollow""} or a self-hosted platform. If you do not currently have a FlowFuse platform account, this guide will help you set up a trial account on FlowFuse Cloud. ## Step 1: Install the Device Agent The Device Agent Installer is a one-line command that will: - Install Node.js - Get your device registered on a FlowFuse platform - Configure it to run as a local service It is the quickest way to get started. If you already have Node.js v22+ installed, or need to customize the setup, check the [manual installation guide](https://flowfuse.com/docs/device-agent/install/manual). ##### Linux/MacOS ```bash /bin/bash -c "$(curl -fsSL https://flowfuse.github.io/device-agent/get.sh)" && ./flowfuse-device-agent-installer ``` ##### Windows 1. Open a powershell terminal with elevated permission: ```bash # From Windows Run dialog or a terminal window, enter: powershell -Command "Start-Process 'powershell' -Verb RunAs" ``` 2. Run the following command in the elevated terminal to download and run the installer: ```bash Set-Location $env:USERPROFILE; powershell -c "irm https://flowfuse.github.io/device-agent/get.ps1 | iex"; .\flowfuse-device-agent-installer.exe ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The installer checks to see if port 1880 is available to use. If it isn't, it will let you know before exiting. This is typically because you already have Node-RED running locally. You can tell the installer to configure its Node-RED to use a different port using the `--port ` argument. Pick a different port, for example `1881` and re-run the above command with `--port 1881` added to the end. ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} By default, the installer will use `/opt/flowfuse-device` (Linux/MacOS) or `c:\opt\flowfuse-device` (Windows) as the install location. To use a different location, use the `--dir` option with the install command. For example, `--dir /path/to/custom/location`. ::: :: ## Step 2: Follow the installer prompts The installer will step you through the whole setup process. It will ask if you are registering a new instance or connecting with a One-Time Code (OTC). ##### Registering a new instance Select this option if you have not yet registered the instance on your FlowFuse platform. It will give you a URL to open in your browser where you can register the instance. Once you complete the registration, keep the browser tab open and switch back to the terminal. ##### Connecting with a One-Time Code If you have already registered the instance on FlowFuse, the platform will have given you a One-Time Code to use when installing the Device Agent. Enter the code when prompted and the Device Agent will connect. ## Step 3: Import existing flows The Agent will check common locations for existing Node-RED flows. If it finds any, it will ask if you want to import those flows into your Device Agent managed Node-RED. This makes it easy to migrate an existing Node-RED setup into a fully managed FlowFuse instance. If you want to run an entirely separate instance, you can skip this step. ## Step 4: Set up a system service The Installer will then setup a system service so the Device Agent automatically runs when the device restarts. ## Step 5: Start editing your flows Once the Device Agent is running, and Node-RED has started, you can start editing your flows. If you still have the browser window open from registering the device, it will automatically put the instance into 'Developer Mode' and open up the Node-RED editor. From there you can start building your applications. ## Deploy Flows to Remote Instances There are two approaches to deploying flows to your Remote Instances. - **Developer Mode**: This mode allows you to edit and deploy flows directly from the FlowFuse platform. - **DevOps Pipelines**: FlowFuse provides [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines) as a way of pushing flows from one Hosted Instance/Remote Instance to another (or several in the case of [Device Groups](https://flowfuse.com/docs/user/device-groups)). This is the recommended approach if you're pushing from development environments (e.g. remote test instances) out to remote production instances. ### Developer Mode 1. Navigate to **Applications** and select the application your device was added to. 2. Go to the **Remote Instances** tab within the application. 3. Locate your newly added device and **click** on your Remote Instance. 4. Then Click **Developer Mode** toggle button on upper right. :br![The "Developer Mode" toggle button available on the Device screen](https://flowfuse.com/docs/device-agent/images/developer.png){dataZoomable=""}*The "Developer Mode" toggle button available on the Device screen* 5. This will enable editor access for your device. :br![The "Device Editor" button available on the Device screen](https://flowfuse.com/docs/device-agent/images/editorEnabled.png){dataZoomable=""}*The "Device Editor" button available on the Device screen* 6. Clicking **Device Editor** will launch the editor. :br![Screenshot of a Node-RED Editor for a Device](https://flowfuse.com/docs/device-agent/images/nr_editor.png){dataZoomable=""}*Screenshot of a Node-RED Editor for a Device* ### DevOps Pipelines ![Screenshot showing the user interface for creating and running DevOps Pipelines in FlowFuse](https://flowfuse.com/docs/device-agent/images/ui-devops-pipelines.png){width="750"}{data-zoomable} *Screenshot showing the user interface for creating and running DevOps Pipelines in FlowFuse* To work with Pipelines, you need at least one other Hosted Instance or Remote Device to push *from*/*to*. You can follow the instructions on setting up a Pipeline and deploying your flows between Hosted Instances/Remote Instances [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines). ## Next Steps Now you have a device connected to the platform, there are many features you can use to manage and monitor your Remote Instances. Here are a few to get you started: - [Snapshots](https://flowfuse.com/docs/user/snapshots) - [Pipelines](https://flowfuse.com/docs/user/devops-pipelines) - [Environment Variables](https://flowfuse.com/docs/user/envvar) - [Logs](https://flowfuse.com/docs/user/logs) # Register your Remote Instance To connect your hardware to FlowFuse, you will need to: 1. Install the Device Agent on your hardware 2. Add a "Remote Instance" to FlowFuse, via the FlowFuse UI 3. Connect your hardware to FlowFuse by configuring the Device Agent The best configuration to use will depend on how many Remote Instances you want to connect: - **[Single Instance Registration](https://flowfuse.com/#single-remote-instance-registration)**: for connecting a single Remote Instance, or a small number of Remote Instances, to the platform. - **[Bulk Registration](https://flowfuse.com/#bulk-registration)**: for setting up one or more Remote Instances which will automatically register themselves to the platform when the device agent is run. ## Single Remote Instance Registration For a single Remote Instance, or small batch of Remote Instances, you can manually register each Remote Instance individually, naming each Remote Instance and assigning it to an application. ### Add Remote Instance 1. Go to your team's **Remote Instances** page. 2. Click the **Add Remote Instance** button. 3. You will be prompted to give the Remote Instance a **Name**, an optional **Type** and to choose which **Application**, if any, the Instance should be assigned to. - ![](https://flowfuse.com/docs/device-agent/images/add_remote_instance.png){width="500"} - The **Type** field can be used to record additional meta information about the Remote Instance. - If you do not wish to assign the Remote Instance to an **Application** at this time, you can do so later. 4. Click **Add** ### Connect Hardware to FlowFuse Once the Remote Instance has been added in the FlowFuse UI, you will be shown the **Device Agent Configuration** dialog which contains all the information needed to connect your hardware to the FlowFuse platform. #### Quick Connect By default, you are offered the [Setup command](https://flowfuse.com/#quick-connect) method that was introduced in FlowFuse V2.1, and provides a one-time passcode to automatically connect the hardware to FlowFuse. ![](https://flowfuse.com/docs/device-agent/images/config_yml1.png){width="500px"} Running this command on hardware with the Device Agent installed will automatically configure it and connect it to FlowFuse. #### Manual Setup For older versions of the device agent, you can expand the **Manual Setup** section and use the configuration data with the [Device Agent Web UI](https://flowfuse.com/#device-agent-web-ui) or the [Manual Download](https://flowfuse.com/#manual-download) methods instead. Repeat these steps for each Remote Instance you want to connect to the platform. ## Bulk Registration If you have dozens, or hundreds of pieces of hardware to connect, you can use the **Device Provisioning Configuration** method. This approach provides you with a single "Provisioning Token" for all of your Remote Instances. When passed to the `flowfuse-device-agent`, this token will automatically register your Remote Instances with the relevant instance or application. There is no need to "Add Remote Instance" each time, as is the case with the [Single Device Registration](https://flowfuse.com/#single-remote-instance-registration) method. ### Generating a "Device Provisioning Configuration" 1. Go to your **Team Settings** page. 2. Open the **Provisioning** tab. 3. Click the **Add Token** button. 4. Enter a value for **Token Name** 5. Optionally, choose whether the device should Auto Assign to an **Instance**, an **Application**, or be left unassigned. - Select an **Instance** if you want the device to be automatically assigned to an instance. - Select an **Application** if you want the device to be automatically assigned to an application. 6. Click **Create** ![Screenshot of a FlowFuse Remote Instance Provisioning Token](https://flowfuse.com/docs/device-agent/images/create-provisioning-token.png){dataZoomable=""} Once the Provisioning Token has been created, you will be shown the **Device Provisioning Configuration** dialog: ![Screenshot of a FlowFuse Remote Instance Provisioning Token](https://flowfuse.com/docs/device-agent/images/provisioning-token.png){dataZoomable=""} **IMPORTANT:** This is the only time the platform will show you this information. Make sure to take a copy or use the **Download** button to save the configuration file locally. Once the token is created, it will be shown in the list of tokens on the **Provisioning** tab. ![Screenshot of FlowFuse Remote Instance Provisioning Tokens](https://flowfuse.com/docs/device-agent/images/provisioning-tokens.png){dataZoomable=""} ## Connecting your Hardware ### Install the configuration The Device Agent requires information about the FlowFuse Platform, and how to connect. This comes in the form of a **Device Configuration** file or a **Device Provisioning Configuration** file present in its working directory There are three methods by which you can get this configuration onto your hardware: - **[Quick Connect](https://flowfuse.com/#quick-connect):** Copy the Setup Command (with one-time passcode) and run it in a terminal window on the hardware. Your hardware will then automatically configure itself. - **[Device Agent Web UI](https://flowfuse.com/#device-agent-web-ui):** Copy the configuration file (`device.yml`) to your hardware using its built in Web UI. - *The Device Agent must be running and the [command line flag](https://flowfuse.com/docs/device-agent/running#device-agent-command-line-options) for the Web UI must be enabled.* - **[Manual Download](https://flowfuse.com/#manual-download):** Download the configuration file directly into the hardware's [Working Directory](https://flowfuse.com/docs/device-agent/install/manual#working-directory). ### Methods #### Quick Connect The Quick Connect method was introduced in FlowFuse v2.1. This is the fastest way to connect your hardware to the platform. When registering your hardware you would have been presented the following dialog, with a one-time-passcode that the Device Agent can use to retrieve its configuration: ![](https://flowfuse.com/docs/device-agent/images/config_yml1.png){style="margin: auto;" width="500px"} If you're no longer able to see that dialog, you can regenerate the configuration by following the [Regenerating Configurations](https://flowfuse.com/#regenerating-configurations) steps, or clicking **"Finish Setup"** on the Remote Instance's page: ![](https://flowfuse.com/docs/device-agent/images/finish-setup.png){style="margin: auto; margin-bottom: 12px;" width="750px"} When the quick connect command has been run, the terminal window will report that the Remote Instance has connected to the platform and will output a new `command` for you to use to start the Remote Instance agent with the new configuration. NOTES - The Setup command is only valid for 24h. If you do not use it within this time, you will need to [regenerate](https://flowfuse.com/#regenerating-configurations) it. - The 3 word One-Time-Code (OTC) contained in the Setup command is single use and is deleted immediately upon use. #### Device Agent Web UI ![](https://flowfuse.com/docs/device-agent/images/device_gui.png){style="margin: auto; margin-bottom: 12px;" width="550px"} If the Device Agent is running [with the Web UI enabled](https://flowfuse.com/docs/device-agent/running#device-agent-command-line-options), you can download the configuration file to the Remote Instance using the Web UI. This is useful if you don't have direct access to the Remote Instance's file system. Once the configuration file is downloaded, the device agent will automatically restart and load the configuration. #### Manual Download Place the **Device Configuration** or **Device Provisioning Configuration** file onto your hardware. in the [Working Directory](https://flowfuse.com/docs/device-agent/install/manual#working-directory) By default, the device agent expects the configuration file to be named `device.yml`, if not, you will need to start the device agent with the `-c` [Command Line Option](https://flowfuse.com/docs/device-agent/running#device-agent-command-line-options) and specify the path of the configuration file. The agent can then be started with the command: [^global-install] ```bash flowfuse-device-agent ``` You will see the Device Agent start and perform a 'call-home' where it connects back to the platform to check what it should be running. #### Additional Information If you copy or download a **Device Provisioning Configuration** file to your hardware, you will see the Device Agent start and perform a 'call-home' where it connects back to the platform to auto register itself in the Team's Remote Instances. If successful, the real **Device Configuration** is generated and downloaded to the device. The original **Provisioning Configuration** will be overwritten meaning subsequent runs will not need to perform the auto registration again. ## Assign the Remote Instance The next step is to assign the device to a Node-RED instance or application. Note, that if you've followed [Single Device Registration](https://flowfuse.com/#single-remote-instance-registration) or [Bulk Registration](https://flowfuse.com/#bulk-registration) to register your device, it will automatically be assigned to an Application or Instance. ### Applications This step will permit you to push Snapshots to your Remote Instance via [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines), or via a [Target Snapshot](https://flowfuse.com/docs/user/snapshots/#application-owned-devices) from the Application. #### Assign to Application 1. Go to your team's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance you want to assign and select the **Add to Application** option. 3. Select the application in the dialog and click **Add** to continue. #### Remove from Application To remove the Remote Instance from an application: 1. Go to your team's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance you want to remove and select the **Remove from Application** option. 3. Confirm the action by clicking the **Remove** option. The Remote Instance will stop running the current Node-RED flows. It will then wait until it is assigned to another application or instance. #### Bulk Assigning Remote Instances to an Application If you have a large number of Remote Instances to assign to an application, you can do them all at once: 1. Select the Remote Instances you want to assign and open the **Actions** dropdown menu. 2. Select the **Move to Application** option and then select the application or instance you want to assign the Remote Instances to. 3. Click **Move** to continue. ##### Details: - Remote Instances that are already assigned to the chosen application will not be changed or updated. - For any Remote Instance that is moved by the operation: - Remote Instances moved by the operation will have their target snapshot cleared, device group membership cleared and will be sent an update command. - Remote Instances in Fleet Mode will automatically apply the changes resulting in Remote Instances restarting with the basic starter snapshot flows. - Remote Instances in [Developer Mode](https://flowfuse.com/docs/device-agent/quickstart/#developer-mode) will continue to run their current flows until they are switched to fleet mode at which point they will update accordingly. NOTE: If you wish to keep the flows currently running on the Remote Instance, it must be in developer mode at the time of operation. Once the Remote Instance is moved, create a new snapshot with the "Set as Target" option checked. ### Hosted Instances This method establishes a deployment relationship where a Hosted Instance becomes the source for snapshot deployments to your Remote Instance(s). **Important:** This is a legacy feature; [assigning to Applications](https://flowfuse.com/docs/device-agent/register#applications) is the recommended approach as it provides better fleet management and DevOps Pipeline capabilities. For guidance, see [when to use each approach](https://flowfuse.com/docs/user/concepts#when-to-use-instance-assignment-vs-devops-pipelines). #### Assign to Hosted Instance 1. Go to your team's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance you want to assign and select the **Add to Instance** option. 3. Select the Hosted Instance in the dialog and click **Add** to continue. **Note:** There are constraints on which instances can be assigned to each other. For detailed information, refer to [Assignment Rules and Constraints](https://flowfuse.com/docs/user/concepts#assignment-rules). ### Remove from Hosted Instance To remove the Remote Instance from a Node-RED instance: 1. Go to your team's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance you want to remove and select the **Remove from Hosted Instance** option. 3. Confirm the action by clicking the **Remove** option. The Remote Instance will stop running the current Node-RED flows. It will then wait until it is assigned to another Hosted Instance or Application ### Bulk Assigning Remote Instances to a Hosted Instance If you have a number of Remote Instances to assign to an instance, you can do them all at once. 1. Select the Remote Instances you want to assign and open the **Actions** dropdown menu adjacent to the **Add Remote Instance** button. 2. Select the **Move to Instance** option and then select the instance you want to assign the Remote Instances to. 3. Click **Move** to continue. #### Details: - Remote Instances that are already assigned to the chosen instance will not be changed or updated. - For any Remote Instance that is moved by the operation: - If the chosen instance has a target snapshot set, the newly assigned Remote Instances will inherit this. - If the chosen instance does not have a target snapshot set, newly assigned Remote Instances will have their target snapshot cleared. - Remote Instances in fleet mode will automatically apply the changes. - Remote Instances in developer mode will continue to run their current flows until they are switched to fleet mode at which point they will update accordingly. ### Bulk Removing Remote Instances from an Application or Instance If you have a number of Remote Instances to remove from an application or instance, you can do them all at once. 1. Select the Remote Instances you want to remove and open the **Actions** dropdown menu adjacent to the **Add Remote Instance** button. 2. Select the **Unassign**option. 1. Depending on where you are viewing Remote Instances, this may say "Remove from Application" or "Remove from Instance". 3. Confirm the action by clicking **Unassign**. #### Details: - Remote Instances that are already unassigned will not be changed or updated. - For any Remote Instance that was previously assigned to an application or instance: - Remote Instances in developer mode will be switched to fleet mode. - Remote Instances will have their target snapshot and Remote Instance group membership cleared. - Remote Instances will be informed of the changes resulting in their flows being cleared and the Remote Instance entering a stopped state waiting for a new assignment. ## Regenerating Configurations To regenerate Remote Instance configurations: 1. Go to your team's or instance's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance and select the **Regenerate Configuration** option. 3. You will need to confirm this action as the existing configuration will be immediately revoked. If the Remote Instance tries to use the old configuration it will fail to connect and will delete its local copy of the snapshot it was running. Click **Regenerate Configuration** to continue. You will then be shown the **Remote Instance Configuration** dialog again with a new setup command and the manual configuration to copy or download. ## Deleting a Remote Instance To delete a Remote Instance: 1. Go to your team's or instance's **Remote Instances** page. 2. Open the dropdown menu to the right of the Remote Instance and select the **Delete Remote Instance** option. 3. Confirm the action by clicking the **Delete** option. The next time the Remote Instance attempts to connect to the platform it will find it is no longer authorised and will stop and delete its local copy of the flows it was running. ## Node-RED Settings Most Node-RED settings are managed by the platform as part of deploying an instance to the Remote Instance. However some settings can be overridden locally on the Remote Instance. ### HTTPS configuration *Available in Device Agent 0.10+* The `https` configuration option in `device.yml` can be used to enable HTTPS within Node-RED. The values are passed through to the [Node-RED `https` setting](https://nodered.org/docs/user-guide/runtime/configuration){rel=""nofollow""}. The `ca`, `key` and `cert` properties can be used to provide custom certificates and keys. The values should be set to the contents of the certificate/key. Alternatively, the properties `caPath`, `keyPath` and `certPath` can be used instead to provide absolute paths to files containing the certificates/keys. ```yml [device.yml] https: keyPath: /opt/flowfuse-device/certs/key.pem certPath: /opt/flowfuse-device/certs/cert.pem caPath: /opt/flowfuse-device/certs/ca.pem ``` ### `httpStatic` configuration *Available in Device Agent 0.10+* This option can be used to serve content from a local directory. If set to a path, the files in that directory will be served relative to `/`. ```yml [device.yml] httpStatic: /opt/flowfuse-device/static-content ``` It is also possible to configure it with a list of directories and the corresponding path they should be served from. ```yml [device.yml] httpStatic: - path: /opt/flowfuse-device/static-content/images root: /images - path: /opt/flowfuse-device/static-content/js root: /js ``` ### `localAuth` configuration *Available in Device Agent 3.20+* This option can be used to enable local login for the Node-RED editor. This option is not recommended for day to day use. It can be configured under the Remote Instance's Settings ![Local Auth settings for Remote Instance](https://flowfuse.com/docs/device-agent/images/device-local-access.png){dataZoomable=""}*Local Auth settings for Remote Instance* Or by adding the following to the `device.yml` file ```yml [device.yml] localAuth: enabled: true user: user-name pass: $hashed-password ``` NOTE: The password should be hashed version generated using the Node-RED admin CLI. For example: ```bash node-red admin hash-pw ``` Alternatively, the password can be set in FlowFuse on the settings -> security tab of the Remote Instance. ## Troubleshooting If you have problems with the device agent the first thing to do is to enable the verbose logging mode. To do this add a `-v` to the command line. This will present a lot more information about what the agent is doing. It will show that is has connected to the FlowFuse instance and every time it checks in, it will also log all the local HTTP requests made when accessing the Node-RED Editor via the FlowFuse application. # Running the Device Agent ## Running If the agent was installed as a global npm module, the command `flowfuse-device-agent` will be on the path. If the default working directory and config file are being used, then the agent can be started with: ```bash flowfuse-device-agent ``` By default, Node-RED will listen to port `1880`, you can change it using the options detailed [here](https://flowfuse.com/docs/device-agent/install/manual#listen-port). This will start the agent, set the Remote Instance in the default of fleet mode, and connect to FlowFuse, waiting until a Target Snapshot has been assigned to it, or it is assigned to an Application. ### When assigned to an instance: Once the agent has been assigned a Target Snapshot, it will download the Snapshot and deploy it to the Remote Instance. ### When assigned to an application: Once the agent has been assigned to an application it starts up. If the device is new, it will get a default set of flows which can be edited directly. See [Editing the Node-RED flows on a Remote Instance that is assigned to an application](https://flowfuse.com/docs/device-agent/deploy#editing-the-node-red-flows-on-a-remote-instance-that-is-assigned-to-an-application) for details. ### Device Agent Command Line Options The following command line options are available: ```text Options -c, --config file Device configuration file. Default: device.yml -d, --dir dir Where the agent should store its state. Default: /opt/flowfuse-device -i, --interval secs -p, --port number -m, --moduleCache Use local npm module cache rather than install --node-options string Node.js command-line options to pass to the Node-RED process. Can be specified multiple times. You must use `=` between the option and its value. Web UI Options -w, --ui Start the Web UI Server (optional, does not run by default) --ui-host string Web UI server host. Default: (0.0.0.0) (listen on all interfaces) --ui-port number Web UI server port. Default: 1879 --ui-user string Web UI username. Required if --ui is specified --ui-pass string Web UI password. Required if --ui is specified --ui-runtime mins Time the Web UI server is permitted to run. Default: 10 Setup command -o, --otc string Setup device using a one time code --otc-no-start Do not start the agent after setup --otc-no-import Do not ask to import Node-RED flows during setup -u, --ff-url url URL of FlowFuse. Required for setup Global Options -h, --help print out helpful usage information --version print out version information -v, --verbose turn on debugging output --log-format json output logs in JSON format ``` ### Command Line Examples *Start the agent with a different port number* ```bash flowfuse-device-agent -p 8080 ``` *Start the agent with a different working directory and the Web UI enabled* ```bash flowfuse-device-agent -d /path/to/working/directory -w --ui-user admin --ui-pass password --ui-port 8081 ``` ## Structured JSON logging By default the Device Agent prints plain-text logs, which can be hard to parse with log aggregation tools. From Device Agent v4, you can output machine-readable, structured logs in JSON format using the `--log-format json` flag: ```bash flowfuse-device-agent --log-format json ``` ## Configuring Node.js Options Node.js command-line arguments can be passed to the Node-RED process started by the Device Agent. This is useful for memory-intensive workflows or certificate management. ### Via command line Use `--node-options` — it can be specified multiple times: ```bash # Set a custom heap size limit flowfuse-device-agent --node-options='--max-old-space-size=256' # Enable system certificate authorities (Linux) flowfuse-device-agent --node-options='--use-openssl-ca' # Enable system certificate authorities (Windows/macOS) flowfuse-device-agent --node-options='--use-system-ca' # Combine multiple options flowfuse-device-agent --node-options='--max-old-space-size=256' --node-options='--use-openssl-ca' ``` ### Via `device.yml` Add a `nodeOptions` array to the `device.yml` configuration file: ```yaml [device.yml] nodeOptions: - "--max-old-space-size=256" - "--use-openssl-ca" ``` ## Running behind a HTTP Proxy If the Remote Instance is behind a HTTP proxy, the agent can be configured to use the proxy by setting the `http_proxy`, `https_proxy` or `all_proxy` environment variables. If necessary, the `no_proxy` environment variable can be used to specify a list of hosts that should not be accessed via the proxy. For connecting to FlowFuse Cloud, the `https_proxy` variable should be set to your proxy URL. This environment variable will be used by the agent for both the HTTP and MQTT connections. ### Example setting the proxy environment variables on Linux ```bash # Set the https_proxy environment variable export https_proxy=http://my-proxy:3128 # Set the no_proxy environment variable to exclude local addresses and all hosts in the .mydomain.com domain export no_proxy=localhost,127.0.0.1,.mydomain.com # Start the agent flowfuse-device-agent ``` *To make these settings permanent, see the documentation for your Linux distribution.* ### Example setting the proxy environment variables on Windows ```bash # Set the https_proxy environment variable set https_proxy=http://my-proxy:3128 # Set the no_proxy environment variable to exclude local addresses and all hosts in the .mydomain.com domain set no_proxy=localhost,127.0.0.1,.mydomain.com # Start the agent flowfuse-device-agent ``` *To make these settings permanent, see the documentation for your version of Windows.* ## Running with no access to npmjs.org By default, the Device Agent will try and download the correct version of Node-RED and any nodes required to run the Snapshot that is assigned to run on the Remote Instance. If the Remote Instance is being run on an offline network or security policies prevent the Device Agent from connecting to npmjs.org then it can be configured to use a pre-cached set of modules. You can enable this mode by adding `-m` to the command line or adding `moduleCache: true` to the `device.yml` file. This will cause the Device Agent to load the modules from the `module_cache` directory in the Device Agent's [Working Directory](https://flowfuse.com/docs/device-agent/install/manual#working-directory) (or whatever is set with the `-d` option) (e.g. `/opt/flowfuse-device/module_cache`.). ### Creating a module cache To create a suitable module cache, the device must be assigned to a Remote Instance. You will need to install the modules on a local device with access to npmjs.org, ensuring you use the same OS and Architecture as your target device, and then copy the modules on to your Remote Instance. 1. From the Snapshot page, select the snapshot you want to deploy and select the option to download its `package.json` file. 2. Place this file in an empty directory on your local device. 3. Run `npm install` to install the modules. This will create a `node_modules` directory. 4. On your target Remote Instance, create a directory called `module_cache` inside the Device Agent Configuration directory. 5. Copy the `node_modules` directory from your local instance to the target instance so that it is under the `module_cache` directory. # Depth Estimation The **Depth Estimation** node allows you to estimate the relative distance of objects within an image using an ONNX model. It generates a depth map that represents how far each pixel is from the camera and can optionally create a visual image of the depth map using different color styles. ## Inputs ### General - **Property:** `input` - **Type:** `object`, `buffer`, `string` or tensor. - **Description:** The input image or tensor to classify. See the **Details** section for supported input formats. ##### Supported Input Formats Typically, the input would be an image which could be: - A `Buffer` object containing the binary image data (e.g. from a `file` node or `http request` node) - A base64-encoded string. - A Jimp image object (e.g, output from `node-red-contrib-image-tools`). ##### Tensor input Alternatively, you can supply a pre-processed tensor in the following format: ```json { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 3, 224, 224] } ``` This represents a flat array of pixel values, the data type of the tensor, and its dimensions (for example, `[batch_size, channels, height, width]`). > TIP: If the model supports batching, the input can be an array of images in one of the supported formats. ## Model Selection You can specify the model in two ways: - Provide a **local path** (for example, `/data/models/resnet50.onnx`), or - Specify a **model name** available on **[Hugging Face](https://huggingface.co/models?pipeline_tag=depth-estimation&library=transformers.js,onnx&sort=trending){rel=""nofollow""}** (for example, [Xenova/depth-anything-small-hf](https://huggingface.co/Xenova/depth-anything-small-hf){rel=""nofollow""}). When specifying a model by name, you can define the data type to use when loading it. Supported types include: - `auto`, Automatically selects the most suitable type. - `fp32`, Standard 32-bit floating-point model. - `fp16`, Half-precision 16-bit floating-point model. - `int8`, 8-bit integer quantized model. - `uint8`, 8-bit unsigned integer model. - `q8`, Quantized Int8 model (default). - `q4`, Quantized Int4 model. - `q4f16`, Quantized Int4 with Float16 model. - `bnb4`, BNB4 quantized model. ## Configuration ### Output Image If enabled, the node generates a visual representation of the depth map based on the selected style and alpha values. The output will include both the raw depth data and a generated image: ```json { "data": { ... }, "image": "Buffer", "width": 640, "height": 480 } ``` If disabled, only the raw depth data will be included in the output. ### style Specifies the color map used when creating the depth visualization. Available options include: `grayscale`, `jet`, `hot`, `hsv`, `spring`, `summer`, `autumn`, `winter`, `bone`, `copper`, `viridis`, `inferno`, `magma`, `plasma`, `rainbow`, `cool`, `warm`, `earth`, `blackbody`, `electric`, `velocity-blue`, `velocity-green`, and many more. These styles correspond to common colormaps used in computer vision to represent depth or heat data. ### alpha Defines the transparency of the generated depth image. You can use either a single value or an array of two values: - A single value (e.g., `0.5`) applies a uniform transparency. - An array `[0.3, 0.8]` defines a transparency range from the nearest (0.3) to farthest (0.8) objects. ## Example Flow ::render-flow{:height='400'} ```json [{"id":"5f4317fdaae093fa","type":"image-depth","z":"e1ceeedf31ce1ebd","name":"","property":"image","propertyType":"msg","model":"Xenova/depth-anything-small-hf","modelType":"name","dtype":"fp16","generateImage":"true","generateImageType":"bool","alpha":"alpha","alphaType":"msg","style":"imageStyle","styleType":"msg","x":830,"y":2440,"wires":[["86d79d2fe917c839"]]},{"id":"6c6723ad96d0592f","type":"inject","z":"e1ceeedf31ce1ebd","name":"football (hot A1>0)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/Football_in_Bloomington%2C_Indiana%2C_1995.jpg/1920px-Football_in_Bloomington%2C_Indiana%2C_1995.jpg","vt":"str"},{"p":"imageStyle","v":"hot","vt":"str"},{"p":"alpha","v":"[1,0]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":370,"y":2420,"wires":[["91f63424a0d29cbc"]]},{"id":"f0c3d67e58965419","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":730,"y":2380,"wires":[["a18b3a2b995d44a3"]]},{"id":"6dfc1deb6464d48a","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"300","data":"image","dataType":"msg","active":true,"x":650,"y":2440,"wires":[["5f4317fdaae093fa"]]},{"id":"86d79d2fe917c839","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"payload.image","width":"300","data":"payload.image","dataType":"msg","active":true,"x":1020,"y":2440,"wires":[["15eb62db5c6df5b0"]]},{"id":"15eb62db5c6df5b0","type":"debug","z":"e1ceeedf31ce1ebd","name":"data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1190,"y":2440,"wires":[]},{"id":"fb988c7f2b5e7ebd","type":"inject","z":"e1ceeedf31ce1ebd","name":"tree (1ch greyscale)","props":[{"p":"url","v":"https://www.jotform.com/blog/wp-content/uploads/2022/02/niko-photos-tGTVxeOr_Rs-unsplash.jpg","vt":"str"},{"p":"imageStyle","v":"greyscale","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":370,"y":2380,"wires":[["91f63424a0d29cbc"]]},{"id":"1276ae55440d8971","type":"inject","z":"e1ceeedf31ce1ebd","name":"bird (viridis)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/3/32/House_sparrow04.jpg","vt":"str"},{"p":"imageStyle","v":"viridis","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":350,"y":2500,"wires":[["91f63424a0d29cbc"]]},{"id":"2d346c4a2bd33b35","type":"inject","z":"e1ceeedf31ce1ebd","name":"cave (density)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/f/f4/Hawaiian_lava_tube.jpg","vt":"str"},{"p":"imageStyle","v":"density","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":350,"y":2580,"wires":[["91f63424a0d29cbc"]]},{"id":"80a435f3656d71c3","type":"inject","z":"e1ceeedf31ce1ebd","name":"octopus (jet A0.9>0.3)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/Octopus2.jpg/1920px-Octopus2.jpg","vt":"str"},{"p":"imageStyle","v":"jet","vt":"str"},{"p":"alpha","v":"[0.5,0.9]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":380,"y":2700,"wires":[["91f63424a0d29cbc"]]},{"id":"f6e800ce3399cb5f","type":"inject","z":"e1ceeedf31ce1ebd","name":"cave (grayscale)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/4/4e/HallOfTheMountainKings.jpg","vt":"str"},{"p":"imageStyle","v":"grayscale","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":360,"y":2620,"wires":[["91f63424a0d29cbc"]]},{"id":"0f4eaaa7bf1dd22e","type":"inject","z":"e1ceeedf31ce1ebd","name":"plane (rainbow)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Spitfire_-_Season_Premiere_Airshow_2018_%28cropped%29.jpg/1920px-Spitfire_-_Season_Premiere_Airshow_2018_%28cropped%29.jpg","vt":"str"},{"p":"colormap","v":"rainbow","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":360,"y":2460,"wires":[["91f63424a0d29cbc"]]},{"id":"2f20edfb8190e918","type":"inject","z":"e1ceeedf31ce1ebd","name":"castle (greys)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/5/50/Bodiam-castle-10My8-1197.jpg","vt":"str"},{"p":"imageStyle","v":"greys","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":350,"y":2540,"wires":[["91f63424a0d29cbc"]]},{"id":"c8f48da588d9c9ce","type":"inject","z":"e1ceeedf31ce1ebd","name":"monkey (rdbu A0>1)","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/4/43/Bonnet_macaque_%28Macaca_radiata%29_Photograph_By_Shantanu_Kuveskar.jpg","vt":"str"},{"p":"imageStyle","v":"rdbu","vt":"str"},{"p":"alpha","v":"[0,1]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":370,"y":2660,"wires":[["91f63424a0d29cbc"]]},{"id":"814e081d13eb58f6","type":"comment","z":"e1ceeedf31ce1ebd","name":"Image Depth","info":"","x":330,"y":2340,"wires":[]},{"id":"a18b3a2b995d44a3","type":"change","z":"e1ceeedf31ce1ebd","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"image","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":855,"y":2380,"wires":[["6dfc1deb6464d48a"]],"l":false},{"id":"91f63424a0d29cbc","type":"junction","z":"e1ceeedf31ce1ebd","x":580,"y":2380,"wires":[["f0c3d67e58965419"]]},{"id":"2eab771c6086f708","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-ai-nodes":"0.1.6","node-red-contrib-image-tools":"2.1.1"}}] ``` :: # Image Classification The **Image Classification** node enables you to classify images using **ONNX models** directly within **Node-RED**. It supports both **pre-trained** and **custom** models, allowing you to identify objects, detect scenes, or categorize images without requiring an external AI service. This node is ideal for computer vision tasks such as **image labeling**, **content moderation**, or **feature recognition** at the edge. ## Inputs ### General - **Property:** `input` - **Type:** `object`, `buffer`, `string` or tensor. - **Description:** The input image or tensor to classify. See the **Details** section for supported input formats. ### Model Selection - **model:** Path to a local ONNX model file or the name of a model to download from **Hugging Face**. - **type:** Data type used when loading the model (only applicable when using a model name). Supported types include `q8` (default, quantized Int8), `fp16` (Float16), `fp32` (Float32), and others. > **Note:** > When a model name is provided, the node automatically downloads and caches it locally if it is not already available. ### Configuration - **topK:** The number of top predictions to return. This can be set manually or passed dynamically via a message property. - **threshold:** Minimum confidence score (0.0–1.0) required for predictions to be included in the output. Predictions below this score are filtered out. This value can also be provided dynamically through a message property. ## Outputs - **Property:** `payload` - **Type:** object or array - **Description:** Contains the classification results returned by the model. The structure of the output depends on the model used. ## Details ### Supported Input Formats The node supports multiple input formats depending on the model’s requirements: - **Buffer**, Binary image data, typically from a file or camera input. - **Base64 string**, Base64-encoded image data. - **Jimp Image Object**, An image object (e.g, output from `node-red-contrib-image-tools`). - **Tensor**, A pre-processed tensor object in the following format: ```json { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 3, 224, 224] } ``` > TIP: If the model supports batching, the input can be an array of images in one of the supported formats. ### Model Selection The **model** property defines which ONNX model to use. You can either: - Provide a **local path** (for example, `/data/models/resnet50.onnx`), or - Specify a **model name** available on **[Hugging Face](https://huggingface.co/models?pipeline_tag=image-classification&library=transformers.js,onnx&sort=trending){rel=""nofollow""}** (for example, [MobileNet-v3-Large](https://huggingface.co/qualcomm/MobileNet-v3-Large){rel=""nofollow""}). When a model name is used, the node automatically downloads and caches it locally for reuse. #### Model Type Options - `auto`, Automatically selects the most suitable type. - `fp32`, Standard 32-bit floating-point model. - `fp16`, Half-precision 16-bit floating-point model. - `int8`, 8-bit integer quantized model. - `uint8`, 8-bit unsigned integer model. - `q8`, Quantized Int8 model (default). - `q4`, Quantized Int4 model. - `q4f16`, Quantized Int4 with Float16 model. - `bnb4`, BNB4 quantized model. ### Configuration Options - **topK:** Defines how many top predictions to return in the output. Use this to limit results to the most relevant classes. - **threshold:** Filters predictions by their confidence score. Only predictions above the threshold are included. ## Example Output ```json [ { "label": "golden retriever", "score": 0.9812 }, { "label": "labrador retriever", "score": 0.0143 }, { "label": "cocker spaniel", "score": 0.0021 } ] ``` Each object in the output array includes: - **label:** The predicted class name. - **score:** The confidence score for that prediction. ## Notes - The node supports any **ONNX-compatible image classification model**, such as **ResNet**, **MobileNet**, or **Vision Transformer (ViT)**. - Quantized models (`q8`, `int8`) are recommended for **edge deployments** due to improved performance and lower memory usage. - Ensure that your ONNX model is trained for **image classification** and compatible with **ONNX Runtime**. - When using a Hugging Face model name, ensure network connectivity during the first run so that the model can be downloaded and cached locally. ## Example Flow ::render-flow{:height='400'} ```json [{"id":"80afcb4f0920c6ce","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":670,"y":3160,"wires":[["cef35d49d8f7e529"]]},{"id":"cef35d49d8f7e529","type":"change","z":"e1ceeedf31ce1ebd","name":"topK 3, thres: 5%","rules":[{"t":"move","p":"payload","pt":"msg","to":"image","tot":"msg"},{"t":"set","p":"topK","pt":"msg","to":"3","tot":"num"},{"t":"set","p":"thres","pt":"msg","to":"0.05","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":3160,"wires":[["d76d57912ca9fd3f"]]},{"id":"d76d57912ca9fd3f","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"224","data":"image","dataType":"msg","active":true,"x":1010,"y":3160,"wires":[["903b8d0c4750e324"]]},{"id":"fb880704ba86d144","type":"debug","z":"e1ceeedf31ce1ebd","name":"class","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload[0].label & \"(\" & $round(payload[0].score * 100,2) & \"%)\"","statusType":"jsonata","x":870,"y":3220,"wires":[]},{"id":"a8e34856bea9f3cd","type":"image-classification","z":"e1ceeedf31ce1ebd","name":"","property":"images","propertyType":"msg","model":"onnx-community/resnet-50-ONNX","modelType":"name","dtype":"fp16","topK":"1","topKType":"num","threshold":"0.1","thresholdType":"num","x":1080,"y":3800,"wires":[["7bd09a7daa0d5dae","0b07230327b5689c"]]},{"id":"903b8d0c4750e324","type":"image-classification","z":"e1ceeedf31ce1ebd","name":"","property":"image","propertyType":"msg","model":"Xenova/vit-base-patch16-224","modelType":"name","dtype":"q8","topK":"topK","topKType":"msg","threshold":"thres","thresholdType":"msg","x":700,"y":3220,"wires":[["fb880704ba86d144"]]},{"id":"e21197eababa2618","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"images[0]","width":"224","data":"images[0]","dataType":"msg","active":true,"x":660,"y":3800,"wires":[["d2b756bb0a700a0e"]]},{"id":"d2b756bb0a700a0e","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"images[1]","width":"224","data":"images[1]","dataType":"msg","active":true,"x":860,"y":3800,"wires":[["a8e34856bea9f3cd"]]},{"id":"b5768dffc25bf899","type":"inject","z":"e1ceeedf31ce1ebd","name":"beer","props":[{"p":"url","v":"https://stoelzle-lausitz.com/cdn/shop/files/stoelzle-lausitz-bierglaeser-glass-mug-full-beer-foam.png","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":3200,"wires":[["49f8de9173d09a2f"]]},{"id":"0ff2faa6328f5e8c","type":"inject","z":"e1ceeedf31ce1ebd","name":"wolf","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Eurasian_wolf_2.jpg/1920px-Eurasian_wolf_2.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":3160,"wires":[["49f8de9173d09a2f"]]},{"id":"5833b4f0c1928af6","type":"inject","z":"e1ceeedf31ce1ebd","name":"owl","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Bubo_bubo_sibiricus_-_01.JPG/1024px-Bubo_bubo_sibiricus_-_01.JPG","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":3240,"wires":[["49f8de9173d09a2f"]]},{"id":"7bd09a7daa0d5dae","type":"debug","z":"e1ceeedf31ce1ebd","name":"class","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload[0][0].label & \"(\" & $round(payload[0][0].score * 100,2) & \"%)\"","statusType":"jsonata","x":1270,"y":3760,"wires":[]},{"id":"0b07230327b5689c","type":"debug","z":"e1ceeedf31ce1ebd","name":"class","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload[1][0].label & \"(\" & $round(payload[1][0].score * 100,2) & \"%)\"","statusType":"jsonata","x":1270,"y":3820,"wires":[]},{"id":"c799ffb88823e69c","type":"comment","z":"e1ceeedf31ce1ebd","name":"Image Classification","info":"","x":470,"y":3060,"wires":[]},{"id":"99eb75b743ba98ea","type":"comment","z":"e1ceeedf31ce1ebd","name":"Batch Image Classification","info":"","x":510,"y":3700,"wires":[]},{"id":"55506dfb9dc40daf","type":"change","z":"e1ceeedf31ce1ebd","name":"move payload images array","rules":[{"t":"set","p":"images","pt":"msg","to":"[]","tot":"json"},{"t":"move","p":"payload","pt":"msg","to":"images[0]","tot":"msg"},{"t":"set","p":"url","pt":"msg","to":"urls[1]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":775,"y":3740,"wires":[["2b8af48bb3f824bc"]],"l":false},{"id":"c9b1c4ff34ec7d02","type":"change","z":"e1ceeedf31ce1ebd","name":"move payload images array","rules":[{"t":"move","p":"payload","pt":"msg","to":"images[1]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":995,"y":3740,"wires":[["e21197eababa2618"]],"l":false},{"id":"c7c8d609c2bfaffc","type":"inject","z":"e1ceeedf31ce1ebd","name":"wolf+clock","props":[{"p":"urls","v":"[]","vt":"json"},{"p":"urls[0]","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Eurasian_wolf_2.jpg/1920px-Eurasian_wolf_2.jpg","vt":"str"},{"p":"urls[1]","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Pendulum_clock_by_Jacob_Kock%2C_antique_furniture_photography%2C_IMG_0931_edit.jpg/250px-Pendulum_clock_by_Jacob_Kock%2C_antique_furniture_photography%2C_IMG_0931_edit.jpg","vt":"str"},{"p":"url","v":"urls[0]","vt":"msg"},{"p":"preprocessorConfigOverrides","v":"{\"size\": {\"width\":224, \"height\":224}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":460,"y":3740,"wires":[["2dd6833f25e7a905"]]},{"id":"2dd6833f25e7a905","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":670,"y":3740,"wires":[["55506dfb9dc40daf"]]},{"id":"2b8af48bb3f824bc","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":890,"y":3740,"wires":[["c9b1c4ff34ec7d02"]]},{"id":"79f068f1e84fdef2","type":"inject","z":"e1ceeedf31ce1ebd","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":3480,"wires":[["c7b482660e93f645"]]},{"id":"c7b482660e93f645","type":"file in","z":"e1ceeedf31ce1ebd","name":"","filename":"dog.jpg","filenameType":"str","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":660,"y":3480,"wires":[["64b15839a5d33ad4"]]},{"id":"70d9f015166b7f63","type":"comment","z":"e1ceeedf31ce1ebd","name":"Auto preprocessing: Using Image as input","info":"","x":560,"y":3120,"wires":[]},{"id":"0c1b6f94772cf829","type":"inject","z":"e1ceeedf31ce1ebd","name":"plane","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/e/eb/British_Airways_Concorde_G-BOAC_03.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":3280,"wires":[["49f8de9173d09a2f"]]},{"id":"584b4e035f0e91dd","type":"comment","z":"e1ceeedf31ce1ebd","name":"Auto preprocessing: Using local file Image as input. \\n NOTE: You will need to add a dog.jpg image to test this","info":"","x":600,"y":3420,"wires":[]},{"id":"25a103fa3e5ced6e","type":"image-classification","z":"e1ceeedf31ce1ebd","name":"","property":"payload","propertyType":"msg","model":"onnx-community/resnet-50-ONNX","modelType":"name","dtype":"fp16","topK":"1","topKType":"num","threshold":"0.5","thresholdType":"num","x":700,"y":3540,"wires":[["7af6dd11b8160481"]]},{"id":"7af6dd11b8160481","type":"debug","z":"e1ceeedf31ce1ebd","name":"class","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload[0].label & \"(\" & $round(payload[0].score * 100,2) & \"%)\"","statusType":"jsonata","x":870,"y":3540,"wires":[]},{"id":"64b15839a5d33ad4","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"180","data":"payload","dataType":"msg","active":true,"x":1010,"y":3480,"wires":[["25a103fa3e5ced6e"]]},{"id":"49f8de9173d09a2f","type":"junction","z":"e1ceeedf31ce1ebd","x":560,"y":3160,"wires":[["80afcb4f0920c6ce"]]},{"id":"1012c4c8ef915cdd","type":"global-config","env":[],"modules":{"node-red-contrib-image-tools":"2.1.1","@flowfuse-nodes/nr-ai-nodes":"0.1.6","@flowfuse/nr-file-nodes":"0.0.8"}}] ``` :: # FlowFuse AI Nodes The **FlowFuse AI** Nodes package adds AI capabilities to Node-RED. It includes nodes for running local ONNX models for image classification, object detection, depth estimation, and custom inference, as well as LLM nodes for sending text prompts to hosted and local large language models from OpenAI, Anthropic, Google Gemini, and Ollama. ## Nodes - [LLM Nodes](https://flowfuse.com/docs/flowfuse-nodes/ai/llm-nodes/): Send text prompts to hosted and local large language models from OpenAI, Anthropic, Google Gemini, and Ollama directly within Node-RED flows. - [Depth Estimation](https://flowfuse.com/docs/flowfuse-nodes/ai/depth-estimation/): The Depth Estimation node estimates the distance of objects in an image and creates a depth map using an ONNX model. - [Image Classification](https://flowfuse.com/docs/flowfuse-nodes/ai/image-classification/): Classify images using ONNX models directly in Node-RED. Supports pre-trained and custom models for tasks like labeling, content moderation, and object recognition. - [Object Detection](https://flowfuse.com/docs/flowfuse-nodes/ai/object-detection/): The Object Detection node identifies and locates objects within images using ONNX models such as YOLO and DETR, enabling real-time computer vision directly in Node-RED without external AI services. - [ONXX](https://flowfuse.com/docs/flowfuse-nodes/ai/onxx/): The ONNX node allows you to perform AI inference directly in Node-RED using ONNX models, supporting image, object, and numeric predictions without external AI services. # LLM Nodes The **LLM nodes** send a text prompt to a large language model and return the model's text response. Four provider nodes are included: **OpenAI**, **Anthropic (Claude)**, **Google Gemini**, and **Ollama**. They share a common configuration and message contract so they can be used interchangeably in a flow. > **Note:** More providers, extra functionality, and multi-modality support for the LLM nodes are coming soon. Please reach out if you have such requirements. ## Configuration All four nodes share the same configuration fields: - **Model:** The model name to call (for example, `gpt-4.1-mini`, `claude-opus-4-6`, `gemini-3.1-flash-lite`, `llama3.2`). - **System:** An optional system prompt that sets the model's behaviour. - **Temperature:** Sampling temperature controlling response randomness (default: `0.7`). - **Max Tokens:** The maximum number of tokens to generate in the response. - **Timeout:** How long to wait for a response before aborting the request. - **API Key:** Supplied via credential override or a provider environment variable. The Ollama node can also run against a local endpoint without a key. ### API Key Environment Variables | Provider | Environment Variable | | --------- | ------------------------------------------------------------- | | OpenAI | `OPENAI_API_KEY` | | Anthropic | `ANTHROPIC_API_KEY` | | Gemini | `GEMINI_API_KEY` | | Ollama | `OLLAMA_API_KEY` (optional, not required for local endpoints) | ## Input The node expects a text prompt in the configured input property (default: `msg.payload`). Non-string values are coerced to a string before being sent. ## Output - **`msg.payload`** - The model's text response. - **`msg.ai_meta`** - Normalised response metadata, including `provider`, `model`, `request_id`, `usage` (input/output/total tokens), and `rate_limits` where the provider exposes them. > **Note:** The Ollama node returns its native response details on `msg.ollama` in addition to the normalised `msg.ai_meta`. ## Example Flow The following example sends the same prompt to OpenAI, Anthropic, and Gemini in parallel. It expects the relevant API keys to be available as environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `GEMINI_API_KEY`). Each provider branch runs independently, so the flow works with only one or two keys configured. ::render-flow ```json [{"id":"e4180867a7047b1f","type":"group","z":"6ab76a52887948e7","style":{"stroke":"#555555","stroke-opacity":"1","fill":"rgba(255, 255, 255, 0.03)","fill-opacity":"1","label":true,"label-position":"nw","color":"#f0f0f0"},"nodes":["35993459d37b6f93","152c15fad2618e06","d31647cc21cfb203","810f53d0e1fd9948","8fc45bb0b9dd8aea","0268271895881e33","a8ae18854eb8b60f","28e9d111cbe9274f","cdc5f07bfba274e9","8df6bcc3d93baa32","937896ec93065516"],"x":114,"y":99,"w":652,"h":362},{"id":"35993459d37b6f93","type":"ff-ai-openai","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"","apiKeyEnv":"OPENAI_API_KEY","apiKeyEnvType":"env","model":"gpt-4.1-mini","modelType":"str","temperature":"0.7","temperatureType":"num","maxTokens":"1024","maxTokensType":"num","timeoutMs":"30000","timeoutMsType":"num","system":"","systemType":"str","organization":"","project":"","sendClientRequestId":false,"x":410,"y":140,"wires":[["d31647cc21cfb203"]]},{"id":"152c15fad2618e06","type":"inject","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"ISS info","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"what speed does the ISS orbit the earth and how many times does it orbit in 24h?","payloadType":"str","x":210,"y":140,"wires":[["35993459d37b6f93"]]},{"id":"d31647cc21cfb203","type":"debug","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"openai response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":140,"wires":[]},{"id":"810f53d0e1fd9948","type":"ff-ai-anthropic","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"","apiKeyEnv":"ANTHROPIC_API_KEY","apiKeyEnvType":"env","model":"claude-haiku-4-5","modelType":"str","temperature":"0.7","temperatureType":"num","maxTokens":"512","maxTokensType":"num","timeoutMs":"30000","timeoutMsType":"num","system":"","systemType":"str","endpoint":"https://api.anthropic.com/v1/messages","anthropicVersion":"2023-06-01","sendClientRequestId":false,"x":420,"y":220,"wires":[["28e9d111cbe9274f"]]},{"id":"8fc45bb0b9dd8aea","type":"inject","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"ISS info","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"what speed does the ISS orbit the earth and how many times does it orbit in 24h?","payloadType":"str","x":210,"y":220,"wires":[["810f53d0e1fd9948"]]},{"id":"0268271895881e33","type":"ff-ai-gemini","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"","apiKeyEnv":"GEMINI_API_KEY","apiKeyEnvType":"env","model":"gemini-3.1-flash-lite","modelType":"str","temperature":"0.7","temperatureType":"num","maxTokens":"1024","maxTokensType":"num","timeoutMs":"30000","timeoutMsType":"num","system":"","systemType":"str","endpointTemplate":"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent","sendClientRequestId":false,"x":410,"y":300,"wires":[["cdc5f07bfba274e9"]]},{"id":"a8ae18854eb8b60f","type":"inject","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"ISS info","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"what speed does the ISS orbit the earth and how many times does it orbit in 24h?","payloadType":"str","x":210,"y":300,"wires":[["0268271895881e33"]]},{"id":"28e9d111cbe9274f","type":"debug","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"claude response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":620,"y":220,"wires":[]},{"id":"cdc5f07bfba274e9","type":"debug","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"gemini response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":620,"y":300,"wires":[]},{"id":"8df6bcc3d93baa32","type":"catch","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"","scope":"group","uncaught":false,"x":230,"y":420,"wires":[["937896ec93065516"]]},{"id":"937896ec93065516","type":"debug","z":"6ab76a52887948e7","g":"e4180867a7047b1f","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":590,"y":420,"wires":[]}] ``` :: # Object Detection The **Object Detection** node enables detection of objects within images using **ONNX models**. It supports a wide range of architectures, including **DETR** and **YOLO**, and accepts image data in multiple formats such as Buffers, base64 strings, or tensors. This node is ideal for computer vision use cases like identifying objects in images, counting items, or performing scene analysis directly within **Node-RED**. ## Inputs ### General - **Property:** `input` - **Type:** `object`, `buffer`, `string` or tensor. - **Description:** The input image or tensor to classify. See the *Details* section for supported formats. ### Model Selection - **model:** Path to a local ONNX model file or the name of a model to download from Hugging Face. - **type:** Data type for the model when using a model name. Supported values include `q8` (default), `fp16`, `fp32`, `int8`, and others. > **Note:** > When a model name is provided, the node automatically downloads and caches it locally if it is not already available. ### Configuration - **threshold:** Minimum confidence score (0.0–1.0) required for a prediction to be included in the output. This can also be passed dynamically via `msg.threshold`. ## Outputs - **payload:** Contains the detection results. Depending on the model type and processing support, the output can be an array or an object. ## Details ### Supported Input Formats The node supports multiple input formats depending on the model’s requirements: - **Buffer**, Binary image data, typically from a file or camera input. - **Base64 string**, Base64-encoded image data. - **Jimp Image Object**, An image object (e.g, output from `node-red-contrib-image-tools`). - **Tensor**, A pre-processed tensor object in the following format: ```json { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 3, 224, 224] } ``` > TIP: If the model supports batching, the input can be an array of images in one of the supported formats. ### Model Selection The `model` property defines which ONNX model to use. You can either: - Provide a **local path** (for example, `/data/models/yolov5.onnx`), or - Specify a **model name** available on **[Hugging Face](https://huggingface.co/models?pipeline_tag=object-detection&library=transformers.js,onnx&sort=trending){rel=""nofollow""}** (for example, [Xenova/detr-resnet-50](https://huggingface.co/Xenova/detr-resnet-50){rel=""nofollow""}). When a model name is provided, it is automatically fetched and cached locally for reuse. #### Model Type Options - `auto`, Automatically selects the most suitable type. - `fp32`, Standard 32-bit floating-point model. - `fp16`, Half-precision 16-bit floating-point model. - `int8`, 8-bit integer quantized model. - `uint8`, 8-bit unsigned integer model. - `q8`, Quantized Int8 model (default). - `q4`, Quantized Int4 model. - `q4f16`, Quantized Int4 with Float16 model. - `bnb4`, BNB4 quantized model. ### Output Format #### When Supported (YOLO/DETR Models) If the model output is recognized by the node, `msg.payload` contains structured detection results: ```json [ { "label": "dog", "score": 0.9796, "bbox": [130, 218, 309, 538] }, { "label": "person", "score": 0.9451, "bbox": [420, 110, 640, 520] } ] ``` Each object includes: - **label:** Detected class name (for example, dog, person, car) - **score:** Confidence score for the detection - **bbox:** Bounding box coordinates `[x_min, y_min, x_max, y_max]` #### When Not Supported (Raw Output) If the node cannot interpret the model output automatically, it returns the raw response: ```json { "result": [...], "labels": { "0": "person", "1": "bicycle", "2": "car" } } ``` You can then use a **Function** node for custom post-processing. ## Notes - The node currently supports **DETR** and **YOLO**-style object detection models. - YOLO models currently accept **only single-image input** (batching support will be added in future releases). - Ensure that your model is compatible with **ONNX Runtime** and designed for **object detection** tasks. - For improved performance on devices with limited resources, use **quantized models** such as `q8`. ## Example Flow ::render-flow ```json [{"id":"6a3ca8414acfad34","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":610,"y":1180,"wires":[["f4998c6a9004a6ba"]]},{"id":"f4998c6a9004a6ba","type":"change","z":"e1ceeedf31ce1ebd","name":"","rules":[{"t":"set","p":"image","pt":"msg","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":1240,"wires":[["d902ee154cb6ceac"]]},{"id":"b1a008f95453208e","type":"inject","z":"e1ceeedf31ce1ebd","name":"people on bikes","props":[{"p":"url","v":"https://learnopencv.com/wp-content/uploads/2021/04/vehicle-traffic-object-detection-test-image.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":380,"y":1240,"wires":[["47d587d70bac373c"]]},{"id":"8d18b7c3c2197981","type":"inject","z":"e1ceeedf31ce1ebd","name":"dog bike truck","props":[{"p":"url","v":"https://djl.ai/examples/src/test/resources/dog_bike_car.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":370,"y":1200,"wires":[["47d587d70bac373c"]]},{"id":"86693a849586867a","type":"inject","z":"e1ceeedf31ce1ebd","name":"desk","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Schreibtisch.2.JPG/450px-Schreibtisch.2.JPG","vt":"str"},{"p":"threshold","v":"0.85","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":350,"y":1160,"wires":[["47d587d70bac373c"]]},{"id":"9113dce8475110f5","type":"object-detection","z":"e1ceeedf31ce1ebd","name":"","property":"image","propertyType":"msg","model":"Xenova/detr-resnet-50","modelType":"name","dtype":"fp16","threshold":"threshold","thresholdType":"msg","x":590,"y":1300,"wires":[["2ccb4544d3d5188d"]]},{"id":"2ccb4544d3d5188d","type":"change","z":"e1ceeedf31ce1ebd","name":"","rules":[{"t":"set","p":"result","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"image","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":1300,"wires":[["385ba9c93695c86b"]]},{"id":"a2ef9dc6bf6007e2","type":"debug","z":"e1ceeedf31ce1ebd","name":"debug 15","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"result","targetType":"msg","statusVal":"","statusType":"auto","x":1100,"y":1360,"wires":[]},{"id":"385ba9c93695c86b","type":"function","z":"e1ceeedf31ce1ebd","name":"Pascal VOC to COCO bbox","func":"const annotations = msg.result.slice(0,20)\n// convert Pascal VOC Format bbox to coco format\nconst bboxes = []\nfor (const annotation of annotations) {\n const [ xmin, ymin, xmax, ymax ] = annotation.bbox\n const width = xmax - xmin\n const height = ymax - ymin\n const percent = annotation.score * 100\n bboxes.push({\n label: `${annotation.label} (${percent.toFixed(1)}%)`,\n bbox: [xmin, ymin, width, height]\n })\n}\nmsg.annotations = bboxes\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":1300,"wires":[["da4063f9d3a2548b"]]},{"id":"da0ae2dfd2df248f","type":"inject","z":"e1ceeedf31ce1ebd","name":"football","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/Football_in_Bloomington%2C_Indiana%2C_1995.jpg/500px-Football_in_Bloomington%2C_Indiana%2C_1995.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":350,"y":1120,"wires":[["47d587d70bac373c"]]},{"id":"f23ba6823adaab87","type":"comment","z":"e1ceeedf31ce1ebd","name":"Object Detection","info":"","x":360,"y":1080,"wires":[]},{"id":"d902ee154cb6ceac","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"240","data":"image","dataType":"msg","active":true,"x":350,"y":1300,"wires":[["9113dce8475110f5"]]},{"id":"d75c7f464cca14c1","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"720","data":"payload","dataType":"msg","active":true,"x":830,"y":1360,"wires":[["a2ef9dc6bf6007e2"]]},{"id":"da4063f9d3a2548b","type":"annotate-image","z":"e1ceeedf31ce1ebd","name":"","fill":"","stroke":"#ffA000","lineWidth":"3","fontSize":24,"fontColor":"#ffA000","x":660,"y":1360,"wires":[["d75c7f464cca14c1"]]},{"id":"47d587d70bac373c","type":"junction","z":"e1ceeedf31ce1ebd","x":500,"y":1180,"wires":[["6a3ca8414acfad34"]]},{"id":"8ee41c61e110e410","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-ai-nodes":"0.1.6","node-red-contrib-image-tools":"2.1.1","node-red-node-annotate-image":"0.2.0"}}] ``` :: # ONXX The **ONNX** node allows you to perform AI inference directly in **Node-RED** using **ONNX models**. It can run a wide range of pre-trained or custom models, including image classification, object detection, and numeric prediction tasks. **ONNX (Open Neural Network Exchange)** is an open standard for representing machine learning models. With this node, you can load an ONNX model and run predictions locally or on the edge without requiring a separate AI service. ## Inputs ### General - **Property:** `input` - **Type:** object, buffer, or tensor - **Description:** The input data to process. It can be an image, array, or tensor. See the **Input Formats** section below for supported structures. ### Model Selection - **Property:** `model` - **Type:** string - **Description:** Path to the ONNX model file. It can be a direct file path (for example, `/data/models/model.onnx`) or an environment variable (for example, `${MODEL_PATH}`). ## Outputs - **Property:** `payload` - **Type:** object or array - **Description:** Contains the model’s output after inference. Depending on the model, this may include predictions, probabilities, or other structured results. ## Input Formats The input format depends on what your ONNX model expects. You can check the model’s input names, types, and shapes by clicking the **Model Info** button in the node configuration panel. ### 1. Tensor Format Use this format when the model expects a single tensor input. ```json { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 3, 224, 224] } ``` - **data:** Flat array of numerical values (for example, pixel data). - **type:** Data type of the tensor (for example, `float32`, `int8`). - **dim:** Tensor dimensions in `[batch_size, channels, height, width]` format. ### 2. Array of Tensors Used when the model expects multiple input tensors. ```json [ { "data": [0.0, 0.1, ...], "type": "float32", "dim": [1, 3, 224, 224] }, { "data": [0, 1, 2, ...], "type": "int8", "dim": [1, 10] } ] ``` ### 3. Named Tensor Properties Used when the model defines multiple named input tensors. ```json { "input_1": { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 3, 224, 224] }, "input_2": { "data": [0.0, 0.1, 0.2, ...], "type": "float32", "dim": [1, 10] } } ``` ### 4. Array-like Input If the model expects a single flat array, you can provide it directly: ```json [0.0, 0.1, 0.2, ...] ``` For batch inputs, use an array of arrays: ```json [ [0.0, 0.1, 0.2, ...], [0.0, 0.1, 0.2, ...] ] ``` ## Configuration - The model must be in the **ONNX (.onnx)** format. - Ensure your input format matches the model’s expected input definition. - Use the **Model Info** button in the configuration panel to inspect model input and output specifications before wiring it into your flow. - The result of the inference is available in `msg.payload` for further processing or visualization. ## Example Flow ::render-flow ```json [{"id":"239b5347fce92ca9","type":"function","z":"e1ceeedf31ce1ebd","name":"load labels","func":"// flag to let the node know we are handling preprocessing\nmsg.noPreprocessorConfig = true\n\nmsg.config = {\n label2id : {\n \"apple\": 0,\n \"kiwi\": 1,\n \"mango\": 2\n },\n id2label : {\n \"0\": \"apple\",\n \"1\": \"kiwi\",\n \"2\": \"mango\"\n }\n}\nreturn msg\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":810,"y":5860,"wires":[["117470780ea20643"]]},{"id":"117470780ea20643","type":"function","z":"e1ceeedf31ce1ebd","name":"preprocessing","func":"// ImageNet normalization values\nconst IMG_MEAN = [0.485, 0.456, 0.406]\nconst IMG_STD = [0.229, 0.224, 0.225]\n\n// The width and height expected by the model\nconst WIDTH = 224\nconst HEIGHT = 224\n\n/**\n * Load and preprocess image for PyTorch ONNX model\n * @param {Buffer} buffer - an image\n * @returns {Promise} - Tensor ready for ort.run\n */\nasync function preprocessImage(buffer) {\n // Load and resize image\n const resolved = await sharp(buffer)\n .resize(WIDTH, HEIGHT)\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n const { data, info } = resolved; // data = Uint8Array, info = {width, height, channels}\n const { width, height, channels } = info;\n\n if (channels !== 3) {\n throw new Error(`Expected 3 channels (RGB), got ${channels}`);\n }\n\n // Convert to float32 and normalize\n const floatData = new Float32Array(width * height * channels);\n for (let i = 0; i < width * height; i++) {\n for (let c = 0; c < 3; c++) {\n // data is 0..255, convert to 0..1\n const v = data[i * 3 + c] / 255.0;\n floatData[c * width * height + i] = (v - IMG_MEAN[c]) / IMG_STD[c];\n // Notice: channel-first layout (C,H,W)\n }\n }\n\n return floatData;\n}\n\nmsg.payload = {\n type: 'float32',\n dims: [1, 3, HEIGHT, WIDTH], // 1 image, 3 chanels, HEIGHT, WIDTH\n data: await preprocessImage(msg.payload)\n}\n// msg.payload = await preprocessImage(msg.payload)\nreturn msg\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"sharp","module":"sharp"}],"x":1020,"y":5860,"wires":[["36477656b418b6f7"]]},{"id":"36477656b418b6f7","type":"advanced-ai","z":"e1ceeedf31ce1ebd","name":"fruit_classifier","property":"payload","propertyType":"msg","model":"C:/Users/sdmcl/repos/node-red/model_cache/flowfuse/fruit_classifier/onnx/model.onnx","modelType":"path","x":900,"y":5920,"wires":[["aa2b68b2ca30ee34"]]},{"id":"aa2b68b2ca30ee34","type":"function","z":"e1ceeedf31ce1ebd","name":"Sort and Label","func":"/*\nData arrives like so:\nmsg.payload.output.cpuData: {\"0\":1.7446770668029785, 1: -2.4252512454986572, 2: 2.786302089691162}\n*/\nconst cpuData = msg.payload.output.cpuData\nconst labels = []\nconst ids = Object.keys(msg.config.id2label).forEach(id => {\n labels.push(msg.config.id2label[id])\n})\n\n// Convert cpuData to logits array\nconst logits = Object.values(cpuData)\n\n// Softmax function\nfunction softmax(arr) {\n const exp = arr.map(x => Math.exp(x))\n const sum = exp.reduce((a, b) => a + b, 0)\n return exp.map(x => x / sum)\n}\n\nconst probs = softmax(logits)\n\n// Build sorted array of results with labels and probabilities\nconst resultArray = probs\n .map((prob, idx) => ({\n classIndex: idx,\n className: labels[idx] || 'Unknown',\n confidence: prob\n }))\n .filter(item => item.confidence > 0)\n .sort((a, b) => b.confidence - a.confidence)\n\nmsg.payload = resultArray\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1080,"y":5920,"wires":[["f3b605669eb8b00b"]]},{"id":"f3b605669eb8b00b","type":"debug","z":"e1ceeedf31ce1ebd","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload[0].className & \" (\" & $round(payload[0].confidence * 100, 2) & \"%)\"","statusType":"jsonata","x":1070,"y":5980,"wires":[]},{"id":"114028180bd0659b","type":"inject","z":"e1ceeedf31ce1ebd","name":"apple","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/0/07/Honeycrisp-Apple.jpg/1200px-Honeycrisp-Apple.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":5800,"wires":[["bc8648c963842493"]]},{"id":"709d0cacee000fde","type":"inject","z":"e1ceeedf31ce1ebd","name":"apple","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Fuji_apple.jpg/1200px-Fuji_apple.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":5840,"wires":[["bc8648c963842493"]]},{"id":"bc8648c963842493","type":"http request","z":"e1ceeedf31ce1ebd","name":"","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":630,"y":5800,"wires":[["e6c4bc36b25440e5"]]},{"id":"38ce06ccd2500c74","type":"inject","z":"e1ceeedf31ce1ebd","name":"kiwi","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Kiwi_%28Actinidia_chinensis%29_2_Luc_Viatour.jpg/250px-Kiwi_%28Actinidia_chinensis%29_2_Luc_Viatour.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":5880,"wires":[["bc8648c963842493"]]},{"id":"8c2a2e98a21f319b","type":"inject","z":"e1ceeedf31ce1ebd","name":"mango","props":[{"p":"url","v":"https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Mangos_-_single_and_halved.jpg/500px-Mangos_-_single_and_halved.jpg","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":430,"y":5920,"wires":[["bc8648c963842493"]]},{"id":"e6c4bc36b25440e5","type":"image viewer","z":"e1ceeedf31ce1ebd","name":"","width":"224","data":"payload","dataType":"msg","active":true,"x":610,"y":5860,"wires":[["239b5347fce92ca9"]]},{"id":"5711f41ecfe998d6","type":"comment","z":"e1ceeedf31ce1ebd","name":"Using a re-trained resnet model trained to recognise apples, kiwis and mangos \\n See info in the INFO panel on the sidebar","info":"This Node-RED demo flow requires you to have trained a model to recognize fruit types (apple, kiwi, mango) using a labeled image dataset.\n\nThe process involves:\n1. Setting up your Python environment with PyTorch, TorchVision, ONNX, and ONNX Runtime.\n1. Organizing your dataset into train, validation, and test folders for each class.\n1. Using transfer learning with a pre-trained ResNet18 model, fine-tuned on your images.\n1. Training the model and evaluating its accuracy.\n1. Exporting the trained PyTorch model to ONNX format for interoperability.\n1. Optionally, testing the ONNX model with ONNX Runtime to verify predictions.\n\nOnce you have the exported ONNX model (e.g., fruit_classifier.onnx), you can use it with this demo flow for inference.","x":630,"y":5740,"wires":[]},{"id":"dcc5015a105e33bb","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-ai-nodes":"0.1.6","node-red-contrib-image-tools":"2.1.1"}}] ``` :: # CIP Suite, EtherNet/IP Nodes A suite of nodes for communicating with Rockwell Automation and Allen-Bradley PLCs, and other CIP-capable devices, using the EtherNet/IP (Ethernet Industrial Protocol) protocol. ## Overview The CIP Suite connects FlowFuse flows to industrial control systems across the full spectrum of Rockwell Automation hardware, from modern ControlLogix and CompactLogix controllers to legacy SLC 500 and PLC-5 systems. It provides dedicated nodes for reading and writing tags, continuous tag monitoring, tag discovery, controller management, and advanced CIP objects covering motion, energy, time synchronization, security, and more. This is a **FlowFuse Certified Node**. Unlike community nodes, which vary in quality and can go unmaintained without warning, FlowFuse vets Certified Nodes for quality, security, and support, and maintains them on an ongoing basis. [Read more about Certified Nodes](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). It is built on the [st-ethernet-ip](https://www.npmjs.com/package/st-ethernet-ip){rel=""nofollow""} protocol driver. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The CIP Suite is not available by default. It is part of the FlowFuse Edge Certified Nodes catalogue, which is part of the **FlowFuse Edge** offering. Please contact our sales team at [Contact us](https://flowfuse.com/contact-us/) to learn more or to request access. ::: :: ## What is EtherNet/IP? EtherNet/IP (Ethernet Industrial Protocol) is an industrial network protocol that adapts the Common Industrial Protocol (CIP) to standard Ethernet. It's the primary communication protocol used by Rockwell Automation and Allen-Bradley industrial controllers and devices. ## Supported Hardware | Platform | Protocol | Notes | | -------------------------------- | ------------- | --------------------------------------- | | **ControlLogix** (L6x, L7x, L8x) | CIP Symbolic | Slot-based backplane routing | | **CompactLogix** (L1x, L2x, L3x) | CIP Symbolic | Typically slot 0 | | **Micro800** (Micro820/850/870) | CIP Symbolic | No backplane; enable Micro800 mode | | **SLC 500** | PCCC over CIP | File-based addressing (N7:0, F8:0) | | **MicroLogix** (1100/1400) | PCCC over CIP | File-based addressing | | **PLC-5** | PCCC over CIP | File-based addressing | | **Third-party CIP devices** | CIP Raw | Any EtherNet/IP device with CIP objects | ## Use case Manufacturers running Allen-Bradley and Rockwell controllers typically have production data trapped on the plant floor. It's visible on an HMI, but invisible to anything above it. The CIP Suite closes that gap in both directions: it reads live tag data off the PLC into your flows, and lets flows write values and commands back when your use case requires it. ### Example: tracking line performance in real time A CompactLogix controller on a packaging line exposes tags for `ProductionCount`, `ConveyorRunning`, and `RejectCount`. Without this suite, getting that data into a dashboard or historian usually means an OT engineer manually configuring a separate SCADA tag, or a nightly CSV export that's already stale by morning. With the CIP Suite, the flow looks like this: 1. **Connect**: a `cip-endpoint` configuration node holds the shared connection to the PLC, with automatic reconnection. 2. **Subscribe**: a `cip-subscribe` node scans the tags cyclically and emits messages on change, with deadband filtering to suppress noise. 3. **Enrich**: a `function` node attaches a line ID, timestamp, or shift code, and converts raw units into something business-readable. 4. **Deliver**: the enriched payload is published to an MQTT broker or Unified Namespace, written to a historian, or dropped straight into [FlowFuse Tables](https://flowfuse.com/docs/flowfuse-nodes/flowfuse-tables/). The result: production counts, machine states, and reject rates are available to dashboards, analytics, and other systems within moments of changing on the floor. No polling scripts, no OPC server to license and maintain, no manual tag mapping in a separate SCADA package. ### Where this shows up in practice - **OEE and downtime tracking**: feed `ConveyorRunning` and cycle-time tags into a calculation flow to surface availability and performance losses as they happen, instead of reconstructing them from end-of-shift reports. - **Live operations dashboards**: wire tag changes straight into [FlowFuse Dashboard](https://flowfuse.com/platform/dashboard/) widgets so operators and supervisors see machine state and counts update in real time. - **Recipe and setpoint downloads**: use `cip-write` to push new setpoints, batch parameters, or recipe values to the PLC from an MES, a dashboard form, or a database lookup. - **Threshold alerting**: route a `Temperature` or `Pressure` tag through a `switch` node and fire a Slack, Teams, or email notification the moment a value crosses a quality limit. - **Unified Namespace / MQTT bridging**: treat the PLC as a data source feeding a plant-wide UNS, so IT and OT systems consume the same live tag data instead of duplicating integrations per PLC. - **Legacy modernization**: connect SLC 500, MicroLogix, and PLC-5 systems through the PCCC nodes and bring decades-old equipment into the same data pipeline as modern controllers, without touching the ladder logic. ### Why it scales beyond one PLC Only the source and sink nodes are protocol-specific. The transform stages of a flow, such as renaming tags, adding context, and routing data, don't care what protocol the data came from. The same flow pattern you build for one Allen-Bradley line can be reused for a Siemens PLC over [OPC UA](https://flowfuse.com/docs/flowfuse-nodes/edge/opcua/) or a Modbus device just by swapping the protocol nodes, which matters a lot on a mixed-vendor plant floor. ## Requirements - Node.js >= 16.0.0 - Node-RED >= 2.0.0 - Access to the FlowFuse Edge Certified Nodes catalogue (part of the **FlowFuse Edge** offering) ## Installation Because this suite is part of the FlowFuse Edge Certified Nodes catalogue, which is part of the **FlowFuse Edge** offering, make sure your account has access before installing. Contact our [sales team](https://flowfuse.com/contact-us/) if you don't. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: ### Install via the Palette Manager (recommended) 1. Open the **Palette Manager** from the top-right menu in the FlowFuse editor. 2. Switch to the **Install** tab. 3. Search for the **FlowFuse Edge Certified Nodes** collection. 4. Locate `@flowfuse-certified-nodes/cip-suite` and click **Install**. After installation, all nodes appear under the **CIP Suite** category in the palette. ## Nodes in the Suite ### Core CIP Nodes (Logix controllers) | Node | Type | Description | | ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **cip-endpoint** | Config | Shared TCP session to a Logix PLC. Auto-reconnect, Micro800 support, multi-hop routing. | | **cip-read** | In/Out | Read tag values. Supports bit access (`Tag.5`), array elements (`Tag[3]`), array ranges (`Tag[0..9]`), UDT/structures, batch reads, and polling. | | **cip-write** | In/Out | Write tag values. Supports atomic bit-level writes, arrays, UDT partial merge, and batch writes. | | **cip-browse** | In/Out | Discover tags on the PLC. Glob/regex filtering, UDT detection, program-scoped tags. | | **cip-subscribe** | Out | Continuous cyclic multi-tag scanning. Deadband filtering, report-by-exception, runtime reconfiguration. | | **cip-controller** | In/Out | Read controller identity, mode, fault status, keyswitch, and tag count. Runtime commands: run/program/test/reset. | | **cip-raw** | In/Out | Send raw CIP service requests to any CIP object, with full response parsing and human-readable status codes. | | **cip-discover** | In/Out | UDP broadcast device discovery on the local network. Standalone; no endpoint required. | ### Legacy PCCC Nodes (SLC 500 / MicroLogix / PLC-5) | Node | Type | Description | | --------------------- | ------ | --------------------------------------------------------------------------------------------------------- | | **cip-pccc-endpoint** | Config | Session to legacy controllers using EtherNet/IP with PCCC encapsulation. | | **cip-pccc-read** | In/Out | Read data-file addresses: `N7:0`, `F8:0`, `B3:0/5`, `T4:0.ACC`, `S:1/5`. Multi-element reads and polling. | | **cip-pccc-write** | In/Out | Write data-file addresses, including safe bit-level writes. | ### Advanced CIP Object Nodes | Node | Description | | ------------------ | -------------------------------------------------------------------------------------------------- | | **cip-io-scanner** | Implicit I/O with cyclic data exchange for remote I/O, drives, and servos. | | **cip-security** | Read TLS/DTLS status and security profiles from the CIP Security Object. | | **cip-sync** | IEEE 1588 PTP time synchronization: grandmaster discovery, offset monitoring, enable/disable. | | **cip-motion** | Motion Axis Object: jog, absolute/relative moves, home, stop, enable/disable, axis status polling. | | **cip-energy** | Energy monitoring: power, energy, and electrical measurements (V/A/Hz/PF/THD). | | **cip-file** | File Object: firmware upload/download, file directory listing, metadata access. | | **cip-param** | Parameter Object: device parameterization with discovery scan and scaled read/write. | ## Tag Addressing ### CIP Symbolic (Logix) | Format | Example | Description | | -------------- | ---------------------------- | ----------------------------- | | Simple | `MyTag` | Read/write a tag | | Bit access | `MyDint.5` | Read/write bit 5 of a DINT | | Array element | `MyArray[3]` | Single array element | | Array range | `MyArray[0..9]` | Read elements 0 to 9 | | Program-scoped | `Program:MainProgram.MyTag` | Tag inside a program | | Batch | `msg.tags = ["Tag1","Tag2"]` | Multi-tag read in one request | ### PCCC (SLC 500 / MicroLogix / PLC-5) | Format | Example | Description | | ----------------- | ---------------- | ---------------------------- | | Integer | `N7:0` | Integer file 7, element 0 | | Float | `F8:5` | Float file 8, element 5 | | Bit | `B3:0/5` | Bit file 3, element 0, bit 5 | | Timer | `T4:0` | Full timer (CTL/PRE/ACC) | | Timer sub-element | `T4:0.ACC` | Timer accumulator only | | Counter | `C5:0.ACC` | Counter accumulator | | Output/Input | `O:0/3`, `I:1/0` | I/O with bit access | | Status | `S:1/5` | Status file with bit | | String | `ST9:0` | String file | | Long | `L10:0` | Long integer file | ## Configuration ### cip-endpoint | Setting | Default | Description | | ------------ | ---------- | ------------------------------------------- | | IP Address | (required) | PLC IP address | | Port | 44818 | EtherNet/IP port | | Slot | 0 | Backplane slot (ControlLogix) | | Timeout (ms) | 5000 | Connection timeout | | Retry (ms) | 5000 | Reconnection interval | | Micro800 | off | Enable for Micro800 controllers | | Routing Path | (optional) | Multi-hop routing, e.g. `1/0/2/192.168.1.1` | ### cip-pccc-endpoint | Setting | Default | Description | | ------------ | ---------- | -------------------------- | | IP Address | (required) | PLC IP address | | Port | 44818 | EtherNet/IP port | | Timeout (ms) | 5000 | Connection/request timeout | | Retry (ms) | 5000 | Reconnection interval | ## Usage ### Reading tags Send a message to a `cip-read` node with the tag name configured on the node or in `msg.tagName`. The output: ```json { "payload": 1250, "tagName": "ProductionCount", "dataType": "DINT", "timestamp": 1710000000000 } ``` ### Writing tags Send a message to a `cip-write` node with the value in `msg.payload`: ```json { "payload": 42, "tagName": "TargetSpeed" } ``` Bit-level writes use the atomic CIP Read-Modify-Write service where the controller supports it, so individual bits change without race conditions. ### Subscribing to tag changes The `cip-subscribe` node scans a group of tags cyclically and emits when values change. Multi-tag output: ```json { "payload": { "Tag1": 42, "Tag2": 3.14 }, "tags": [ { "name": "Tag1", "value": 42, "type": "DINT", "changed": true }, { "name": "Tag2", "value": 3.14, "type": "REAL", "changed": false } ], "scanRate": 1000, "timestamp": 1710000000000 } ``` Deadband filtering suppresses small fluctuations on analog values so only meaningful changes flow downstream. ### Reading legacy controllers The `cip-pccc-read` node returns data-file values from SLC 500, MicroLogix, and PLC-5 controllers: ```json { "payload": 1234, "address": "N7:0", "fileType": "Integer", "timestamp": 1710000000000 } ``` ## Node Status Indicators | Color | Shape | Meaning | | ------ | ----- | --------------------- | | Green | dot | Connected / OK | | Yellow | ring | Connecting / warning | | Red | ring | Error / disconnected | | Blue | dot | Operation in progress | ## Best Practices ### Connection Management - Share a single `cip-endpoint` configuration node across all read, write, browse, and subscribe nodes talking to the same PLC. - The endpoint reconnects automatically at the configured retry interval; use a **status node** if you need to react to connection changes in your flow. - All nodes skip new requests while a previous one is in flight, which protects the PLC from overload. ### Network Configuration - Ensure your FlowFuse instance can reach the PLC IP address. - Configure firewall rules to allow TCP port 44818 (EtherNet/IP). - Use static IP addresses for PLCs in production environments. - Use the routing path setting to reach PLCs behind ControlLogix backplanes or across multiple hops. ### Tag Selection and Scan Rates - Only subscribe to tags you need to minimize network traffic. - Use appropriate scan rates based on how quickly data changes; faster scan rates increase CPU and network load. - Use deadband filtering on analog tags to avoid flooding downstream systems with insignificant changes. ### Writing Safely - Validate values in a `function` node before they reach a `cip-write` node, especially when values originate from dashboards or external systems. - Prefer bit-level writes for command bits so unrelated bits in the same word are never disturbed. - Keep safety functions and interlocks in the PLC program. Never rely on a flow to enforce a safety condition. ### Error Handling - Connect a **catch node** to handle errors gracefully. - Use the admin metrics endpoint (`GET /cip-endpoint/:id/metrics`) to monitor response times, error counts, and uptime. ## Troubleshooting ### Cannot Connect to PLC - Verify IP address and network connectivity (ping test). - Check the slot number matches your controller configuration. - For Micro800 controllers, enable Micro800 mode on the endpoint. - Verify firewall settings allow TCP 44818. ### Tags Not Updating - Confirm tag names match exactly (case-sensitive). - For program-scoped tags, use the full `Program:ProgramName.TagName` format. - For legacy controllers, verify the data-file address format (e.g. `N7:0`, not a tag name). - Check the scan rate isn't too slow for your application. ### Connection Drops Frequently - Reduce scan rates to decrease network load. - Check network stability and switch configuration. - Verify the PLC isn't overloaded with connections from other clients. # Edge Certified Nodes This section contains documentation for **FlowFuse Edge Certified Nodes** that connect your FlowFuse instances to industrial protocols, PLCs, SCADA systems, and factory-floor equipment. FlowFuse Certified Nodes are packages that FlowFuse has vetted for quality, security, and support, and maintains on an ongoing basis. To learn more about what certification means and how these nodes are delivered, [read the FlowFuse Certified Nodes blog post](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The FlowFuse Edge Certified Nodes catalogue is part of the **FlowFuse Edge** offering. [Contact us](https://flowfuse.com/contact-us/) to get access or to learn more. ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} This section is expanding. We are actively working to bring more Edge Certified Nodes to FlowFuse, and additional documentation will be added here over time. ::: :: ## Nodes This section lists the **Edge Certified Nodes** documented in FlowFuse: - [RTSP Video Feed](https://flowfuse.com/docs/flowfuse-nodes/edge/rtsp/): Documentation for the FlowFuse RTSP Video Feed node, which connects to an RTSP camera stream and extracts still frames as PNG images for use in flows, dashboards, and local AI models. - [CIP Suite, EtherNet/IP Nodes](https://flowfuse.com/docs/flowfuse-nodes/edge/cip-suite/): A suite of nodes for reading, writing, and monitoring data on Rockwell Automation and Allen-Bradley PLCs, and other CIP-capable devices, using the EtherNet/IP protocol. - [Modbus](https://flowfuse.com/docs/flowfuse-nodes/edge/modbus/): A FlowFuse-certified package for reading and writing coils and registers over Modbus TCP, Modbus UDP (where supported), and Serial (RTU/ASCII), and for simulating a Modbus server, all from within your flows. - [OPC UA for FlowFuse - FlowFuse Certified Node](https://flowfuse.com/docs/flowfuse-nodes/edge/opcua/): Connect a FlowFuse instance to industrial OPC UA servers: read, write, monitor, call methods, browse, read history, work with files, or host your own OPC UA server. A FlowFuse Certified Node. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: # Modbus Connect a FlowFuse instance to Modbus TCP and Serial devices — PLCs, sensors, meters, drives, and gateways. Poll or read coils and registers, write setpoints and commands, and simulate a Modbus server for testing, all from within your flows. This is a **FlowFuse Certified Node**. Unlike community nodes, which vary in quality and can go unmaintained without warning, FlowFuse vets Certified Nodes for quality, security, and support, and maintains them on an ongoing basis. [Read more about Certified Nodes](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). ## Get the Certified Node in FlowFuse ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The Modbus package is not available by default. It is part of the FlowFuse Edge Certified Nodes catalogue, which is part of the **FlowFuse Edge** offering. Please contact our sales team at [Contact us](https://flowfuse.com/contact-us/) to learn more or to request access. ::: :: ### Installation steps 1. Open your instance in the FlowFuse editor. 2. Click the menu icon (☰) in the top-right corner. 3. Select **Manage palette**. 4. Go to the **Install** tab. 5. Switch to the **FlowFuse Edge Certified Nodes** category. 6. Search for `@flowfuse-certified-nodes/modbus`. 7. Click **Install**. The Modbus nodes then appear in your palette, ready to drag onto the canvas. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} If your device or hosted instance was already running before Modbus was enabled for your team, it won't show the package in the Install tab search. Restart the instance or device first, then repeat the steps above. ::: :: ## What is Modbus? Modbus is one of the oldest and most widely supported industrial communication protocols, implemented by an enormous range of PLCs, sensors, power meters, VFDs, and gateways. It comes in several transports, all supported side by side: - **Modbus TCP** — Modbus messages over a standard Ethernet/IP network, typically on port 502. The framing can be the default MBAP header or RTU-buffered. - **Modbus UDP** — the same message format over UDP instead of TCP, for the devices and gateways that expect it. Support varies by device. - **Modbus Serial** — Modbus RTU (binary) or ASCII framing over RS-232/RS-485. Modbus-Client also offers TELNET and C701 modes for TCP-to-serial gateways. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Modbus now uses **client** for the device making requests and **server** for the device answering them. Older device manuals and tools use **master** and **slave** for the same two roles — a Modbus master is a client, a Modbus slave is a server. This package follows the newer terminology, so the FlowFuse instance is the client and the PLC, meter, or drive it polls is the server. ::: :: Every Modbus device exposes its data as one of four addressable table types, and every read or write targets one of them: | Table | FC (read) | FC (write) | Access | Manual-style reference | Typical use | | ----------------- | --------- | ---------- | ------------------ | ---------------------- | --------------------------------------------------- | | Coils | 1 | 5, 15 | Read/write, 1 bit | `0xxxx` / `00001+` | Digital outputs — relays, enable flags | | Discrete Inputs | 2 | — | Read-only, 1 bit | `1xxxx` / `10001+` | Digital inputs — switches, limit sensors | | Holding Registers | 3 | 6, 16 | Read/write, 16-bit | `4xxxx` / `40001+` | Setpoints, configuration, read/write process values | | Input Registers | 4 | — | Read-only, 16-bit | `3xxxx` / `30001+` | Read-only measurements — sensor readings | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} **Register addressing** Addresses in this package are raw, zero-based protocol addresses. Device manuals often number the same register differently. A manual that lists a holding register as `40001` (or `4x0001`, or `400001` on devices with more than 9,999 registers) is describing the register at address `0`. Other manuals number within the table starting at `1`, so their "register 1" is also address `0`. Check which convention your device's register map uses before entering an address — an off-by-one here is the most common cause of reading the wrong value, and an address one past the end of the table returns Illegal Data Address. ::: :: A device is also identified by a **Unit ID** (also called station address, or slave ID in older documentation), which matters when several logical devices share one connection — for example several RTU devices on the same RS-485 bus, or several logical devices behind one TCP gateway. On a serial bus, valid device addresses are **1–247**. Address **0 is the broadcast address** — a write sent to unit 0 goes to every device on the bus and none of them reply, so a read addressed to unit 0 will always time out. Addresses 248–255 are reserved. Native Modbus TCP devices often ignore the unit ID altogether (`0`, `1`, and `255` are all common), but it matters as soon as a gateway sits in front of serial devices. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Modbus itself moves only raw bits and 16-bit words — the protocol carries no data-type information. A read returns an **array** of booleans (coils/discrete inputs) or **raw 16-bit register values, each `0`–`65535`,** (input/holding registers). Anything larger or more structured — 32-bit integers, floating-point values, scaled measurements — spans multiple registers: the device packs it across them, and you reconstruct it in your flow. See [Decoding register values](https://flowfuse.com/#decoding-register-values) for how to do this. ::: :: ## Use case Most Modbus devices already hold the data a plant needs — tank levels, motor status, energy readings, setpoints — but that data is normally only visible to a single SCADA package or the device's own local panel. This certified node package turns a FlowFuse instance into a Modbus client (and, where useful, a Modbus server), so that data becomes ordinary flow data you can route, transform, and act on alongside everything else in Node-RED. ### Example: bringing a legacy PLC's data onto MQTT An older PLC exposes tank level, pump status, and a fault word through its holding registers, with no other way off the machine except a proprietary HMI cable. A single **Modbus-Client** connection points at the PLC over TCP. A **Modbus-Read** node polls the holding registers on a fixed interval, emitting the raw register values as an array. Because those registers represent more than plain 16-bit integers (tank level as a float spanning two registers, a fault word to be split bit by bit), a downstream **function node** — or a dedicated buffer-parser node — decodes the raw buffer into named, typed values. Those values are published to the plant's MQTT broker, so dashboards and historians see live data from a PLC that was never designed to leave the panel. A **Modbus-Flex-Write** node, gated behind an operator confirmation on a dashboard, lets the same flow push a new setpoint back down to the PLC. ### Where this shows up in practice - **Polling process data**: use [Modbus-Read](https://flowfuse.com/#modbus-read) to cyclically poll coils and registers at a fixed rate and feed the results straight into a Unified Namespace, historian, or dashboard. - **On-demand reads**: use [Modbus-Getter](https://flowfuse.com/#modbus-getter-and-modbus-flex-getter) to read a value only when triggered by an incoming message, instead of continuously polling. - **Dynamic, message-driven addressing**: use [Modbus-Flex-Getter](https://flowfuse.com/#modbus-getter-and-modbus-flex-getter) when the table, address, quantity, or unit ID needs to change per message, for example a generic "read any register on any configured device" flow. - **Ordered multi-range reads**: use [Modbus-Flex-Sequencer](https://flowfuse.com/#modbus-flex-sequencer) to read several different ranges in a defined order through one node, instead of wiring up many separate reads. - **Writing setpoints and commands**: use [Modbus-Write and Modbus-Flex-Write](https://flowfuse.com/#modbus-write-and-modbus-flex-write) to set coils or holding registers from a dashboard form, an MES, or a calculated value elsewhere in the flow. - **Decoding raw registers**: turn raw 16-bit register arrays into signed/unsigned integers, 32-bit values, or floating-point numbers — see [Decoding register values](https://flowfuse.com/#decoding-register-values). - **Testing and simulation**: use [Modbus-Server](https://flowfuse.com/#modbus-server) to run a buffer-backed Modbus server, so you can build and test a flow against a virtual device before any real hardware is available. ## The Node Set Each node performs one Modbus operation and reuses the shared connection you configure once on **Modbus-Client**. | Node | Purpose | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Modbus-Client (config) | Defines the connection — TCP host/port (with default/RTU-buffered/UDP/TELNET/C701 framing) or serial port/baud rate, timeout, and reconnect behaviour. Every other node references it. | | Modbus-Read | Polls a fixed table/address/quantity on a repeating interval and emits the raw result on every poll. | | Modbus-Getter | Reads a fixed table/address/quantity, but only when triggered by an incoming message rather than on a timer. | | Modbus-Flex-Getter | Like Modbus-Getter, but the table, address, quantity, and unit ID are taken from the incoming message, so one node can serve many different reads. | | Modbus-Flex-Sequencer | Reads several configured ranges in a defined order through a single node. | | Modbus-Write | Writes a single coil or holding register, or a block of them, using addressing fixed on the node. | | Modbus-Flex-Write | Like Modbus-Write, but the target table, address, and value(s) are taken from the incoming message. | | Modbus-Flex-Fc | Sends custom or less-common function codes using configurable argument maps, for devices that need function codes beyond the standard read/write set. | | Modbus-Flex-Connector | Changes the connection endpoint (host/port or serial settings) at runtime from an incoming message, to reconnect or switch devices without redeploying. | | Modbus-Response | Displays the status and content of a Modbus response in the editor, as a diagnostic/inspection aid downstream of a read. | | Modbus-IO-Config (config) | Holds a JSON IO file mapping IEC-style addresses (`%QW0`, `%IX8.0`, …) to typed names, where a name's first letter sets its data type. Read nodes can attach it to their output. | | Modbus-Response-Filter | Filters an IO-mapped payload down to named values, so downstream nodes receive only the fields they need. | | Modbus-Server | Runs a buffer-backed Modbus TCP server inside Node-RED, so it responds to reads and writes from an external Modbus client — useful for testing and simulation. | | Modbus-Queue-Info | Reports (and can reset) the internal request queue depth for a connection, useful for spotting a device that can't keep up with the configured poll rate. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} All nodes that share the same Modbus-Client connection share one underlying socket/serial port and are queued through it in order, so a device is never sent two requests at once. A busy queue (see Modbus-Queue-Info) usually means the poll rate is faster than the device can respond to. ::: :: ## Configure a Modbus Client Connection Every node in this package uses a **Modbus-Client** configuration node to communicate with a Modbus device. Create the connection once, then reuse it across every Read, Getter, Write, and Server node that connects to the same device or bus. 1. Drag any Modbus node (for example, **Modbus-Read**) onto the canvas and double-click it. 2. Next to the **Server** field, click the **+** icon to create a new **Modbus-Client** configuration. 3. Select the connection **Type**: - **TCP** — Enter the device's IP address and port (default: `502`). Then choose the **TCP Type**: **Default**, **RTU Buffered**, **UDP**, **TELNET**, or **C701** (for TCP-to-serial gateways). - **Serial** / **Serial Expert** — Select the serial port, baud rate, parity, data bits, stop bits, and the Modbus framing (**RTU** or **ASCII**). 4. Configure the remaining connection settings: - **Unit ID** — Default: `1`. Serial devices use `1`–`247` (`0` is broadcast); TCP accepts `0`–`255`. - **Timeout** — Default: `1000` ms - **Reconnect Timeout** — Default: `2000` ms 5. Click **Done**, then **Deploy** the flow. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The connection is shared. Changing its parameters affects every node that uses it, and you must redeploy the flow for connection changes to take effect. To change the endpoint at runtime without redeploying, see [Modbus-Flex-Connector](https://flowfuse.com/#modbus-flex-connector). ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Keep host/IP addresses and serial port paths in FlowFuse Environment Variables (your instance's **Settings → Environment**) rather than hard-coding them in the connection, so the same flow can be promoted across instances that reach the device differently. ::: :: ## Modbus-Read The Modbus-Read node polls a fixed location on a repeating interval — the node acts continuously once deployed, with no input needed. ### Configuration - **FC (Function Code)** — which table to read: Coils, Discrete Inputs, Holding Registers, or Input Registers (FC 1–4). - **Address** — the starting zero-based address in that table — see [Register addressing](https://flowfuse.com/#what-is-modbus) if your device manual or documentation uses 4xxxx-style numbering. - **Quantity** — how many consecutive coils/registers to read in one request. - **Poll rate** — how often to repeat the read (milliseconds). - **Unit ID** — overrides the connection's default unit ID for this node, when needed. Each poll emits a message carrying the raw values on `msg.payload` (an array of booleans for coils/discrete inputs, or an array of raw 16-bit register values, each `0`–`65535`, for registers) and the raw response buffer. If the register values represent something other than plain 16-bit integers, decode the buffer downstream — see [Decoding register values](https://flowfuse.com/#decoding-register-values). ```text [Modbus-Read] (polls every 1000ms) → [function / buffer-parser] → [Debug] ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Set the poll rate no faster than the device can reliably answer. A rate that outpaces the device causes requests to back up in the queue, check [Modbus-Queue-Info](https://flowfuse.com/#modbus-queue-info) if reads start arriving late or with gaps. ::: :: ## Modbus-Getter and Modbus-Flex-Getter Use these nodes instead of Modbus-Read when a read should happen on demand rather than on a timer. - **Modbus-Getter** — configuration (FC, address, quantity, unit ID) is fixed on the node, exactly like Modbus-Read, but the node only reads when it receives an input message. Useful for occasional, event-driven reads, for example reading a value only when a dashboard page opens. - **Modbus-Flex-Getter** — the same operation, but every parameter is taken from the incoming message (`msg.payload` carries `fc`, `address`, `quantity`, and `unitid`) instead of the node's own configuration, so one node can serve reads against many different addresses, tables, or devices. The result comes back on `msg.payload` (an array of values) with the request parameters echoed alongside it, so a Flex-Getter response can be routed and identified even when many different reads share the same node. Both getter nodes support read function codes 1, 2, 3, and 4. ## Modbus-Flex-Sequencer Modbus-Flex-Sequencer reads several configured ranges in order through a single node, triggered by any incoming message. Each range in the sequence is defined by `unitid`, `fc` (function code, given as `FC1`–`FC4` or `1`–`4`), `address`, and `quantity`, and supports read function codes 1–4 only. The sequence is configured as a list on the node, but can be overridden per-message by supplying `msg.sequences`. It is useful when a device's data of interest is spread across several non-contiguous ranges (or tables) that you would otherwise have to read with several separate nodes and then recombine. Each range is read in turn over the shared connection, respecting the same queue as every other node on that connection. ## Modbus-Write and Modbus-Flex-Write - **Modbus-Write** — the target table (Coils or Holding Registers), address, and quantity are fixed on the node; the value(s) to write come from the incoming message. Uses write function codes FC 5/6 (single coil/register) and FC 15/16 (multiple). - **Modbus-Flex-Write** — the table, address, and value(s) are all taken from the incoming message, so a single node can write to different coils/registers depending on what triggers it. ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} Writes to production equipment change real-world state. Gate write nodes behind validation or an operator confirmation step before deploying to a live instance. ::: :: ## Modbus-Flex-Fc Some devices expose data through function codes outside the standard read/write set. Modbus-Flex-Fc lets you send those custom or less-common function codes using configurable argument maps, so you can talk to a device whose behaviour isn't covered by the ordinary Read/Write/Getter nodes. This is an advanced node — you'll need the exact request format from the device's Modbus documentation. ## Modbus-Flex-Connector Modbus-Flex-Connector changes the active connection's endpoint at runtime from an incoming message — for example switching the target host/port or serial settings, or forcing a reconnect — without editing the Modbus-Client config and redeploying. This is useful for flows that must talk to one of several devices in turn over a single set of nodes, or that need to recover a connection programmatically. ## Decoding register values Modbus registers are just 16-bit words — the protocol itself carries no data-type information, so a temperature reading, a setpoint, and a status word are all indistinguishable raw integers until you interpret them. A read node therefore hands your flow an **array of raw 16-bit register values** (or booleans for coils and discrete inputs). Turning those into meaningful values — signed integers, 32-bit integers or floats spanning two registers, scaled measurements, or a status word split into individual bits — is done **downstream in your flow**, not by the Modbus read node itself. There are a few common approaches: - **A function node** — reconstruct the value in JavaScript. `msg.payload` is an array of numbers, not a Buffer, so `Buffer` methods aren't available on it directly. Either read the raw bytes from the response buffer the node attaches to the message, or build a Buffer from the values first, writing each register big-endian to match the wire order: ```js const input = msg.payload // array of 16-bit register values const buf = Buffer.alloc(input.length * 2) input.forEach((reg, i) => buf.writeUInt16BE(reg, i * 2)) msg.payload = {} // decode into a new object msg.payload.temperature = buf.readFloatBE(0) // registers 0–1 as a 32-bit float msg.payload.counter = buf.readUInt32BE(4) // registers 2–3 as a 32-bit unsigned int msg.payload.setpoint = buf.readInt16BE(8) // register 4 as a signed 16-bit int return msg ``` :brAvoid `Buffer.from(new Uint16Array(msg.payload).buffer)` — that uses the host's byte order, which may be little-endian depending on where Node-RED runs, and silently gives you byte-swapped values. - **A buffer-parser node** — a dedicated parsing node (such as `node-red-contrib-buffer-parser`) accepts the values array directly, so you don't have to deal with the Buffer question at all. It lets you declare each field's type and endianness in configuration rather than in code, which is convenient when a single response contains many differently-typed fields. - **The package's built-in IO mapping** — a **Modbus-IO-Config** node holds a JSON file that maps IEC-style addresses (`%QW0`, `%IX8.0`, and so on) to names, where the first character of each name selects the data type (`i` integer, `w` word, `u` unsigned, `b` boolean, `f`/`r` float, and so on). A read node can attach that mapping to its output, and **Modbus-Response-Filter** narrows the result down to the named fields a downstream node needs. This is convenient when your address list originates from a PLC export. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Choose the data type and word order that matches how the device packs its data, which is usually documented in the device's Modbus register map. If a decoded value looks wildly wrong (for example a temperature reading in the millions), the most common cause is a word-order mismatch on a multi-register value. Try the other byte/word order before assuming the device or wiring is at fault. ::: :: ## Modbus-Response The Modbus-Response node displays the status and content of a Modbus response in the editor. Placed downstream of a read node, it is an inspection/diagnostic aid that shows what came back, which is helpful while building and debugging a flow. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Modbus-Response is for **display and inspection** — it does not convert raw registers into typed values. To turn raw registers into integers, floats, or scaled measurements, decode them in your flow as described in [Decoding register values](https://flowfuse.com/#decoding-register-values). ::: :: ## Modbus-Server The Modbus-Server node runs a buffer-backed Modbus TCP server inside your FlowFuse instance, responding to reads and writes initiated by an external Modbus client (a PLC, SCADA system, or test tool). Fixed-size in-memory buffers back the holding registers, coils, input registers, and discrete inputs, which suits development and testing rather than full device emulation. This is primarily useful for: - **Testing flows without hardware** — stand up a server so a Modbus-Read/Write flow can be built and verified before real hardware is available. - **Local integration testing** — exercise read and write paths end to end against a server running in the same instance. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The in-package Modbus-Server is a **buffer-backed server for demos and tests**. It is not intended to replicate a specific device's complete register-map behaviour. For more elaborate simulation needs, a dedicated Modbus simulation tool is a better fit. ::: :: ## Modbus-Queue-Info Each Modbus-Client connection processes its requests through a single ordered queue. Modbus-Queue-Info reports the current depth of that queue (and can reset it), which is the quickest way to spot a connection whose poll rate is faster than the device — or shared bus — can service. A queue that grows over time means requests are arriving faster than replies, and the poll rate should be lowered. ## Network Requirements When the certified node runs inside a corporate or industrial network, allow the following outbound connections through your firewall or URL-filtering proxy. Share this section with your IT or OT/security team. | Hostname / Target | Port | Protocol | Purpose | Required? | | ----------------------------- | ------------- | --------------- | ------------------------------------------------ | ----------------------------- | | Your Modbus TCP/UDP device(s) | typically 502 | TCP / UDP | Modbus TCP/UDP data plane | Yes, for TCP/UDP connections | | Local serial port | — | RS-232 / RS-485 | Modbus RTU/ASCII data plane | Yes, for Serial connections | | `registry.npmjs.org` | 443 | HTTPS | Package installation (one-time, via the palette) | Standard Node-RED requirement | The Modbus TCP/UDP port (default 502) is configurable per device; update the firewall rule if your device uses a non-default port. The certified node does not make any external licence-check or activation calls — licensing is managed through FlowFuse. ## Troubleshooting | Symptom | What to check | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Connection never establishes | Verify the IP/port (TCP/UDP) or serial port/baud rate/parity (Serial) match the device, and that the device is reachable on the network or physically wired. | | Illegal Function (exception code 1) | The device does not support the function code you're using — check its register map for which FCs it implements. | | Illegal Data Address (exception code 2) | The address (or address + quantity) falls outside the device's supported range for that table. | | Illegal Data Value (exception code 3) | The value being written is out of range for the target register/coil. | | Slave Device Failure (exception code 4) | The protocol's name for a server-side fault — an internal fault on the device itself. Check the device's own diagnostics. | | Requests time out intermittently | Lower the poll rate, check for other clients competing for the same device, and confirm the timeout setting is generous enough for the device's actual response time. | | Decoded values look wrong (huge, negative, or garbled) | Check the data type and byte/word order used in your decoding step against the device's documented register format — see [Decoding register values](https://flowfuse.com/#decoding-register-values). | | Requests appear delayed or bunched up | Check [Modbus-Queue-Info](https://flowfuse.com/#modbus-queue-info) — the poll rate is likely faster than the device (or shared bus) can keep up with. | | Serial connection works from a test tool but not from FlowFuse | Confirm no other process holds the serial port open, and that the FlowFuse instance's OS user has permission to access it. | | Multiple devices on one connection interfere with each other | Confirm each node/device uses the correct **Unit ID** — a wrong unit ID silently talks to the wrong logical device on a shared bus or gateway. | For help enabling the Modbus Certified Node, licensing, or anything else, [contact FlowFuse](https://flowfuse.com/contact-us/). # OPC UA for FlowFuse - FlowFuse Certified Node Connect a FlowFuse instance to industrial OPC UA servers. Read and write values, monitor changes in real time, call methods, browse the address space, read history, work with files, or host your own OPC UA server, all from within your flows. This is a **FlowFuse Certified Node**. Unlike community nodes, which vary in quality and can go unmaintained without warning, FlowFuse vets Certified Nodes for quality, security, and support, and maintains them on an ongoing basis. [Read more about Certified Nodes](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). ## Table of contents 1. [Overview](https://flowfuse.com/#_1-overview) 2. [Get the certified node in FlowFuse](https://flowfuse.com/#_2-get-the-certified-node-in-flowfuse) 3. [The node set](https://flowfuse.com/#_3-the-node-set) 4. [NodeIds and how to address data](https://flowfuse.com/#_4-nodeids-and-how-to-address-data) 5. [Configure a connection](https://flowfuse.com/#_5-configure-a-connection) 6. [Read](https://flowfuse.com/#_6-read) 7. [Write](https://flowfuse.com/#_7-write) 8. [Extension Object](https://flowfuse.com/#_8-extension-object) 9. [Call](https://flowfuse.com/#_9-call) 10. [Monitor](https://flowfuse.com/#_10-monitor) 11. [Monitor Event](https://flowfuse.com/#_11-monitor-event) 12. [Browse](https://flowfuse.com/#_12-browse) 13. [Explore](https://flowfuse.com/#_13-explore) 14. [History Read](https://flowfuse.com/#_14-history-read) 15. [File Operation](https://flowfuse.com/#_15-file-operation) 16. [Hosting an OPC UA server](https://flowfuse.com/#_16-hosting-an-opc-ua-server) 17. [Network requirements](https://flowfuse.com/#_17-network-requirements) 18. [Troubleshooting](https://flowfuse.com/#_18-troubleshooting) ## 1. Overview OPC UA (Unified Architecture) is the leading standard for industrial communication, supported by thousands of devices and systems. This certified node lets a FlowFuse instance act as an OPC UA client and, on self-hosted FlowFuse, as an OPC UA server. Everything runs inside your FlowFuse instance. Connections, certificates, and credentials live alongside the rest of your flow configuration. Define a connection once and every OPC UA node in the instance shares it. Client and server nodes share a single certificate store, so a trust decision you make for one applies to the other. ### Capabilities - Connect to one or many OPC UA servers, with optional security and authentication. - Read single values, all attributes, arrays, or values mapped into a structured object. - Write single values, batches, arrays, matrices, and complex extension-object structures. - Monitor in real time through subscriptions, with deadband filtering and event/alarm monitoring. - Call methods to drive device operations. - Discover the address space through Browse (one level) and Explore (recursive). - Access historical data, raw, modified, and aggregated. - Read, write, and append files on servers that implement the OPC UA FileType interface. - Host an OPC UA server from within a self-hosted FlowFuse instance using Function nodes. ### Use case Most industrial equipment already speaks OPC UA, but the data usually stays inside the machine's own server: readable by the vendor's tools, invisible to dashboards, brokers, and databases. This certified node closes that gap. A FlowFuse instance connects to the equipment as an OPC UA client, so the values, alarms, and history locked inside each machine become ordinary flow data you can route, transform, and act on. On self-hosted FlowFuse it also works in the other direction, exposing flow data as an OPC UA server for SCADA systems and historians to consume. #### Example: connecting a filling machine to the plant A filling machine exposes `FillLevel`, `LineSpeed`, and an alarm hierarchy through its embedded OPC UA server. Today that data lives on the machine's own panel. With one shared connection, a Monitor node streams the process values into the plant's Unified Namespace over MQTT, with a deadband filter suppressing sensor jitter before it ever reaches the network. A Monitor Event node subscribed with `Severity >= 600` routes machine alarms to the maintenance team's chat channel. And when the recipe changes, a Write node, gated behind an operator confirmation on a dashboard, pushes the new fill setpoint back to the machine. One connection replaces a per-machine SCADA tag mapping exercise, and the same flow pattern repeats for every OPC UA machine on the floor. #### Where this shows up in practice Real deployments usually combine several of the nodes on a single shared connection (see [The node set](https://flowfuse.com/#_3-the-node-set) for what each one does): - **Unified Namespace and broker integration**: subscribe to the variables that matter with [Monitor](https://flowfuse.com/#_10-monitor) and publish changes to MQTT, so every system in the plant consumes the same live equipment data instead of integrating with each machine separately. - **Supervisory control**: fetch current values on demand with [Read](https://flowfuse.com/#_6-read), push setpoints and recipe parameters back with [Write](https://flowfuse.com/#_7-write), and invoke machine operations such as starting a batch or acknowledging an alarm with [Call](https://flowfuse.com/#_9-call). Writes change real-world state, so gate them behind validation or an operator confirmation step. - **Alarm-driven maintenance**: subscribe to a server's alarms and events with [Monitor Event](https://flowfuse.com/#_11-monitor-event), filter by type and severity on the server side, and route what remains to notifications, dashboards, or logs, so the maintenance team hears about a fault from the machine, not from the operator. - **Shift reports, trends, and audits**: pull raw or server-aggregated values over a time range with [History Read](https://flowfuse.com/#_14-history-read) to build end-of-shift reports, quality trends, and incident timelines from data the equipment already recorded. - **Fast commissioning**: map a machine's entire address space with [Explore](https://flowfuse.com/#_13-explore) and feed the result straight into a Monitor node, turning hours of manual tag mapping into a single deploy. - **Exposing flow data to SCADA and historians**: on self-hosted FlowFuse, [host an OPC UA server](https://flowfuse.com/#_16-hosting-an-opc-ua-server) so external OPC UA clients, including another FlowFuse instance, can read the data your flows produce. This is not available on FlowFuse Cloud. ## 2. Get the Certified Node in FlowFuse FlowFuse delivers Certified Nodes to your instances as a managed catalogue. The OPC UA package is part of the FlowFuse Edge Certified Nodes catalogue, which is part of the **FlowFuse Edge** offering. [contact us](https://flowfuse.com/contact-us/) to enable it for your team ### Installation steps 1. Open your instance in the FlowFuse editor. 2. Click the menu icon (☰) in the top-right corner. 3. Select **Manage palette**. 4. Go to the **Install** tab. 5. Switch to the **FlowFuse Edge Certified Nodes** category. 6. Search for `@flowfuse-certified-nodes/opcua`. 7. Click **Install**. The OPC UA nodes then appear in your palette, ready to drag onto the canvas. !["The OPC UA nodes in the Node-RED palette"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/node-palette.png "The OPC UA nodes in the Node-RED palette"){dataZoomable=""} ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Keep endpoint URLs, usernames, and passwords in FlowFuse Environment Variables (your instance's **Settings → Environment**) rather than hard-coding them in nodes. This keeps credentials out of your flow JSON and lets you promote the same flow across instances. ::: :: ## 3. The Node Set Each node performs one OPC UA operation and reuses the shared connection you configure once. | Node | Purpose | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | OPC UA Client (config) | Defines the server endpoint, security, authentication, subscription parameters, and namespace aliases. Every other node references it. | | Read | Fetch a value, all attributes, an array of values, or a structured object, on demand, triggered by an input message. | | Write | Send single values, batches, arrays, matrices, or extension objects to writable variables. | | Extension Object | Builds a correctly encoded ExtensionObject from a plain JavaScript object before a Write. | | Call | Invoke a server method with input arguments and receive its output arguments. | | Monitor | Subscribe to variable value changes and emit a message on each change, with optional deadband filtering. | | Monitor Event | Subscribe to events and alarms, with server-side Where/Select filtering. | | Browse | List the references (children) of a single node, one level. | | Explore | Recursively traverse a subtree and return its structure as JSON. | | History Read | Retrieve raw, modified, or aggregated historical data over a time range. | | File Operation | Read, write, append, or size files on servers implementing the FileType interface. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Properties of the input message take precedence over a node's own configuration. Configure a node statically, or drive it dynamically from upstream messages. ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Each node reports its state in the editor with a coloured status dot, grey (not connected), blue (operation in progress), green (success), red (failure), and on some nodes (such as History Read) yellow (partial success or quality issues). Watch it alongside the debug sidebar when wiring up a flow. ::: :: ## 4. NodeIds and How to Address Data A NodeId is the unique identifier of a node in a server's address space. It has a namespace index, an identifier type, and an identifier value. The certified node accepts several formats: | Format | Example | | ----------------------- | ---------------------------------------------------- | | Numeric | `ns=2;i=1001` | | String | `ns=2;s=TemperatureSetpoint` | | GUID | `ns=2;g=12345678-1234-1234-1234-123456789abc` | | Unified (namespace URI) | `nsu=http://opcfoundation.org/UA/ADI;i=1234` | | Browse path | `/Objects/2:DeviceSet/3:MyDevice/3:Temperature` | | Aliased browse path | `/Objects/di:DeviceSet/ns3:MyDevice/ns3:Temperature` | | Verified browse path | `[/2:MyDevice/3:Temperature](ns=1;s=Temperature)` | When you give a browse path, the node resolves it against the server's address space and caches the result, so there is only a small one-time cost on the first read or after a reconnect. The cache clears when the connection is re-established, in case the server's address space has changed. Use the flask button to verify a NodeId or browse path; use the `...` button to browse the live server. These buttons work only once the endpoint is configured and the flow is deployed. !["A verified browse path in the NodeId field, with the browse and verify buttons"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/nodeid-as-browse-path.png "A verified browse path in the NodeId field, with the browse and verify buttons"){dataZoomable=""} Clicking the browse (`...`) button opens the live address space, so you can pick a node visually instead of typing its NodeId: !["Browsing the live server address space to select a NodeId"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/use-node-browser-to-select-node-id.png "Browsing the live server address space to select a NodeId"){dataZoomable=""} ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} Namespace indexes are not guaranteed stable across servers. If you move a flow between servers, prefer browse paths or the namespace alias table over hard-coded numeric namespace indexes. ::: :: ### Browse path syntax Every browse path starts with a reference-type separator and then a sequence of browse names. The certified node follows the OPC Foundation Part 4 specification. - **Namespace qualification**, every browse name needs a namespace-index prefix (`2:DeviceSet`) **except** names in the standard namespace 0, where the prefix is optional. Omitting a required prefix is the most common cause of a path that won't resolve. - **Hierarchical references (`/`)**, follow any hierarchical reference: `/Objects/2:DeviceSet/3:MyDevice/3:Temperature`. - **Aggregates references (`.`)**, follow an Aggregates (component/property) reference: `/Objects/Server.NamespaceArray`. - **Specific reference types (``)**, follow one named reference type: `2:Boiler/2:Temperature`. - **Reference-type modifiers**, `<#HasComponent>` follows only that exact type, not its subtypes; `2:Parent` follows the reference in the inverse direction; a namespace-qualified reference type uses `<0:HasProperty>`. - **Wildcards**, omit the final browse name to match all targets of the last step: `/Objects/2:Server/`. - **Escaping special characters**, escape `/ . : & < > # !` inside a browse name with a leading `&`. A node literally named `Device.Name` becomes `Device&.Name`. ### Browse path errors | Status code | Likely cause | | ------------------------- | --------------------------------------------------------------------------------------------------------- | | BadBrowseNameInvalid | A missing required namespace prefix, an unescaped special character, or a case mismatch in a browse name. | | BadNoMatch | The full path does not exist on the server, or a namespace index is wrong. | | BadReferenceTypeIdInvalid | A reference type name in the path is unknown or not namespace-qualified. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Browse paths are convenient and portable, but resolution has a cost. For variables you read or write frequently, resolve the path once and use the returned NodeId, or use the verified-browse-path format that caches the NodeId alongside the path. ::: :: ## 5. Configure a Connection Every OPC UA node depends on a connection defined by the **OPC UA Client** configuration node. Create it once and reference it through the endpoint field on any node. 1. Drag any OPC UA node (e.g. Read) onto the canvas and double-click to edit it. Every node carries an **Endpoint** field. :br!["The Endpoint field on a Read node"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/read-showing-endpoint-field.png "The Endpoint field on a Read node"){dataZoomable=""} 2. Open the endpoint dropdown and choose **add new opcua endpoint**, or click `[+]`. :br!["Adding a new OPC UA endpoint from the dropdown"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/endpoint-add-new.png "Adding a new OPC UA endpoint from the dropdown"){dataZoomable=""} 3. Enter the endpoint URL, for example `opc.tcp://192.168.1.10:4840`, and the security and authentication settings. Only the `opc.tcp://` protocol is supported; `http`/`https` endpoint URLs are not. 4. Click **Save**, then **Done**. 5. Click **Deploy**, then open the debug sidebar to confirm data flows. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The connection is shared. Changing its parameters affects every node that uses it, and you must redeploy the flow for connection changes to take effect. ::: :: ### Connection editor areas - **Check the connection**, confirm the endpoint is reachable before building on it. A successful test reports the connection as established: :br!["The Check Connection button reporting a successful connection"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/endpoint-verification.png "The Check Connection button reporting a successful connection"){dataZoomable=""} - **Secure connection**, choose a security policy and message security mode, then an authentication method. - **Custom certificate**, supply your own certificate and private key when the server requires mutual trust. - **Subscription parameters**, set publishing interval, lifetime, keep-alive, max notifications per publish, and priority once; every Monitor and Monitor Event node inherits them. The connection ships with three subscriptions, **Default** (1000 ms publishing interval), **Fast** (500 ms), and **Slow** (5000 ms), and you can add more for items that need a different publishing rate. The default subscription cannot be deleted, as it also drives keep-alive. :br!["The Subscriptions tab of the connection editor"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/endpoint-subscriptions.png "The Subscriptions tab of the connection editor"){dataZoomable=""} - **Namespace alias table**, map a stable short alias to a namespace URI so your NodeIds survive server reassignments. The **Extract** button reads the server's namespace array and fills the table for you; you can press it again after a server change without losing aliases you already set. ### Security policy and message mode - **Security policy**, the cipher suite used to protect the channel. Common values are `None`, `Basic256`, and `Basic256Sha256` (the modern SHA-256 choice; prefer it over the older SHA-1-based `Basic256`). - **Message security mode**, `None` sends messages in cleartext; `Sign` adds integrity (messages are signed but not encrypted); `SignAndEncrypt` adds both integrity and confidentiality. Match the policy and mode the server advertises. - **Authentication**, connect anonymously, with a username and password, or with an X.509 certificate. If credentials are wrong or the account lacks rights, the check reports `BadUserAccessDenied`: :br!["A connection rejected with BadUserAccessDenied"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/endpoint-verification-user-access-denied.png "A connection rejected with BadUserAccessDenied"){dataZoomable=""} ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} A username and password sent over message security mode `None` travel in cleartext. Combine credentials with at least `Sign` mode. ::: :: ### Certificate trust Secure connections rely on mutual certificate trust. By default the client side automatically accepts the server's certificate, so the step that usually needs action is the reverse: **the server must trust the client's certificate**. For a `Sign` or `SignAndEncrypt` connection, download the client certificate from the connection editor (in PEM or DER format) and add it to your OPC UA server's trusted list, otherwise the server refuses the connection. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Auto-accepting the server certificate is convenient but means the client does not verify the server's identity. To enforce server-certificate validation, set `rejectUnauthorized` to `true` in the connection node's global settings. The client then refuses any server whose certificate is not already in its trusted store, so you must add the server's certificate to the client side first. ::: :: Client and server nodes in your instance share one PKI store. On FlowFuse it lives under `/opcua-for-flow-fuse/PKI`. For trust decisions to survive restarts and redeploys, that directory must be on persistent storage, see [Hosting an OPC UA server](https://flowfuse.com/#_16-hosting-an-opc-ua-server) for the storage details, which apply to client connections too. If the client certificate is not yet trusted by the server, **Check Connection** reports the handshake failure and reminds you to add the client certificate to the server's trusted list: !["A secure connection failing because the certificate is not yet trusted"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/endpoint-verification-secure-error.png "A secure connection failing because the certificate is not yet trusted"){dataZoomable=""} ## 6. Read The Read node fetches data on demand, it acts only when it receives an input message. ```text [Inject] → [Read] → [Debug] ``` !["The Read node configuration with endpoint, AttributeId, and NodeId fields"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/opcu-client2-read.png "The Read node configuration"){dataZoomable=""} ### NodeId source (priority order) 1. `msg.nodeId` 2. `msg.topic` 3. `msg.payload` 4. The node's configured NodeId ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} A default Inject node sets `msg.payload` to a timestamp, which silently overrides the node's configured NodeId (priority rule 3). To use the node's own configuration, ensure the Inject node does **not** set `msg.nodeId`, `msg.topic`, or `msg.payload`. ::: :: ### Message properties - `msg.attributeId`, which attribute to read: `Value` (default), `DataType`, `BrowseName`, `DisplayName`, or `All`. - `msg.outputType`, `Value` returns the bare value on `msg.payload`; `DataValue` returns the full DataValue (value plus `statusCode`, `sourceTimestamp`, `serverTimestamp`, and picoseconds). ### Read modes - **Single value**, one NodeId; the value comes back on `msg.payload`. - **All attributes**, set `msg.attributeId` to `All` to get every attribute of one node. Reads a single NodeId only, use multiple Read nodes for multiple nodes. The payload then contains `nodeId`, `statusCode`, `nodeClass`, `browseName`, `displayName`, `description`, `writeMask`/`writeMaskAsString`, `userWriteMask`/`userWriteMaskAsString`, `value` (with timestamps), `dataType`/`dataTypeName`, `valueRank`, `arrayDimensions`, `accessLevel`/`accessLevelAsString`, `userAccessLevel`/`userAccessLevelAsString`, and `minimumSamplingInterval`. - **Multiple values as an array**, inject an array of NodeIds via `msg.topics`; `msg.payload` comes back as a parallel value array, with a parallel `msg.dataType` array. Array reads are only available by injecting a message (the node's own config holds a single NodeId). - **Multiple values as a structure**, inject a JSON object in `msg.topic` whose leaves are NodeIds; the node returns the same shape with values substituted. The Explore node can generate this structure. Example, read a single value by injecting the NodeId: ```js msg.topic = "ns=2;s=TemperatureSetpoint"; // msg.payload comes back as the current value ``` Example, read every attribute of one node: ```js msg.nodeId = "ns=2;s=TemperatureSetpoint"; msg.attributeId = "All"; ``` Reading `All` on a structured DataType node returns its full definition, for example the fields of an `RfidScanResult` extension object: !["Debug output of an All-attributes read showing a DataType definition"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/example-all-attributes-on-rfid-scan-result.png "Debug output of an All-attributes read"){dataZoomable=""} Example, read several values as an array: ```js msg.topics = ["ns=1;s=Temperature", "ns=0;s=Pressure"]; msg.attributeId = "Value"; // msg.payload → [23.4, 1.2] ; msg.dataType → ["Double", "Double"] ``` ## 7. Write The Write node sends values to writable variables. It determines the target type automatically from the server in most cases. ### NodeId source (priority order) 1. `msg.nodeId` 2. `msg.topic` 3. Node configuration ### Write modes - **Single value**, inject the value as `msg.payload` and the target as `msg.topic` (or `msg.nodeId`). A successful write reports `statusCode: "Good"`. - **Explicit data type**, set `msg.dataType` when automatic conversion is insufficient. Valid values: `Boolean`, `SByte`, `Byte`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`, `UInt64`, `Float`, `Double`, `String`, `DateTime`, `ByteString`, `Guid`, `LocalizedText`, and `ExtensionObject`. - **Variant**, send `{ dataType, value, arrayType }` for finer control, including arrays. Use `dimensions` for a matrix layout. `dataType` and `arrayType` are optional, the node can infer them from the server. - **DataValue**, send a full DataValue (`statusCode`, `sourceTimestamp`/`serverTimestamp`, picoseconds, and a nested `value`) when you need to set timestamps or quality alongside the value. - **Batch write**, send an array of write requests in `msg.payload`. Returns `msg.results` with one status per write. Keep batches under 50–100 items and always inspect individual `statusCode` values. - **Extension objects**, use OPC UA JSON encoding. Both the 1.05 encoding (`UaTypeId` / `UaBody`) and legacy 1.04 (`TypeId` / `Body`) are supported. Automatic conversion maps JavaScript values to OPC UA types: integers → Int32/UInt32, decimals → Float/Double, text → String, booleans → Boolean, and arrays → OPC UA arrays. Override it with `msg.dataType` only when needed. Example, write a single value: ```js msg.topic = "ns=2;s=TemperatureSetpoint"; msg.payload = 25.5; // dataType inferred from the server ``` Example, batch write several variables in one message: ```js msg.payload = [ { nodeId: "ns=2;s=TemperatureSetpoint", value: 25.5 }, { nodeId: "ns=2;s=PressureSetpoint", value: 1013 }, { nodeId: "ns=2;s=EnableFlag", value: true } ]; // inspect msg.results, one statusCode per write ``` Example, write a 2×3 matrix using explicit dimensions: ```js msg.payload = { dataType: "Double", value: [1, 2, 3, 4, 5, 6], arrayType: "Matrix", dimensions: [2, 3] // 2 rows, 3 columns }; ``` Example, write an extension object using OPC UA JSON encoding, in both the 1.05 and legacy 1.04 forms: ```js // 1.05, UaTypeId is a NodeId string, UaBody holds the fields msg.payload = { UaTypeId: "nsu=http://opcfoundation.org/UA/AutoID/;i=3007", UaBody: { CodeType: "Ean13", ScanData: { Epc: { PC: 12, UId: "XBASE64d=" } } } }; // 1.04 (legacy), TypeId is an object (IdType/Namespace/Id); binary fields use a Buffer msg.payload = { TypeId: { IdType: 0, Namespace: 3, Id: 3007 }, Body: { CodeType: "Ean13", ScanData: { Epc: { PC: 12, UId: Buffer.from("Hello") } } } }; ``` ### Output message A Write returns the original `payload` and `nodeId` plus the result: `statusCode`, the `dataType` actually written, the `attributeId`, and a `message` field carrying an error description when the write fails. ### Common write status codes | Status code | Meaning | | ------------------- | ---------------------------------------------- | | Good | Write succeeded. | | BadNodeIdUnknown | Variable not found. | | BadTypeMismatch | Conversion failed, set the correct `dataType`. | | BadUserAccessDenied | Account lacks write permission. | | BadNotWritable | Variable is read-only. | | BadConnectionClosed | Connection was lost during the write. | ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} Writes to production equipment change real-world state. Gate write nodes behind validation or an operator confirmation step before deploying to a live instance. ::: :: ## 8. Extension Object The Extension Object node builds a correctly encoded ExtensionObject from a plain JavaScript object, so you do not have to hand-encode it for a Write. It queries the server for the target node's DataType definition, constructs the encoded value, and passes it to Write. ```text [Inject payload] → [Extension Object] → [Write] → [Debug] ``` Inject a plain JavaScript object as `msg.payload`; the node encodes it for the server's DataType and emits the ExtensionObject ready for the Write node. ```js msg.nodeId = "ns=2;s=DeviceConfiguration"; msg.payload = { setpoint: 72.0, mode: "Auto", enabled: true }; ``` The node also accepts an array of objects and encodes it as an array of ExtensionObjects, detecting the array type from the server's definition. ## 9. Call The Call node invokes a server method, a server-side function such as start a batch, reset a counter, or acknowledge an alarm, that takes input arguments and returns output arguments. ### Configuration - **ObjectId**, the NodeId of the object that owns the method. - **MethodId**, the NodeId of the method itself. - **Check button**, validates the method and loads its input/output argument definitions. Use the Browse node first if you need to discover an object's methods. Pass the method's arguments as a named-property object in `msg.payload`. Override the object and method dynamically with `msg.objectId` and `msg.methodId` (these take precedence over the node configuration). Arguments may themselves be arrays or ExtensionObjects. The result carries output arguments on `msg.payload`, plus a `statusCode` and the echoed `objectId` and `methodId`. ```js msg.objectId = "ns=2;s=ProcessController"; msg.methodId = "ns=2;s=StartProcess"; msg.payload = { processId: "PROC_001", targetTemperature: 150.0 }; ``` Example, pass an array argument and an ExtensionObject argument: ```js msg.payload = { setpoints: [25.0, 30.0, 35.0], config: { UaTypeId: "ns=2;i=1001", UaBody: { mode: "Auto", limit: 80 } } }; ``` ### Method status code categories | Category | Examples | Retryable? | | ------------------- | ----------------------------------------------------------------------- | -------------------- | | Success | Good | | | Parameter | BadInvalidArgument, BadArgumentsMissing, BadTypeMismatch, BadOutOfRange | No, fix inputs | | Method availability | BadMethodInvalid, BadMethodNotCallable, BadNotExecutable | No | | Security | BadUserAccessDenied, BadNoValidCertificate | No, fix access/certs | | Communication | BadCommunicationError, BadTimeout, BadServerNotConnected | Yes, retry/back off | | Server state | BadStateNotActive, BadShutdown | Sometimes | | Resource | BadOutOfMemory, BadResourceUnavailable, BadTooManyOps | Yes, wait and retry | ## 10. Monitor The Monitor node subscribes to variables and emits a message whenever a value changes, no polling. Use Monitor for live data; use Read for occasional on-demand checks. !["The Monitor node configuration with subscription, NodeId, sampling interval, deadband, and queue size"](https://flowfuse.com/docs/flowfuse-nodes/edge/images/opcua/opcua-client2-monitor.png "The Monitor node configuration"){dataZoomable=""} ### Concepts - **Subscription**, configured on the connection (publishing interval, lifetime, priority). One subscription efficiently carries many monitored items. - **Monitored item**, each tracked variable, with its own sampling interval, queue size, and optional filter. - **Notification behaviour**, a monitored item notifies only when the value actually changes (per the OPC UA spec), and once at monitoring start: the initial value is always reported. - **Resilience**, the Monitor node reconnects automatically after a connection loss and re-establishes its subscription and monitored items, resuming without manual intervention. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Each notification carries `msg.sequenceNumber`. Watch it for gaps to detect dropped notifications, for example when a server-side queue overflows under a heavy change rate. ::: :: ### NodeId source (priority order) 1. `msg.nodeId` 2. `msg.topic` 3. The node's configured NodeId ### Modes - **Single variable**, configure a static NodeId, or inject one via `msg.nodeId` at runtime to change which variable is monitored without redeploying. To inject, clear the NodeId field and turn off **Start Immediately**. - **Multiple variables (array)**, inject an array of NodeIds; the output `msg.payload` is the full array of current values in the same order, re-sent whenever any one of them changes. All items share the subscription's sampling interval. - **Named JSON structure**, inject a JSON object whose leaf values are NodeIds (the shape Explore produces); notifications arrive in the same shape with current readings. - **Entire subtree**, use Explore once (output type `NodeId`) to discover all variables under a branch, then feed that structure into Monitor to watch them all live. In the array and named-structure modes, the set you inject replaces the previous one. See [Changing or stopping what is monitored](https://flowfuse.com/#changing-or-stopping-what-is-monitored) to add, remove, or stop monitored items at runtime. ### Message properties (override node config per message) - `msg.outputType`, `Value` or `DataValue`. - `msg.samplingInterval`, milliseconds, or `0` to use the variable's minimum sampling interval. - `msg.deadbandType`, `None`, `Absolute`, or `Percent`. - `msg.deadbandValue`, the deadband threshold. - `msg.queueSize`, server-side queue depth (default 1000). - `msg.discardOldest`, when the queue is full, drop the oldest (`true`) or the newest (`false`). Note the message property defaults to `false`, the opposite of the node's **Discard Oldest** field (which defaults to `true`): set it explicitly when injecting to avoid silently discarding the newest values. ### Quick start 1. Add a Monitor node and select the endpoint and a subscription. 2. Enter the NodeId (e.g. `ns=1;s=Temperature`). 3. Enable **Start Immediately** and deploy. The node emits a message on each value change. Example, monitor several variables by injecting an array of NodeIds: ```js msg.payload = [ "ns=1;s=Temperature", "ns=1;s=Pressure", "ns=1;s=FlowRate" ]; ``` All items in the array share one subscription, sampling interval, and queue, more efficient than one Monitor node per variable. As a rough guide: under 10 items is trivial, 10–100 is the efficient sweet spot, 100–1000 may need tuned subscription parameters, and beyond 1000 split the work across multiple Monitor nodes. ### Changing or stopping what is monitored The injected NodeId set is declarative: whatever you inject becomes the complete set of monitored items, replacing whatever was monitored before. The node has no separate stop or unsubscribe command because you do not need one, you change the set instead. - **Reduce the set**, inject a shorter array. The dropped NodeIds are released as monitored items on the server, not merely filtered out of the node's output. - **Stop monitoring**, inject an empty array (`[]`), or an empty object (`{}`) if you are injecting the named JSON structure. Every monitored item is released and the node stops emitting. - **Restart monitoring**, inject the NodeId array again. The subscription itself persists while empty, which costs almost nothing and makes restarting immediate. This is how you start and stop monitoring programmatically, from an Inject node or a dashboard control, without editing and redeploying the flow. Example, stop monitoring: ```js msg.payload = []; ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Injecting an empty array releases the monitored items but leaves the now-empty subscription in place on the connection, so it is the right way to pause and resume a stream. To tear the session down completely, disconnect the connection instead. ::: :: ### Deadband filtering [Deadband filtering](https://flowfuse.com/blog/2026/04/stop-noisy-sensor-data-deadband-filter-flowfuse/#what-is-a-deadband-filter) suppresses insignificant changes, essential for analog values that jitter. The server applies it, so it also reduces network traffic. | Type | Behaviour | | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | None | All changes reported (default). | | Absolute | Report only when `abs(new − lastReported) > value`. | | Percent | Report only when the change exceeds a percentage of the variable's EURange. Requires the variable to expose an EURange property. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Deadband compares against the last reported value, not the last sampled value. The initial value is always reported. Use Absolute when a variable has no EURange, Percent silently does nothing without one. ::: :: ## 11. Monitor Event The Monitor Event node subscribes to events and alarms rather than value changes, alarm activations, state transitions, audit/security events, and system notifications. ### Filtering - **Where Clause**, a server-side filter selecting which events to receive. Far more efficient than post-filtering in a Function node. - **Select Clause**: comma-separated fields to retrieve (e.g. `EventId, Time, Message, Severity`). The `...` button opens a graphical selector that browses the event type hierarchy. Request only the fields you use: smaller messages, less processing. Common sets: basic `EventId,Time,Message,Severity`; alarms add `SourceName,ActiveState,AckedState`; audits add `ActionTimeStamp,ClientUserId`. Match the subscription to the event rate: a slower publishing interval suits alarms and audits, a faster one suits high-frequency process events. For mixed workloads, use separate Monitor Event nodes on different subscriptions (see [Configure a connection](https://flowfuse.com/#_5-configure-a-connection)) so critical alarms don't queue behind noisy low-priority events. ### Where Clause syntax Filter on the standard event fields (`EventId`, `EventType`, `SourceName`, `Time`, `Message`, `Severity`): - Type filtering, `ofType('AlarmConditionType')` - Numeric comparison, `Severity >= 700` - String equality, `SourceName = "Reactor1"` - Boolean fields, `ActiveState = true` - Combine with `AND` / `OR`, `ofType('AlarmConditionType') AND Severity >= 700` Common syntax mistakes: quote type names (`ofType('AlarmConditionType')`, not `ofType(AlarmConditionType)`); use `>=` not `=>`; and use `AND`/`OR`, not `&&`/`||`. Restrict the Where Clause to standard fields, type-specific fields belong in the Select Clause, not the filter. ### Event type hierarchy and severity Events derive from `BaseEventType`, with `SystemEventType`, `AuditEventType`, and `AlarmConditionType` (and alarm subtypes such as `LimitAlarmType` and `DiscreteAlarmType`) beneath it. The Select Clause fields available depend on the event type, alarms expose `ActiveState`, `AckedState`, `ConfirmedState`, and `Retain`; audit events expose `ActionTimeStamp`, `Status`, and related fields. Severity runs from 1 (informational) to 1000 (critical). A rough scale: 1–200 low/informational, 201–400 minor, 401–600 noteworthy, 601–800 important warnings, 801–1000 critical. A common production filter is `Severity >= 600`. To capture all server events, monitor the Server object at `i=2253` with an empty Where Clause. Example, monitor all server events: ```text NodeId: i=2253 Where Clause: (empty) Select Clause: EventId,EventType,SourceName,Time,Message,Severity ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} If no events arrive, verify the server supports events (read the object's `EventNotifier` attribute, a non-zero value means it emits events), point the NodeId at an event-generating object (try `i=2253`), and clear the Where Clause. If too many arrive, tighten the Where Clause by type and severity, or point at specific equipment instead of the Server object. ::: :: ## 12. Browse The Browse node lists the references of one node, one level of the address space. Specify the node with `msg.nodeId` or `msg.topic`. ### Configuration options - **NodeId**, the node whose references to list. - **ReferenceTypeId**, which reference type to follow, chosen from the reference-type hierarchy (e.g. `HasComponent`). - **IncludeSubType**, follow only the exact type (`false`) or also its subtypes (`true`). - **BrowseDirection**, forward, inverse, or both. - **Node Class Mask**, return only targets of selected node classes: Object, Variable, Method, ObjectType, VariableType, ReferenceType, DataType, View. - **Result Mask**, which fields to return: ReferenceType, IsForward, NodeClass, BrowseName, DisplayName, TypeDefinition. The NodeId is always included regardless of the mask. Any of these can be overridden per message (for example `msg.referenceTypeId`), so a single Browse node can serve different queries driven from upstream. ```js msg.nodeId = "ns=0;i=85"; // the Objects folder ``` The node returns the node's direct children on `msg.payload`, each entry carries the fields selected in the Result Mask, for example: ```json [ { "nodeId": "ns=2;s=DeviceSet", "browseName": "DeviceSet", "nodeClass": "Object" }, { "nodeId": "ns=2;s=Boiler", "browseName": "Boiler", "nodeClass": "Object" } ] ``` Browse is also how you discover a method's owning object and arguments before configuring a Call node: browse the object for `HasComponent` references of node class Method. ## 13. Explore The Explore node recursively traverses a subtree and returns the whole structure as a JSON object, unlike Browse, which shows a single level. ### Features - Recursive traversal of an entire subtree in one operation. - Multiple output types for leaf variables (see below). - Depth control, exclusion of empty nodes, and selection of which references to follow. - Output that feeds directly into the Monitor node and the Read-as-structure mode. Traversal stops at variable nodes: the explorer reads each variable (returning the selected output type) and does not recurse into a variable's own children. To inspect a variable's children, start an Explore directly on that variable node. ### Output types The output type sets what each leaf variable returns: | Output type | Returns | Typical use | | ----------------- | ---------------------------------------- | ---------------------------------------------------- | | Value | Current values | Display, analysis, dashboards | | NodeId | Node identifiers | Wiring into Monitor / Read / Write | | DataValue | Values with quality and timestamps | Quality checks, freshness validation | | Variant | Value with its data-type information | Typed values without full quality/timestamp metadata | | StatusCode | Quality only (Good/Bad/Uncertain) | Health checks, finding bad sensors | | BrowsePath | Full browse paths with namespace indices | Documentation, technical reference | | AliasedBrowsePath | Simplified dotted paths | Human-readable references, config files | | NSUNodeId | NodeIds with namespace URIs | Cross-server / portable configurations | ### Scope controls - **followOrganizes**, `true` follows `Organizes`, `HasProperty`, and `HasComponent` (complete equipment discovery); `false` follows only `HasProperty`/`HasComponent` (direct properties). - **excludeEmpty**, set `msg.excludeEmpty = true` to drop branches that contain no variables, for cleaner output. - **Depth**, Explore follows the full hierarchy under the start node (default maximum depth 10). Rather than a depth setting, control scope by choosing a more specific start NodeId: roughly 2–3 levels for one piece of equipment, 4–6 for a production line, 7–10 for a whole plant. ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} Starting Explore at the Objects folder (`ns=0;i=85`) or another root node traverses the entire server and can return a very large structure. Start from a specific NodeId (or `/Server/ServerStatus` if you are just getting your bearings) and expand scope deliberately. ::: :: Example, exploring `/Server/ServerStatus` with output type `NodeId` returns a nested object whose leaves are the NodeIds of each variable: ```json { "StartTime": "ns=0;i=2257", "CurrentTime": "ns=0;i=2258", "State": "ns=0;i=2259", "BuildInfo": { "ProductName": "ns=0;i=2263", "SoftwareVersion": "ns=0;i=2264" } } ``` ### Common pattern, explore to monitor Explore a subtree once to discover all its variables, then feed that structure straight into a Monitor node, Monitor returns the same shape with each leaf replaced by its live value. ```text [Inject] → [Explore] → [Monitor] → [Debug] ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Address spaces rarely change, so cache the Explore result (with a timestamp) and reuse it rather than re-exploring on every deploy. Re-explore periodically only if your server adds or removes variables at runtime. ::: :: ## 14. History Read The History Read node retrieves historical data from servers that store it, the basis for trends, shift reports, audits, and incident investigation. ### History types - **Raw**, the raw historical values stored on the server. - **Raw Modified**, raw values that have since been edited or corrected, with modification tracking. - **Processed Details**, server-side aggregates over a processing interval (set the **Aggregate Type**). ### Key options - **Start Time / End Time**, absolute (ISO 8601, e.g. `2024-11-20T08:00:00Z`, with optional timezone offset) or relative (`now`, `1 hour ago`, `30 minutes ago`, `7 days ago`). - **Processing Interval**, the aggregation bucket for Processed Details: `30 seconds`, `5 minutes`, `1 hour`, `1 day`, `1 week`. - **numValuesPerNode** (`msg.numValuesPerNode`), cap on values returned per node (default 100). - **returnBounds** (`msg.returnBounds`), include the bounding values just outside the time range, for continuous trends. The NodeId follows the usual priority (`msg.nodeId` → `msg.topic` → `msg.payload` → node config). Multiple variables can be read by injecting an array of NodeIds. ### Aggregate types (Processed Details) `Average`, `Count`, `Delta`, `DeltaBounds`, `DurationBad`, `DurationGood`, `Interpolative`, `Maximum`, `Minimum`, `PercentBad`, `PercentGood`, `Range`, `StandardDeviationPopulation`, `StandardDeviationSample`, `Total`, `VariancePopulation`, `VarianceSample`, `WorstQuality`. ### Output types - **Time+Value**, `{ value, sourceTimestamp }` per point. - **Time+Variant**, adds the full Variant (`value: { dataType, value }`). - **DataValue**, adds `statusCode` and `serverTimestamp`. - **StatusCode**, quality only. - **DataValueReversible**, DataValue with reversible encoding. `msg.payload` is an array of points, and the node echoes the request (`nodeId`, `startTime`, `endTime`) alongside it. Always check each point's `statusCode` and filter out bad-quality points before charting or reporting. On failure the payload is an empty array carrying a top-level `statusCode` and a `message`; an empty array with a `Good` status simply means no data exists in the requested range, distinguish the two before treating it as an error. ```js msg.startTime = "1 hour ago"; msg.endTime = "now"; // last hour, raw msg.startTime = "7 days ago"; msg.processingInterval = "1 day"; // daily averages (Processed Details + Average) msg.startTime = "2024-11-20T08:00:00Z"; msg.endTime = "2024-11-20T17:00:00Z"; // explicit range ``` ### Status codes | Status code | Meaning | | ------------------------------ | ---------------------------------- | | Good | Data retrieved. | | BadHistoryOperationUnsupported | The server does not store history. | | BadNodeIdUnknown | Variable not found. | | BadHistoryOperationInvalid | Invalid history parameters. | | BadTimestampsToReturnInvalid | Invalid timestamp specification. | | BadMaxAgeInvalid | Invalid time range specified. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Keep time ranges and `numValuesPerNode` bounded, prefer aggregation to thin large ranges for charts, and verify the server supports history before relying on it. For very long ranges, chunk the request into smaller windows. ::: :: ## 15. File Operation The File Operation node reads and writes files on servers implementing the OPC UA FileType interface (`ns=0;i=11575`), useful for recipes, configurations, and logs. The NodeId may be a standard NodeId or a browse path (e.g. `/ns1:Logs/ns1:system.log`), set in the node or via `msg.nodeId`. ### Modes | Mode | Use | | --------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Read | Read file contents. | | ReadSize | Return the file size in bytes on `msg.payload`. | | Write (a.k.a. WriteEraseExisting) | Create or overwrite a file. An object/array payload is auto-converted to JSON; booleans/numbers become strings. | | WriteAppend | Append data to an **existing** file. End the payload string with `\n` for log lines. | ### Encoding and format - **Encoding** (read/write), `none` (raw binary, default), `setbymsg` (use `msg.encoding` at runtime), `utf8`, `ascii`, `utf-16le`, `Shift_JIS`, `EUC-JP`, `GB2312`, `GBK`, `Big5`, plus many regional Windows / ISO / IBM / Mac / KOI8 encodings. - **Format** (read only), `buffer` (raw Buffer, default), `utf8` (decoded string), or `lines` (array split on newlines, ideal for CSV). For text, pair `encoding: utf8` with `format: utf8`; for binary files (images, PDFs, executables) use `encoding: none` with `format: buffer`. The node chunks large files automatically based on the server's `MaxByteStringLength` and transport limits, so there is no practical size cap beyond the server's own. ### Output and status Read returns content on `msg.payload` (Buffer, string, or string array per the Format). A write returns the byte count on `msg.size`. The node status shows `Operating` during transfer, `size = X` on success, and `failed` on error (details in the debug panel). ```js msg.nodeId = "ns=2;s=RecipeFile"; msg.payload = "appended log line\n"; // WriteAppend mode ``` ### Safe write patterns - **Create-or-update**, run a `ReadSize` first, then branch on the result: route to `Write` when the file is missing and to `WriteAppend` when it already exists. - **Atomic write**, write to a temporary file, then rename it over the target once the write succeeds, so a failure mid-write cannot corrupt the original. ### File operation errors | Error | Cause | | ------------------------------------ | ------------------------------------------------------------ | | `nothing to write` | `msg.payload` is empty or undefined. | | `expecting a nodeIdString` | Invalid NodeId format. | | BadNodeIdUnknown | File not found, for WriteAppend, create it first with Write. | | BadNotWritable / BadUserAccessDenied | File is read-only or your account lacks permission. | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Use the Browse node to discover available File objects, and the Read node to inspect file metadata such as `Size`, `OpenCount`, and `UserWritable`. Garbled text usually means the wrong encoding, try a different one. ::: :: ## 16. Hosting an OPC UA Server On **self-hosted FlowFuse**, the certified node can run an OPC UA server inside your instance using only Function nodes, no `settings.js` edit, no external module declaration, no extra npm install. ::div{.ff-callout.ff-callout--caution} Caution :::div{.ff-callout__content} Server hosting is \*\*not supported on FlowFuse Cloud\*\*, Cloud exposes HTTP/HTTPS only and cannot expose the arbitrary TCP port (\`opc.tcp\://\`) a server needs. Use a self-hosted FlowFuse instance and ensure the chosen port is reachable through your container and network configuration. ::: :: When the palette loads, it publishes a bootstrap helper in the Node-RED global context. Retrieve it and destructure `{ bootstrap, opcua }`: `bootstrap` carries the server helpers and `opcua` re-exports the full `node-opcua` namespace. This works even with `functionExternalModules: false`. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} `global.get("sterfive")` below is the literal runtime key the certified node exposes. Keep it exactly as written in your Function nodes, or the code will not find the helper. ::: :: ### Basic pattern Two Function nodes share live variables through flow context: one boots the server once on deploy, the other updates a variable's value at any time without restarting. ```text [Inject once after deploy] → [Boot Server] → [Debug] [Inject value] → [Update Value] ``` Boot Server calls `bootstrapServer({...})`. The helper is idempotent, the same config reuses the running handle; a changed config tears down and rebuilds. It runs `onPopulate` exactly once on first build and registers `SIGINT`/`SIGTERM`/`exit` handlers for clean shutdown, so you do not add your own. Update Value writes new values with `setValueFromSource()`, which does no I/O and is safe to call thousands of times per second. ```js // Boot Server const { bootstrap, opcua } = global.get("sterfive"); const { bootstrapServer } = bootstrap; const handle = await bootstrapServer({ port: 4840, endpoint: "node-red-server", nodesets: ["standard"], onPopulate: (addressSpace, exposed) => { const ns = addressSpace.getOwnNamespace(); const device = ns.addObject({ organizedBy: "ObjectsFolder", browseName: "Device001" }); exposed.temperature = ns.addVariable({ componentOf: device, browseName: "Temperature", nodeId: "s=Temperature", dataType: "Double", }); exposed.temperature.setValueFromSource({ dataType: opcua.DataType.Double, value: 20.0 }); } }); flow.set("$opcuaHandle", handle); ``` ```js // Update Value const handle = flow.get("$opcuaHandle"); handle.exposed.temperature.setValueFromSource({ dataType: "Double", value: msg.payload }); ``` ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Keep the handle in flow context, never in a local `const`/`let`, a Function node's body re-runs on every message. Prefix context keys with `$` (e.g. `$opcuaHandle`) and avoid the bare key `opcua` (it collides with other vendors), dotted keys (Node-RED reads them as nested paths), and colon-separated keys. Put construction-time options (`port`, `nodesets`, security, `users`) only in the Boot node, never in the Update node. ::: :: ### Key `bootstrapServer` options | Option | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `port` | TCP port to listen on (default 4840). | | `endpoint` | Endpoint name string. | | `applicationName` / `productUri` | Identity strings the server advertises. | | `nodesets` | Built-in names (`"standard"`, `"di"`, `"autoId"`, `"machinery"`, `"ia"`) or absolute paths to `.NodeSet2.xml` files. | | `securityPolicies` | Array of SecurityPolicy values (e.g. None, Basic256Sha256). | | `securityModes` | Array of MessageSecurityMode values (None, Sign, SignAndEncrypt). | | `allowAnonymous` | Allow anonymous sessions (default `true`). | | `users` | Array of `{ username, password, roles }` objects. | | `discoveryServerEndpointUrl` | Discovery-server (LDS) registration setting, advanced. | | `registerServerMethod` | Discovery-server (LDS) registration setting, advanced. | | `shutdownTimeoutMs` | Grace period for clean shutdown. | | `onPopulate` | Callback run once when a new server is built, add your variables, objects, and methods here. | | `forceRebuild` | Set `true` to rebuild without a config change (e.g. after editing the trust store). | ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The server's identity is a config hash of `port`, `endpoint`, `applicationName`, `productUri`, `nodesets`, `securityPolicies`, `securityModes`, `allowAnonymous`, and `users`. Change any of these and the next deploy rebuilds the server (re-running `onPopulate`). `onPopulate` and `forceRebuild` are excluded from the hash, so editing `onPopulate` alone does not trigger a rebuild, use `forceRebuild: true` if you need one. ::: :: ### Adding methods All address-space construction must happen inside `onPopulate`, it runs exactly once on a fresh build, never on a same-config redeploy. Do not put `addObject` or `addMethod` calls in nodes that handle every message; they throw on a duplicate `browseName` and pile up across redeploys. ```js const resetMethod = ns.addMethod(device, { browseName: "Reset", inputArguments: [], outputArguments: [], }); resetMethod.bindMethod(async (_inputArguments, _context) => { exposed.temperature.setValueFromSource({ dataType: opcua.DataType.Double, value: 20.0 }); return { statusCode: opcua.StatusCodes.Good, outputArguments: [] }; }); ``` ### Cleaning up timers If `onPopulate` starts intervals or timeouts, register them with the address space so they stop automatically on shutdown. Otherwise they fire against a disposed address space and cause `'AddressSpace has been disposed'` on the next deploy. ```js const timerId = setInterval(() => { sensor.setValueFromSource({ dataType: "Double", value: Math.random() }); }, 250); addressSpace.registerShutdownTask(() => clearInterval(timerId)); ``` ### Structured namespaces and companion specifications Organise large address spaces with folders and objects (`addressSpace.getOwnNamespace().addFolder(...)`, `addObject(...)`), and prefer string NodeIds (`nodeId: "s=DeviceA.Speed"`) so subscribers stay stable. For a scalable Update node, key your variables by `msg.topic` and look them up with `handle.addressSpace.findNode("ns=1;s=...")` rather than adding a Function node per variable. To expose standard companion-spec types (DI, Machinery, AutoID/RFID, IA), load the nodesets, then instantiate their object types inside `onPopulate`: ```js const nsDI = addressSpace.getNamespaceIndex("http://opcfoundation.org/UA/DI/"); const deviceType = addressSpace.findObjectType("DeviceType", nsDI); const device = deviceType.instantiate({ organizedBy: addressSpace.rootFolder.objects, browseName: "MyDevice", optionals: ["SerialNumber"], }); ``` Access a component of an instantiated type with `device.getComponentByName(name, namespaceIndex)`, pass the namespace index, since the same browse name can exist in several namespaces. For composite extension-object values (for example an `RfidScanResult`), build the value with `addressSpace.constructExtensionObject(dataTypeNode, fields)`, then write it with `setValueFromSource({ dataType: opcua.DataType.ExtensionObject, value: extObj })`. Load companion nodesets in dependency order (AutoID needs DI, MachineTool needs Machinery, and so on); the helper reports a clear validation error if a dependency is missing. ### Large variable sets When populating thousands of variables, enable `addressSpace.isFrugal = true` during construction to skip reverse-reference materialisation and cut memory overhead. Re-enable normal mode before adding nodes that need full bidirectional references (type instances, browsable methods). ```js addressSpace.isFrugal = true; for (let i = 0; i < 5000; i++) { ns.addVariable({ componentOf: dataset, browseName: `Var${i}`, dataType: "Double" }); } addressSpace.isFrugal = false; ``` ### High-frequency variables For variables that change faster than clients need to sample, set `minimumSamplingInterval` (ms) to hint the server's fastest meaningful sample rate and stop clients requesting sub-millisecond rates. For values that update sub-millisecond in your code, batch externally, call `setValueFromSource` once per tick (e.g. every 50 ms) rather than from a tight loop. ```js ns.addVariable({ browseName: "FastSensor", nodeId: "s=FastSensor", dataType: "Double", minimumSamplingInterval: 50, // 50 ms = 20 Hz maximum value: { dataType: "Double", value: 0 }, }); ``` ### Multiple servers in one instance `bootstrapServer` accepts an optional second `ownerKey` argument. Without distinct keys, a second call tears down the first server. Each server must bind a different port and use a different flow-context key. ```js const handleAlpha = await bootstrapServer({ port: 4840, endpoint: "alpha" }, "alpha"); const handleBeta = await bootstrapServer({ port: 4841, endpoint: "beta" }, "beta"); ``` Beyond roughly five servers per instance, prefer separate processes; the event loop becomes the bottleneck before the OPC UA stack does. ### User authentication Authentication is declarative through the `users` array. Each entry has a username, a password, and roles. The helper bcrypt-hashes clear-text passwords at boot and maps role names to their NodeIds; a value already in bcrypt form (`$2a$`/`$2b$`/`$2y$` prefix) is passed through verbatim. The `users` array controls only session activation, per-node authorization comes from each variable's access-level attributes combined with role mapping. Set `allowAnonymous: false` to refuse anonymous sessions. The helpers `bootstrap.ensureBcryptHash(plain)` and `bootstrap.isBcryptHash(hash)` are available for tooling. ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} If you omit `users`, the helper installs a default test set (`root/secret`, `gdsadmin/admingds`, `user1/password1`, `user2/password2`) intended only for development. Always set your own `users` for any instance reachable beyond your development machine. ::: :: ### Security, certificates, and PKI storage Enable secure endpoints with `securityPolicies` and `securityModes`; the server advertises the cartesian product of allowed (policy, mode) pairs as separate endpoints. A self-signed server certificate is generated on first boot, or you can provision a CA-issued one. A client connecting over a secure mode for the first time lands its certificate in the `rejected` folder, move it to `trusted` and subsequent connections succeed. Every server also installs the push-certificate-management service for over-the-wire certificate rotation. On FlowFuse, the node stores its PKI (its own certificate, the trusted list, and the rejected list) under `/opcua-for-flow-fuse/PKI`. For this to survive restarts and redeploys, that directory must be on persistent storage: on container-based FlowFuse (Cloud, Kubernetes, Docker) this is the persistent volume mounted at `/data/storage`; on the FlowFuse Device Agent the working directory is on the device's local filesystem. If you re-trust a server's certificate after every deploy, your PKI directory is not landing on persistent storage. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Username/password over `MessageSecurityMode.None` travels in cleartext. Combine credentials with at least Sign mode. Disabling `SecurityPolicy.None` entirely can break naive clients that probe without security first. ::: :: ### Lifecycle, diagnostics, and stopping Process exit and config-change redeploys are handled automatically, do not add your own `process.on("SIGINT")` handler or `node.on("close", ...)` for basic flows. For an explicit stop, call `handle.shutdown(timeoutMs)` (idempotent and concurrency-safe, a second call while a shutdown is in flight returns the same promise) and clear the flow-context references afterwards, so a stale handle isn't reused against a disposed address space. Use `handle.isRunning()` as the single source of truth for server state. `bootstrap.shutdownAllServers(timeoutMs)` stops every registered server at once. For live diagnostics, `bootstrap.getServerInfo(handle.server)` returns a snapshot of `traffic` (bytes read/written, channel and session counts, subscription counts, rejection counts), `sessions`, `channels`, `certificates`, and `capabilities`. `bootstrap.displayServerInfoOn(info, { log: (m) => node.log(m), warn: (m) => node.warn(m) })` pretty-prints it to the Node-RED log (the second argument is a logger object with `log`/`warn` methods). A growing gap between `cumulatedSessionCount` and `currentSessionCount` is a useful signal that clients are reconnecting in a loop. ## 17. Network Requirements When the certified node runs inside a corporate or industrial network, allow the following outbound connections through your firewall or URL-filtering proxy. All traffic originates from the host running your FlowFuse instance; the certified node never accepts inbound connections to these endpoints. Share this section with your IT or security team. | Hostname | Port | Protocol | Purpose | Required? | | --------------------- | -------------- | ------------------ | ------------------------------------------------- | ----------------------------- | | Your OPC UA server(s) | typically 4840 | TCP (`opc.tcp://`) | OPC UA data plane, read, write, subscribe, browse | Yes | | `registry.npmjs.org` | 443 | HTTPS | Package installation (one-time, via the palette) | Standard Node-RED requirement | The OPC UA port (default 4840) is configurable per server; update the rule if your server uses a different port. The certified node does not make any external licence-check or activation calls, licensing is managed through FlowFuse, so no outbound connection to a third-party licence service is required. ## 18. Troubleshooting | Symptom | What to check | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Connection never goes green | Verify the endpoint URL and that the server is reachable. Use **check the connection** in the connection editor. | | Changes to connection settings have no effect | The connection is shared and cached, redeploy the flow after editing connection parameters. | | Security handshake fails | Confirm the security policy and message mode match the server, and that the **client** certificate has been added to the **server's** trusted list, the client auto-accepts the server certificate by default, so the missing trust is usually on the server side. | | Browse path won't resolve (BadBrowseNameInvalid / BadNoMatch) | Add the required namespace prefix (`2:Name`), escape special characters with `&`, and match the server's case exactly. | | Write rejected, BadNotWritable / BadUserAccessDenied | The variable is read-only, or your user lacks write permission. | | Write rejected, BadTypeMismatch | Set the correct `dataType`, or let the node infer it by sending a plain value. | | BadNodeIdUnknown | Use Browse or Explore to confirm the exact namespace index and identifier, these differ between servers. | | Browse / verify button does nothing | The endpoint must be configured and the flow deployed first. | | Read returns wrong value, node config NodeId ignored | A default Inject sets `msg.payload` to a timestamp, which overrides the configured NodeId. Clear `msg.payload`, `msg.topic`, and `msg.nodeId` from the Inject node. | | Monitor sends nothing | Check the NodeId, lower or remove the deadband, and confirm the value is changing. Percent deadband requires EURange, use Absolute otherwise. | | Monitor floods the flow | Add or increase a deadband, or raise the sampling interval. | | No events received (Monitor Event) | Verify the server supports events (read the `EventNotifier` attribute), point the NodeId at an event-generating object (try `i=2253`), and clear the Where Clause. | | Where Clause has no effect or errors | Quote type names (`ofType('AlarmConditionType')`), use `>=` not `=>`, use `AND`/`OR`, and filter only on standard event fields. | | Method call keeps failing | Check the status-code category, parameter/security errors need fixing, communication/resource errors are retryable. | | History Read returns nothing, BadHistoryOperationUnsupported | The server does not store history for that variable. Confirm history is enabled server-side. | | File read returns garbled text | Wrong encoding, try a different one (`utf8`, `Shift_JIS`, …); for binary use `encoding: none` + `format: buffer`. | | File append fails, BadNodeIdUnknown | The file must exist before WriteAppend; create it first with Write. | | Explore returns far too much data | You started at a root node (e.g. `i=85`). Start from a specific NodeId, set `excludeEmpty: true`, or use `followOrganizes: false`. | | Hosted server: 'AddressSpace has been disposed' | A timer or cached handle outlived a shutdown, register timers with `addressSpace.registerShutdownTask` inside `onPopulate` and clear flow-context keys after `shutdown()`. | | Hosted server shipped with default users | Set your own `users` array, the built-in `root/secret` set is for development only. | | Second `bootstrapServer` call tears down the first | Pass a distinct `ownerKey` to each call, and give each server a different port. | | Memory grows with large address spaces | Enable `addressSpace.isFrugal = true` in `onPopulate` before adding thousands of variables. | | Cannot host a server on FlowFuse Cloud | Server hosting requires a self-hosted FlowFuse instance with the OPC UA TCP port exposed. Cloud exposes HTTP/HTTPS only. | | Re-trusting the server certificate after every deploy | The PKI directory (`/opcua-for-flow-fuse/PKI`) is not on persistent storage. On container-based FlowFuse it must sit under the `/data/storage` persistent volume. Also confirm the certificate is in `trusted`, not `rejected`. | For help enabling the OPC UA Certified Node, licensing, or anything else, [contact FlowFuse](https://flowfuse.com/contact-us/). # RTSP Video Feed The **RTSP Video Feed** node connects to an [RTSP](https://en.wikipedia.org/wiki/Real-Time_Streaming_Protocol){rel=""nofollow""} video stream from an IP camera or NVR and extracts still frames as PNG images. This is a **FlowFuse Certified Node**. Unlike community nodes, which vary in quality and can go unmaintained without warning, FlowFuse vets Certified Nodes for quality, security, and support, and maintains them on an ongoing basis. [Read more about Certified Nodes](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). The node orchestrates `ffmpeg` to acquire and decode the video stream. By handling the video decoding externally, `ffmpeg` reduces the processing load on the main FlowFuse event loop. However, higher frame rates and image resolutions generate more image data, which can increase CPU and memory usage as frames are transferred and processed within your flows. The node is a source node with no input connector. It begins capturing frames as soon as the flow is deployed and displays a green **Running** status on the canvas when it has successfully connected to the stream. If `ffmpeg` exits unexpectedly, the node restarts it automatically after a short delay and displays the exit code in the node status. Extracted frames can either be emitted as messages into the flow or written directly to disk as a numbered sequence of PNG files. See [Operating modes](https://flowfuse.com/#operating-modes) for details. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The RTSP Video Feed node is not available by default. It is part of the FlowFuse Edge Certified Nodes catalogue, which is part of the **FlowFuse Edge** offering. Please contact our sales team at [Contact us](https://flowfuse.com/contact-us/) to learn more or to request access. ::: :: ## Use case Most plants and facilities already have IP cameras on the network, but the footage is locked inside an NVR: useful for reviewing an incident after the fact, invisible to everything else. The RTSP Video Feed node turns those existing cameras into a live data source. It captures still frames from the stream at a rate you choose and hands them to your flow as PNG images, where they can be analyzed, displayed, filtered, or stored like any other data. No new hardware, no separate video-analytics appliance. ### Example: catching mislabeled products on a packaging line A camera above a packaging line already streams to the site NVR. Point the RTSP Video Feed node at that same stream, set it to 1 FPS, and wire it to a [FlowFuse AI node](https://flowfuse.com/docs/flowfuse-nodes/ai/) running an image classification model. Each frame is checked for a missing or misprinted label, and when one is detected, the flow raises a dashboard alert and publishes the offending frame over MQTT for the quality team to review. What used to require an operator glancing at a screen, or a dedicated vision system with its own controller and license, becomes a three-node flow on hardware you already own. ### Where this shows up in practice - **Machine vision**: feed frames to a [FlowFuse AI node](https://flowfuse.com/docs/flowfuse-nodes/ai/) for object detection or image classification, then act on the result, for example counting parts on a line, checking a fill level, or verifying a safety gate is closed. - **Remote monitoring**: display the frames live on a [FlowFuse Dashboard](https://flowfuse.com/platform/dashboard/) so an operator can watch a machine or area from anywhere the dashboard is reachable, alongside the process data on the same page. - **Event snapshots**: route frames through a `function` or `switch` node and keep only the ones that matter, for example the frame captured the moment a PLC tag or sensor reports a fault, and forward it with an [`mqtt out`](https://flowfuse.com/docs/flowfuse-nodes/mqtt/mqtt-out/) node or attach it to an alert. - **Timelapse and archiving**: switch to disk-writing mode and the node writes a numbered sequence of PNG files for later review or timelapse assembly, without adding any messages to the flow. ### Combining video with other plant data Because the frames arrive as ordinary messages, they combine naturally with everything else in a flow. A single flow can join a camera frame with the PLC tag values captured at the same moment, so a quality event is recorded with both the picture and the process conditions that produced it. That correlation is exactly what standalone camera systems can't do. Set the FPS no higher than the use case requires: in message mode each captured frame becomes a message, and higher frame rates increase CPU and memory usage. ## Requirements The node requires `ffmpeg`. In most cases this is handled automatically: the node pulls in `ffmpeg-static` on install, which provides a prebuilt `ffmpeg` binary for your platform. If a prebuilt binary is not available for your platform, the node falls back to an `ffmpeg` binary on the system `PATH`. If neither is found, the node will not load and an error is written to the FlowFuse log. ## Install 1. Open the **Palette Manager** from the top-right menu in the FlowFuse editor. 2. Switch to the **Install** tab. 3. Find the **FlowFuse Edge Certified Nodes** collection. 4. Locate `@flowfuse-certified-nodes/rtsp` and click **Install**. `ffmpeg` is pulled in automatically during install. ![Palette Manager open on the Install tab with the FlowFuse Edge Certified Nodes collection visible and the RTSP node's Install button highlighted](https://flowfuse.com/docs/flowfuse-nodes/edge/images/rtsp/rtsp-edge-catalog.png)*Locating and installing the RTSP Video Feed node from the FlowFuse Edge Certified Nodes catalogue.* ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: ## Configuration Open the node's settings by double-clicking it on the canvas. | Field | Required | Description | | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **RTSP URL** | Yes | The stream URL, e.g. `rtsp://192.168.1.50:554/live/ch1`. Must be a valid URL. | | **Username** | No | Username for streams that require authentication. | | **Password** | No | Password for streams that require authentication. Stored as a FlowFuse credential and never written to the flow file. | | **FPS** | No | Frames per second to capture, from `1` to `60`. Defaults to `1`. | | **File path** | No | Directory frames are written to in disk-writing mode. If left empty, the OS temp directory (e.g. `/tmp`) is used. | | **Output image as `msg.payload`** | No | Switches between message mode and disk-writing mode. Enabled by default. See [Operating modes](https://flowfuse.com/#operating-modes). | | **Name** | No | Optional label for the node in the FlowFuse editor. | | **Topic** | No | Sets `msg.topic` on emitted messages. Useful when routing frames to MQTT. | ![RTSP Video Feed node settings panel showing the RTSP URL, Username, Password, FPS, File path, and Output image fields](https://flowfuse.com/docs/flowfuse-nodes/edge/images/rtsp/rtsp-config-node.png)*The RTSP Video Feed node configuration panel.* ## Operating modes The **Output image as `msg.payload`** checkbox controls how the node handles captured frames. ### Output enabled (default) The node emits each captured frame as a message at the configured FPS rate. **Output properties:** | Property | Type | Description | | ------------- | ------ | ----------------------------------------- | | `msg.payload` | Buffer | The captured frame as a PNG image buffer. | | `msg.topic` | String | The topic configured on the node. | The output can be wired to any node that accepts an image buffer, including [FlowFuse Dashboard widgets](https://flowfuse.com/platform/dashboard/), [MQTT out nodes](https://flowfuse.com/docs/flowfuse-nodes/mqtt/mqtt-out/), and [FlowFuse AI nodes](https://flowfuse.com/docs/flowfuse-nodes/ai/). ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Every captured frame becomes a message in the flow. A high FPS value increases the number and size of messages being processed. Set FPS no higher than your use case requires. ::: :: ### Output disabled The node emits no messages. Instead, `ffmpeg` writes a continuous numbered sequence of PNG files to the directory set in **File path**, named as follows: ```text rtsp--.png ``` If **File path** is left empty, files are written to the OS temp directory (e.g. `/tmp`). On many Linux distributions this is a RAM-backed filesystem, so frames consume memory rather than disk space and are cleared on reboot. The node does not delete files written to disk. At a high FPS rate, files will accumulate and eventually fill the available storage. Monitor available disk space when using this mode. # FlowFuse Tables **FlowFuse Tables** provides managed databases that can be directly accessed from Node-RED flows on the FlowFuse platform. :br It simplifies data management and integration by offering purpose-built nodes that allow you to query, insert, and modify data without complex setup. ## Nodes The following documents describe the nodes available under FlowFuse Tables: - [Query](https://flowfuse.com/docs/flowfuse-nodes/flowfuse-tables/query/): The Query node allows you to run SQL queries against FlowFuse Tables, supporting parameterized queries, Mustache templates, and AI-assisted query generation for seamless database interactions. # Query The Query node allows you to write and run queries against database tables managed by [FlowFuse Tables](https://flowfuse.com/docs/user/ff-tables/). The node is pre-configured to connect automatically when used within a FlowFuse Node-RED instance. With **FlowFuse Expert** integration, queries can be generated from natural language prompts, making database operations accessible without SQL expertise. ## Outputs The response (rows) is provided in `msg.payload` as an array. When **Split results** is enabled with **Number of rows = 1**, `msg.payload` contains a single row object instead. ### Additional Metadata - `msg.pgsql.rowCount` - Number of rows affected - `msg.pgsql.command` - The executed command For multiple queries, `msg.pgsql` is returned as an array. ## Inputs SQL queries can be configured directly in the node or passed dynamically via `msg.query`. ### Parameterized Queries (Recommended) Pass parameters as an array via `msg.params`: ##### Input Data ```javascript msg.params = [ msg.id ]; ``` ##### Query defined in the node ```sql SELECT * FROM table WHERE id = $1 ``` > Tip: For production environments, it is recommended to use parameterized queries instead. Parameterized queries automatically handle quoting and escaping, making them safer and more reliable. ### Named Parameters Pass parameters as an object via `msg.queryParameters`: ##### Input Data ```javascript msg.queryParameters.id = msg.id; ``` ##### Query defined in the node ```sql SELECT * FROM table WHERE id = $id; ``` ### Mustache Templates Reference message properties using Mustache syntax: ##### Query defined in the node ```sql SELECT * FROM table WHERE id = {{{ msg.id }}} SELECT * FROM table WHERE name = '{{{ msg.name }}}' ``` > Note: Care must be taken to ensure incoming string data is properly escaped (e.g., single quotes must be doubled: `'` to `''`) to prevent syntax errors and SQL injection. > Note: Inserting dynamic values into SQL statements using Mustache templates exposes your data to SQL Injection risks if the input is untrusted. We strongly recommend using Parameterized Queries or Named Parameters instead; these features are designed to safely separate data from the SQL command. ## Important Details ### Case Sensitivity By default, PostgreSQL converts unquoted table and column names to lowercase, making them case-insensitive (e.g., `SELECT DataVal FROM MyTable` is the same as `SELECT dataval FROM mytable`). To avoid errors and ensure portability, it is common to use only lowercase, unquoted identifiers. However, where required, you can wrap names in double quotes (e.g., `SELECT "DataVal" FROM "MyTable"`) to explicitly force them to be case-sensitive if the names were defined that way. ### Security Best Practices Parameterized queries are **strongly recommended** for production use over Mustache templates for security and maintainability. ### Named Parameters Limitation Named parameters are emulated (not native PostgreSQL), making them less robust than numeric parameters. ### Backpressure Management When **Split results** is enabled, the node waits for `msg.tick` before releasing the next batch, preventing memory issues. It exposes `node.tickConsumer` and `node.tickProvider` for automatic flow control. ### Split Results Sequences Streaming messages follow sequence conventions with: - `msg.parts.id` - `msg.parts.index` - `msg.parts.count` - `msg.complete` flag ## Requirements FlowFuse Tables requires **Enterprise tier** and must be enabled for your team. ## Example Flow ::render-flow{:height='500'} ```json [{"id":"9cd7498d4f832f1e","type":"group","z":"9db2d9ed7f00b8af","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["f27702ab8c8a58d2","da6e293fa363b172","f74ede3fbcf4ef32","8bfb39f252c16a79","41e1f9e0e97768a8"],"x":34,"y":39,"w":892,"h":142},{"id":"f27702ab8c8a58d2","type":"tables-query","z":"9db2d9ed7f00b8af","g":"9cd7498d4f832f1e","name":"","query":"","split":false,"rowsPerMsg":1,"x":670,"y":140,"wires":[["8bfb39f252c16a79"]]},{"id":"da6e293fa363b172","type":"inject","z":"9db2d9ed7f00b8af","g":"9cd7498d4f832f1e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":140,"wires":[["f74ede3fbcf4ef32"]]},{"id":"f74ede3fbcf4ef32","type":"template","z":"9db2d9ed7f00b8af","g":"9cd7498d4f832f1e","name":"Set Query","field":"query","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"CREATE TABLE \"sensor_data\" (\n \"id\" SERIAL PRIMARY KEY,\n \"sensor_id\" TEXT NOT NULL,\n \"timestamp\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n \"temperature\" REAL,\n \"unit\" TEXT DEFAULT 'celsius'\n);\n","output":"str","x":300,"y":140,"wires":[["f27702ab8c8a58d2"]]},{"id":"8bfb39f252c16a79","type":"debug","z":"9db2d9ed7f00b8af","g":"9cd7498d4f832f1e","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":140,"wires":[]},{"id":"41e1f9e0e97768a8","type":"comment","z":"9db2d9ed7f00b8af","g":"9cd7498d4f832f1e","name":"Pass the query via msg.query","info":"","x":180,"y":80,"wires":[]},{"id":"91fa219afc78e395","type":"group","z":"9db2d9ed7f00b8af","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["aefa193bfd04e5c2","e5bd3fbf9af5e519","6cc5a12665df32cb","1ebe4f1381fe71dd","7a397cd61dd6a210"],"x":34,"y":519,"w":892,"h":142},{"id":"aefa193bfd04e5c2","type":"tables-query","z":"9db2d9ed7f00b8af","g":"91fa219afc78e395","name":"","query":"SELECT * FROM public.sensor_readings WHERE \"temperature\" > {{{msg.temperatureThreshold}}};","split":false,"rowsPerMsg":1,"x":670,"y":620,"wires":[["6cc5a12665df32cb"]]},{"id":"e5bd3fbf9af5e519","type":"inject","z":"9db2d9ed7f00b8af","g":"91fa219afc78e395","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":620,"wires":[["7a397cd61dd6a210"]]},{"id":"6cc5a12665df32cb","type":"debug","z":"9db2d9ed7f00b8af","g":"91fa219afc78e395","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":620,"wires":[]},{"id":"1ebe4f1381fe71dd","type":"comment","z":"9db2d9ed7f00b8af","g":"91fa219afc78e395","name":"Using Mustache template","info":"","x":170,"y":560,"wires":[]},{"id":"7a397cd61dd6a210","type":"change","z":"9db2d9ed7f00b8af","g":"91fa219afc78e395","name":"Set temperatureThreshold","rules":[{"t":"set","p":"temperatureThreshold","pt":"msg","to":"20","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":620,"wires":[["aefa193bfd04e5c2"]]},{"id":"e00a02a11a322683","type":"group","z":"9db2d9ed7f00b8af","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["14d47d8931a2855b","58a32e0340ed268d","762caff19e1722b7","997d92441f4d2eeb","ae3e4a6940a8e236"],"x":34,"y":359,"w":892,"h":142},{"id":"14d47d8931a2855b","type":"tables-query","z":"9db2d9ed7f00b8af","g":"e00a02a11a322683","name":"","query":"DELETE FROM \"sensor_data\"\nWHERE \"id\" = $id;\n","split":false,"rowsPerMsg":1,"x":670,"y":460,"wires":[["762caff19e1722b7"]]},{"id":"58a32e0340ed268d","type":"inject","z":"9db2d9ed7f00b8af","g":"e00a02a11a322683","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":460,"wires":[["ae3e4a6940a8e236"]]},{"id":"762caff19e1722b7","type":"debug","z":"9db2d9ed7f00b8af","g":"e00a02a11a322683","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":460,"wires":[]},{"id":"997d92441f4d2eeb","type":"comment","z":"9db2d9ed7f00b8af","g":"e00a02a11a322683","name":"Named parameterized query","info":"","x":180,"y":400,"wires":[]},{"id":"ae3e4a6940a8e236","type":"change","z":"9db2d9ed7f00b8af","g":"e00a02a11a322683","name":"Set queryParameters","rules":[{"t":"set","p":"queryParameters","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"queryParameters.id","pt":"msg","to":"3","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":460,"wires":[["14d47d8931a2855b"]]},{"id":"fd2f3651994d44d3","type":"group","z":"9db2d9ed7f00b8af","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["423532459421ddb0","f14ffc1ff606f377","28579ce56dbb5dab","35f9bc3ff82a3789","68d530295082581a","2cb15cbf3a1a3878"],"x":34,"y":199,"w":892,"h":142},{"id":"423532459421ddb0","type":"tables-query","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"","query":"INSERT INTO \"sensor_data\" (\"sensor_id\", \"temperature\", \"unit\")\nVALUES ($1, $2, $3);\n","split":false,"rowsPerMsg":1,"x":670,"y":300,"wires":[["28579ce56dbb5dab"]]},{"id":"f14ffc1ff606f377","type":"inject","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":300,"wires":[["68d530295082581a"]]},{"id":"28579ce56dbb5dab","type":"debug","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":300,"wires":[]},{"id":"35f9bc3ff82a3789","type":"comment","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"Numeric parameterized query","info":"","x":180,"y":240,"wires":[]},{"id":"68d530295082581a","type":"function","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"Simulate Sensor","func":"// Simulate sensor data and output as an object in msg.payload\n\nlet temperature = (20 + Math.random() * 10).toFixed(2);\nlet sensorIds = [\"sensor_01\", \"sensor_02\", \"sensor_03\"];\nlet sensor_id = sensorIds[Math.floor(Math.random() * sensorIds.length)];\n\nmsg.payload = {\n sensor_id: sensor_id,\n temperature: Number(temperature),\n unit: \"celsius\",\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":300,"wires":[["2cb15cbf3a1a3878"]]},{"id":"2cb15cbf3a1a3878","type":"change","z":"9db2d9ed7f00b8af","g":"fd2f3651994d44d3","name":"Set Params","rules":[{"t":"set","p":"params","pt":"msg","to":"[msg.payload.sensor_id, msg.payload.temperature, msg.payload.unit]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":300,"wires":[["423532459421ddb0"]]},{"id":"bf517ea3d503dd5a","type":"global-config","env":[],"modules":{"@flowfuse/nr-tables-nodes":"0.1.0"}}] ``` :: ## Generate Queries with FlowFuse Expert In the Query node, click **"Assistant"**, enter plain English like *"Show me all readings from today"*, and the AI automatically generates the SQL query. :video{ariaLabel="Query Node FlowFuse Expert" autoPlay="true" height="1702" loop="true" muted="true" playsInline="true" preload="none" width="3024"} For more detailed information on natural language queries with the Query node, read this article: [FlowFuse Expert for FlowFuse Tables](https://flowfuse.com/blog/2025/09/ai-assistant-flowfuse-tables/). # Hub Certified Nodes This section contains documentation for **FlowFuse Hub Certified Nodes** that connect your FlowFuse instances to IT systems, databases, cloud services, APIs, and enterprise applications. FlowFuse Certified Nodes are packages that FlowFuse has vetted for quality, security, and support, and maintains on an ongoing basis. To learn more about what certification means and how these nodes are delivered, [read the FlowFuse Certified Nodes blog post](https://flowfuse.com/blog/2025/07/certified-nodes-v2/). ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The FlowFuse Hub Certified Nodes catalogue is part of the **FlowFuse Hub** offering. [Contact us](https://flowfuse.com/contact-us/) to get access or to learn more. ::: :: ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} This section is expanding. We are actively working to bring more Hub Certified Nodes to FlowFuse, and additional documentation will be added here over time. ::: :: ## Nodes This section lists the **Hub Certified Nodes** documented in FlowFuse: - [Redis](https://flowfuse.com/docs/flowfuse-nodes/hub/redis/): A FlowFuse-certified package that lets you connect to Redis, store and retrieve data, publish and subscribe to messages, execute commands, and integrate Redis into your flows. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: # Redis A FlowFuse-certified package that lets you connect to Redis, store and retrieve data, publish and subscribe to messages, execute commands, and integrate Redis into your flows. Built on [ioredis](https://github.com/luin/ioredis){rel=""nofollow""}, so anything ioredis supports (Cluster, Sentinel, TLS, connection strings, IORedis options objects) is supported here too. ## Features - Run any Redis command through a single configurable node - Publish/subscribe for pub/sub messaging patterns - Blocking list operations for building simple queues - Run Lua scripts on the server for atomic, multi-step operations - Inject a live Redis client into flow/global context for direct API access in function nodes - Connections are pooled per config node, one connection is reused across every node pointed at the same server config, unless a node explicitly requests a dedicated (blocking) connection - TLS and Redis Cluster support ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The Redis node is not available by default. It is part of the FlowFuse Hub Certified Nodes catalogue, which is part of the **FlowFuse Hub** offering. Please contact our sales team at [Contact us](https://flowfuse.com/contact-us/) to learn more or to request access. ::: :: ## Use case Redis is commonly used with FlowFuse as a fast shared data layer between flows and instances, since Node-RED's own context is per-instance and does not span multiple instances. This package exposes Redis through several nodes, each suited to a different pattern: - **Shared state and caching**: `redis-command` runs `SET`/`GET` and other commands, so several flows or instances can read and update the same values: a cached sensor reading, a rate counter, or an inventory count. Setting an expiry (`EX`) turns any key into a time-limited cache entry. - **Work queues**: `redis-out` with `rpush` adds items to a list, and `redis-in` with `blpop` lets a worker flow pick them up in order as they arrive, decoupling a producer flow from a consumer. - **Messaging (pub/sub)**: `redis-out` `publish` broadcasts a message to every `redis-in` `subscribe` node listening on that channel at once, across instances. - **Atomic operations**: `redis-lua-script` runs a Lua script on the server in a single round trip, so a multi-step operation cannot interleave with another client, for example checking and decrementing stock so two concurrent orders cannot oversell the last unit. - **Direct client access**: for anything the nodes above do not cover (pipelines, `SCAN`, RedisJSON, or other module commands), `redis-instance` injects a live ioredis client into context for use in function nodes. Connections are pooled per config node, so pointing many nodes at the same server reuses one connection rather than opening a separate one for each. Blocking operations such as `subscribe` and `blpop` take their own dedicated connection automatically. ## Install 1. Open the **Palette Manager** from the top-right menu in the FlowFuse editor. 2. Switch to the **Install** tab. 3. Find the **FlowFuse Hub Certified Nodes** collection. 4. Locate `@flowfuse-certified-nodes/redis` and click **Install**. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version. ::: :: ## Nodes in this package | Node | Type | Purpose | | -------------------- | ---------------- | ----------------------------------------------------------------------------------- | | **redis-config** | config node | Holds the connection details shared by every other node | | **redis-command** | request/response | Executes any Redis command (`GET`, `SET`, `HMSET`, `SADD`, ...) | | **redis-in** | input | Subscribes to pub/sub channels/patterns, or performs blocking reads (`BLPOP`, etc.) | | **redis-out** | output | Publishes messages or pushes to lists | | **redis-lua-script** | request/response | Runs a Lua script on the server, optionally cached (`EVALSHA`) | | **redis-instance** | injector | Places a live ioredis client into flow or global context | Every node except `redis-config` and `redis-instance` sits inline in a flow: it receives a `msg`, talks to Redis, and sends a `msg` on. ## Configuring the connection (redis-config) You create **one `redis-config` node per Redis server** you connect to, then point every other node at it: you don't re-enter connection details on each node. 1. Drag any Redis node onto the canvas and open it 2. Click the pencil icon next to **Server** to add a new connection 3. Fill in: - **Name**, a friendly label for this connection, e.g. `Production Redis` - **Connection Options**, either a connection string, or a JSON object of [ioredis options](https://github.com/luin/ioredis#connect-to-redis){rel=""nofollow""} - **Cluster**, enable if connecting to a Redis Cluster 4. Click **Add**, then **Done** Connection Options accepts either format, use whichever is more convenient: ```text redis://username:password@your-redis-host:6379/0 ``` ```json { "host": "your-redis-host", "port": 6379, "password": "your-password", "db": 0 } ``` The configuration examples in this README use `localhost`, `6379`, and `db: 0` as placeholder values for a local Redis instance. These values are provided for demonstration purposes only and should be replaced with the connection details for your own Redis server. Every node pointed at the same `redis-config` node shares one underlying connection automatically. A node only opens its own dedicated connection when it needs to hold one open: `redis-in` does this automatically for `subscribe`/`psubscribe`/blocking commands, or you can force it with a node's **Block** option. ## What `redis-command` accepts `redis-command` runs any Redis command and returns the reply as `msg.payload`. Its inputs come from two places, and either can be fixed on the node or supplied dynamically on the incoming `msg`: | Field | Node config | Incoming `msg` | Meaning | | ----------- | ------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | | **Command** | `command` | | The Redis command to run (`set`, `get`, `hmset`, `sadd`, ...). Always fixed on the node. | | **Key** | `topic` | `msg.topic` | The key the command operates on. Leave the node's **Topic** blank to take it from `msg.topic` instead. | | **Params** | `params` (+ `paramsType`) | `msg.payload` | The remaining arguments, as a JSON array. Leave the node's **Params** empty to take them from `msg.payload` instead. | | **Server** | `server` | | Which `redis-config` connection to use. | The reply is written to `msg.payload`; `msg.topic` is passed through unchanged. **Example, `SET mykey "Hello there"`:** either fix `topic: mykey` and `params: ["Hello there"]` on the node and inject any message, or leave both blank and inject `{"topic": "mykey", "payload": ["Hello there"]}`. The reply is `OK`. **Example, `GET mykey`:** fix `topic: mykey` on the node (`get` takes no extra params, so `payload` can be `[]` or omitted). The reply is `Hello there`. For the full list of commands and their arguments, see the [official Redis command reference](https://redis.io/docs/latest/commands/){rel=""nofollow""}. ## Working with JSON Redis stores everything as strings. `redis-command` doesn't stringify or parse for you, so wrap it with a `json` node on the way in and out: - **Writing:** `msg.payload` must already be a string when it reaches `redis-command`, put a `json` node (stringify mode) before it, or stringify in a `change`/`function` node. - **Reading:** the reply on `msg.payload` comes back as a string: put a `json` node (parse mode) after it to get an object back. ## Pub/Sub messaging **`redis-out`** publishes. Config: **Command** `publish`, **Topic** the channel to publish on (or take it from `msg.topic` if left blank). `msg.payload` is the message sent. **`redis-in`** subscribes. Config: **Command** `subscribe` (exact channel) or `psubscribe` (pattern, e.g. `TOPIC:*`), **Topic** the channel/pattern. It has no fixed input, once deployed it emits one `msg` per received message, with `msg.payload` the message body and `msg.topic` the channel it arrived on. Subscribing opens a dedicated blocking connection automatically (no need to set **Block** yourself) and stays open until the node is redeployed or removed. ## Lists / queues **`redis-out`** with **Command** `rpush`: **Topic** is the list key (or `msg.topic`), `msg.payload` is the value pushed. **`redis-in`** with **Command** `blpop`: **Topic** is the list key, **Timeout** is how long to block (`0` = wait forever). It emits a `msg` as soon as an item is available, with `msg.payload` the popped value, a simple queue consumer paired with the `rpush` producer above. ## Lua scripting (redis-lua-script) Use Lua scripts for atomic, multi-step operations that would otherwise need several round trips and risk a race between them. Keys go through `KEYS[]`, everything else through `ARGV[]`. | Field | Node config | Meaning | | ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Keys** | `keyval` | How many leading elements of `msg.payload` are treated as Redis keys (`KEYS[1]`, `KEYS[2]`, ...); the rest become `ARGV[]`. | | **Script** | `func` | The Lua source to run. | | **Stored** | `stored` | Cache the script on the server with `SCRIPT LOAD` and invoke it by SHA (`EVALSHA`) instead of resending the source every call, worth enabling once a script is stable. | | **Server** | `server` | Which `redis-config` connection to use. | The script's return value becomes `msg.payload`. ```lua -- keyval: 1, payload: ["key", "value"] local foo = redis.call('SET', KEYS[1], ARGV[1]) return foo ``` `cjson.encode`/`cjson.decode` are available inside scripts for working with JSON payloads server-side, see the sample flow below for examples including `ZADD`/`ZRANGE` with encoded JSON members, and string manipulation with `string.sub`. **A more realistic example, atomically check and decrement stock, so two concurrent orders can't both succeed against the last unit:** ```lua -- keyval: 1, payload: ["inventory:product:SKU-12345", 3] local key = KEYS[1] local requested = tonumber(ARGV[1]) local current = tonumber(redis.call('GET', key) or "0") if current >= requested then redis.call('DECRBY', key, requested) return {1, current - requested} else return {0, current} end ``` `msg.payload` comes back as `[1, remaining]` on success or `[0, available]` if there wasn't enough stock, unpack it in a `function` node afterwards. ## Direct client access (redis-instance) For anything not covered by the other nodes (pipelines, `SCAN`, custom/module commands), drop a `redis-instance` node on the canvas. It doesn't sit inline in a flow; it just injects a ready-to-use ioredis client into context on deploy. | Field | Node config | Meaning | | ------------ | ----------- | ---------------------------------------------------------------------- | | **Server** | `server` | Which `redis-config` connection to use. | | **Topic** | `topic` | The variable name the client is stored under in context, e.g. `redis`. | | **Location** | `location` | `flow` or `global`, where in context the client is placed. | Deploy, then use it in any function node: ```javascript const redis = flow.get('redis'); // or global.get('redis') const info = await redis.info(); msg.payload = info; return msg; ``` **Pipelining** multiple writes in one round trip: ```javascript const redis = flow.get('redis'); const pipeline = redis.pipeline(); sensors.forEach(sensor => { pipeline.set(`sensor:${sensor.id}:latest`, JSON.stringify(sensor), 'EX', 3600); }); pipeline.exec((err, results) => { if (err) { node.error(err, msg); return; } msg.payload = { stored: results.length }; node.send(msg); }); ``` **Scanning keys** without blocking the server (`KEYS *` blocks Redis on large datasets, `SCAN` doesn't): ```javascript const redis = flow.get('redis'); let cursor = '0'; const keys = []; do { const [next, batch] = await redis.scan(cursor, 'MATCH', 'sensor:*:latest', 'COUNT', 100); cursor = next; keys.push(...batch); } while (cursor !== '0'); msg.payload = keys; return msg; ``` Custom/module commands (e.g. RedisJSON, RediSearch) can also be issued via `redis.call('MODULE.COMMAND', ...)` on the injected client. ## Sample flow ::render-flow{:height='300'} ```json [{"id":"2c33ebeb73062ab3","type":"group","z":"FFF0000000000001","name":"Your First Redis Flow","style":{"label":true},"nodes":["e5f3326ef736cef6","8486e88d8623b42a","d1ebde22d0e224a8","22db25c60af16ab2","fe2be532c53402b3","ebc1e8fc0f1ba2d6","adb95c63d206c633"],"x":414,"y":1219,"w":712,"h":162},{"id":"e5f3326ef736cef6","type":"redis-command","z":"FFF0000000000001","g":"2c33ebeb73062ab3","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"mykey","params":"[\"Hello from Node-RED\"]","paramsType":"json","payloadType":"json","block":false,"x":710,"y":1260,"wires":[["8486e88d8623b42a"]]},{"id":"8486e88d8623b42a","type":"debug","z":"FFF0000000000001","g":"2c33ebeb73062ab3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":870,"y":1260,"wires":[]},{"id":"d1ebde22d0e224a8","type":"redis-command","z":"FFF0000000000001","g":"2c33ebeb73062ab3","server":"e370dc92b39a7ba4","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":900,"y":1340,"wires":[["22db25c60af16ab2"]]},{"id":"22db25c60af16ab2","type":"debug","z":"FFF0000000000001","g":"2c33ebeb73062ab3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1030,"y":1340,"wires":[]},{"id":"fe2be532c53402b3","type":"change","z":"FFF0000000000001","g":"2c33ebeb73062ab3","name":"Set Key for GET","rules":[{"t":"set","p":"topic","pt":"msg","to":"mykey","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"[]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":1340,"wires":[["d1ebde22d0e224a8"]]},{"id":"ebc1e8fc0f1ba2d6","type":"inject","z":"FFF0000000000001","g":"2c33ebeb73062ab3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1260,"wires":[["e5f3326ef736cef6"]]},{"id":"adb95c63d206c633","type":"inject","z":"FFF0000000000001","g":"2c33ebeb73062ab3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1340,"wires":[["fe2be532c53402b3"]]},{"id":"e370dc92b39a7ba4","type":"redis-config","name":"Local","options":"{\"host\":\"localhost\",\"port\":6379,\"db\":0}","cluster":false,"optionsType":"json"},{"id":"5e2842a4730589c0","type":"group","z":"FFF0000000000001","name":"Working with JSON Data","style":{"label":true},"nodes":["6acc07a393e5ee26","3255e8a9122253b7","a3b0b4a74133778e","4faba929a84fb358","e5c359b934174c8e","abf011c2e81960c4","22a2b54caae5869d","b9598f313e2a9fd2","d0051f637fd1621a","9e59f275496d722d"],"x":414,"y":1399,"w":852,"h":162},{"id":"6acc07a393e5ee26","type":"redis-command","z":"FFF0000000000001","g":"5e2842a4730589c0","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1040,"y":1440,"wires":[["3255e8a9122253b7"]]},{"id":"3255e8a9122253b7","type":"debug","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1170,"y":1440,"wires":[]},{"id":"a3b0b4a74133778e","type":"inject","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1440,"wires":[["d0051f637fd1621a"]]},{"id":"4faba929a84fb358","type":"redis-command","z":"FFF0000000000001","g":"5e2842a4730589c0","server":"e370dc92b39a7ba4","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":900,"y":1520,"wires":[["b9598f313e2a9fd2"]]},{"id":"e5c359b934174c8e","type":"debug","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1170,"y":1520,"wires":[]},{"id":"abf011c2e81960c4","type":"change","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"Set Key for GET","rules":[{"t":"set","p":"topic","pt":"msg","to":"sensor:data","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"[]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":1520,"wires":[["4faba929a84fb358"]]},{"id":"22a2b54caae5869d","type":"inject","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1520,"wires":[["abf011c2e81960c4"]]},{"id":"b9598f313e2a9fd2","type":"json","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"","property":"payload","action":"","pretty":false,"x":1030,"y":1520,"wires":[["e5c359b934174c8e"]]},{"id":"d0051f637fd1621a","type":"change","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"Set Key","rules":[{"t":"set","p":"topic","pt":"msg","to":"sensor:data","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"{\t \"temperature\": 22.5,\t \"humidity\": 65,\t \"timestamp\": $now()\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":680,"y":1440,"wires":[["9e59f275496d722d"]]},{"id":"9e59f275496d722d","type":"json","z":"FFF0000000000001","g":"5e2842a4730589c0","name":"","property":"payload","action":"","pretty":false,"x":890,"y":1440,"wires":[["6acc07a393e5ee26"]]},{"id":"39de5da95585227f","type":"group","z":"FFF0000000000001","name":"Pub/Sub Messaging","style":{"label":true},"nodes":["7756558fd542d657","bf2dc26107eff967","9856ddb8e7bc28e8","bcd9b22fd67ecf7c","22f0526fab3b589b"],"x":414,"y":1579,"w":632,"h":162},{"id":"7756558fd542d657","type":"inject","z":"FFF0000000000001","g":"39de5da95585227f","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1620,"wires":[["9856ddb8e7bc28e8"]]},{"id":"bf2dc26107eff967","type":"redis-out","z":"FFF0000000000001","g":"39de5da95585227f","server":"e370dc92b39a7ba4","command":"publish","name":"","topic":"alerts:temperature","obj":true,"x":930,"y":1620,"wires":[]},{"id":"9856ddb8e7bc28e8","type":"change","z":"FFF0000000000001","g":"39de5da95585227f","name":"Set Alert Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"ALERT: Temperature critical in Zone A: 85°C - Equipment shutdown initiated","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":1620,"wires":[["bf2dc26107eff967"]]},{"id":"bcd9b22fd67ecf7c","type":"redis-in","z":"FFF0000000000001","g":"39de5da95585227f","server":"e370dc92b39a7ba4","command":"subscribe","name":"","topic":"alerts:temperature","obj":true,"timeout":0,"x":530,"y":1700,"wires":[["22f0526fab3b589b"]]},{"id":"22f0526fab3b589b","type":"debug","z":"FFF0000000000001","g":"39de5da95585227f","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":890,"y":1700,"wires":[]},{"id":"adc4b0df6b0e84e3","type":"group","z":"FFF0000000000001","name":"Using Lua Scripts for Atomic Operations","style":{"label":true},"nodes":["ca8a523710138a11","8804755befa1fbc8","0898e80e28b7d276","0b16690b3c2c10df","9a03ebdea51eefdc","c641d8e54620a73a","7c11a5f1c9716527","a07a025db962be50"],"x":414,"y":1759,"w":1112,"h":142},{"id":"ca8a523710138a11","type":"inject","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1860,"wires":[["8804755befa1fbc8"]]},{"id":"8804755befa1fbc8","type":"function","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"Prepare Lua Script Arguments","func":"// msg.payload format: [keys..., args...]\n// First element(s) are the keys, remaining elements are arguments\nmsg.payload = [\n \"inventory:product:SKU-12345\", // KEYS[1]\n 3 // ARGV[1] - quantity requested\n];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":1860,"wires":[["0898e80e28b7d276"]]},{"id":"0898e80e28b7d276","type":"redis-lua-script","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","server":"e370dc92b39a7ba4","name":"","keyval":"1","func":"local key = KEYS[1]\nlocal requested = tonumber(ARGV[1])\n\nlocal current = tonumber(redis.call('GET', key) or \"0\")\n\nif current >= requested then\n redis.call('DECRBY', key, requested)\n return {1, current - requested}\nelse\n return {0, current}\nend","stored":false,"block":false,"x":1040,"y":1860,"wires":[["0b16690b3c2c10df"]]},{"id":"0b16690b3c2c10df","type":"function","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"Format Lua Script Response","func":"const result = msg.payload;\nconst success = result[0];\nconst remaining = result[1];\n\nif (success === 1) {\n msg.payload = {\n status: \"success\",\n message: `Order processed. Remaining stock: ${remaining}`,\n remaining: remaining\n };\n} else {\n msg.payload = {\n status: \"failed\",\n message: `Insufficient stock. Available: ${remaining}`,\n available: remaining\n };\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1240,"y":1860,"wires":[["9a03ebdea51eefdc"]]},{"id":"9a03ebdea51eefdc","type":"debug","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1430,"y":1860,"wires":[]},{"id":"c641d8e54620a73a","type":"redis-command","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"inventory:product:SKU-12345","params":"[10]","paramsType":"json","payloadType":"json","block":false,"x":780,"y":1800,"wires":[["7c11a5f1c9716527"]]},{"id":"7c11a5f1c9716527","type":"debug","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1030,"y":1800,"wires":[]},{"id":"a07a025db962be50","type":"inject","z":"FFF0000000000001","g":"adc4b0df6b0e84e3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":1800,"wires":[["c641d8e54620a73a"]]},{"id":"01a96fb2044d723f","type":"group","z":"FFF0000000000001","name":"Direct Redis Client Access with redis-instance","style":{"label":true},"nodes":["745f41f352fa2212","a9ec9a8a5a66daa3","e3a5634ef0ebcb78","5192081309e08db9","4f169053bbd7add9","15f0b0462f795828","4a394700c6914961"],"x":414,"y":1919,"w":732,"h":222},{"id":"745f41f352fa2212","type":"redis-instance","z":"FFF0000000000001","g":"01a96fb2044d723f","server":"e370dc92b39a7ba4","name":"","topic":"redis","location":"flow","x":490,"y":1960,"wires":[]},{"id":"a9ec9a8a5a66daa3","type":"inject","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":520,"y":2040,"wires":[["e3a5634ef0ebcb78"]]},{"id":"e3a5634ef0ebcb78","type":"function","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"Batch Store Sensor Readings (Pipeline)","func":"const redis = flow.get('redis');\n\n// Create a pipeline\nconst pipeline = redis.pipeline();\n\n// Add multiple sensor readings in one batch\nconst sensors = [\n { id: 'temp-01', value: 23.5, unit: 'C' },\n { id: 'temp-02', value: 24.1, unit: 'C' },\n { id: 'humidity-01', value: 65, unit: '%' },\n { id: 'pressure-01', value: 1013, unit: 'hPa' }\n];\n\nsensors.forEach(sensor => {\n const key = `sensor:${sensor.id}:latest`;\n const data = JSON.stringify({\n value: sensor.value,\n unit: sensor.unit,\n timestamp: Date.now()\n });\n pipeline.set(key, data, 'EX', 3600); // Expire in 1 hour\n});\n\n// Execute all commands at once\npipeline.exec((err, results) => {\n if (err) {\n node.error(err, msg);\n return;\n }\n\n msg.payload = {\n message: `Stored ${results.length} sensor readings`,\n results: results\n };\n node.send(msg);\n});","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":2040,"wires":[["5192081309e08db9"]]},{"id":"5192081309e08db9","type":"debug","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":2040,"wires":[]},{"id":"4f169053bbd7add9","type":"inject","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":520,"y":2100,"wires":[["15f0b0462f795828"]]},{"id":"15f0b0462f795828","type":"function","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"Scan and List Latest Sensor Keys","func":"const redis = flow.get('redis');\n\nasync function scanKeys() {\n const matchPattern = 'sensor:*:latest';\n const allKeys = [];\n let cursor = '0';\n\n try {\n do {\n // Scan with pattern matching\n const result = await redis.scan(\n cursor,\n 'MATCH', matchPattern,\n 'COUNT', 100\n );\n\n cursor = result[0];\n const keys = result[1];\n allKeys.push(...keys);\n\n } while (cursor !== '0');\n\n msg.payload = {\n pattern: matchPattern,\n count: allKeys.length,\n keys: allKeys\n };\n\n node.send(msg);\n\n } catch (err) {\n node.error(err, msg);\n }\n}\n\nscanKeys();","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":2100,"wires":[["4a394700c6914961"]]},{"id":"4a394700c6914961","type":"debug","z":"FFF0000000000001","g":"01a96fb2044d723f","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":2100,"wires":[]},{"id":"1a2b3c4d5e6f7081","type":"group","z":"FFF0000000000001","name":"Lists and Queues","style":{"label":true},"nodes":["1a2b3c4d5e6f7001","1a2b3c4d5e6f7002","1a2b3c4d5e6f7003","1a2b3c4d5e6f7005","1a2b3c4d5e6f7006","1a2b3c4d5e6f7007"],"x":414,"y":2159,"w":612,"h":162},{"id":"1a2b3c4d5e6f7001","type":"inject","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":2200,"wires":[["1a2b3c4d5e6f7002"]]},{"id":"1a2b3c4d5e6f7002","type":"change","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","name":"Prepare Order Payload","rules":[{"t":"set","p":"payload","pt":"msg","to":"Order #1042 - 2x Widget, 1x Gadget","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":2200,"wires":[["1a2b3c4d5e6f7003"]]},{"id":"1a2b3c4d5e6f7003","type":"redis-out","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","server":"e370dc92b39a7ba4","command":"rpush","name":"","topic":"queue:orders","x":930,"y":2200,"wires":[]},{"id":"1a2b3c4d5e6f7005","type":"inject","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":510,"y":2280,"wires":[["1a2b3c4d5e6f7006"]]},{"id":"1a2b3c4d5e6f7006","type":"redis-in","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","server":"e370dc92b39a7ba4","command":"blpop","name":"","topic":"queue:orders","timeout":0,"x":750,"y":2280,"wires":[["1a2b3c4d5e6f7007"]]},{"id":"1a2b3c4d5e6f7007","type":"debug","z":"FFF0000000000001","g":"1a2b3c4d5e6f7081","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":2280,"wires":[]},{"id":"2a2b3c4d5e6f7082","type":"group","z":"FFF0000000000001","name":"Lua Scripting - JSON and Sorted Sets","style":{"label":true},"nodes":["2a2b3c4d5e6f7001","2a2b3c4d5e6f7002","2a2b3c4d5e6f7003","2a2b3c4d5e6f7004","2a2b3c4d5e6f7005","2a2b3c4d5e6f7006","2a2b3c4d5e6f7007","2a2b3c4d5e6f7008","2a2b3c4d5e6f7009","2a2b3c4d5e6f700a","2a2b3c4d5e6f700b","2a2b3c4d5e6f700c","2a2b3c4d5e6f700d","2a2b3c4d5e6f700e","2a2b3c4d5e6f700f","2a2b3c4d5e6f7010","2a2b3c4d5e6f7011","2a2b3c4d5e6f7012","2a2b3c4d5e6f7013","2a2b3c4d5e6f7014"],"x":404,"y":2339,"w":952,"h":322},{"id":"2a2b3c4d5e6f7001","type":"inject","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Add Reading (ZADD)","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":2380,"wires":[["2a2b3c4d5e6f7002"]]},{"id":"2a2b3c4d5e6f7002","type":"function","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Prepare ZADD Args","func":"// msg.payload format: [key, score, JSON-encoded member]\nconst reading = { test: 1, hello: \"world1\", hexstr: \"000102\" };\nmsg.payload = [\n \"device:96a4:1a87:04\", // KEYS[1]\n Date.now(), // ARGV[1] - score\n JSON.stringify(reading) // ARGV[2] - member\n];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":2380,"wires":[["2a2b3c4d5e6f7003"]]},{"id":"2a2b3c4d5e6f7003","type":"redis-lua-script","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","server":"e370dc92b39a7ba4","name":"ZADD with JSON Member","keyval":"1","func":"local foo = redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2])\nreturn foo","stored":false,"block":false,"x":1020,"y":2380,"wires":[["2a2b3c4d5e6f7004"]]},{"id":"2a2b3c4d5e6f7004","type":"debug","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":2380,"wires":[]},{"id":"2a2b3c4d5e6f7005","type":"inject","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Decode Member","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":2440,"wires":[["2a2b3c4d5e6f7006"]]},{"id":"2a2b3c4d5e6f7006","type":"function","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Prepare Decode Args","func":"msg.payload = [\n \"device:96a4:1a87:04\",\n \"{\\\"temperature\\\":22.5,\\\"hello\\\":\\\"world\\\"}\"\n];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":2440,"wires":[["2a2b3c4d5e6f7007"]]},{"id":"2a2b3c4d5e6f7007","type":"redis-lua-script","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","server":"e370dc92b39a7ba4","name":"cjson.decode Extract Field","keyval":"1","func":"local foo = cjson.decode(ARGV[1])\nreturn foo.temperature","stored":false,"block":false,"x":1020,"y":2440,"wires":[["2a2b3c4d5e6f7008"]]},{"id":"2a2b3c4d5e6f7008","type":"debug","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":2440,"wires":[]},{"id":"2a2b3c4d5e6f7009","type":"inject","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Build JSON (cjson.encode)","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":2500,"wires":[["2a2b3c4d5e6f700a"]]},{"id":"2a2b3c4d5e6f700a","type":"redis-lua-script","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","server":"e370dc92b39a7ba4","name":"cjson.encode Build JSON","keyval":"0","func":"local foo = {}\nfoo.field1 = 1\nfoo.field2 = \"hello world\"\nfoo.field3 = {}\nfoo.field3.name = \"paul\"\nreturn cjson.encode(foo)","stored":false,"block":false,"x":840,"y":2500,"wires":[["2a2b3c4d5e6f700b"]]},{"id":"2a2b3c4d5e6f700b","type":"json","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"","property":"payload","action":"","pretty":false,"x":1020,"y":2500,"wires":[["2a2b3c4d5e6f700c"]]},{"id":"2a2b3c4d5e6f700c","type":"debug","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":2500,"wires":[]},{"id":"2a2b3c4d5e6f700d","type":"inject","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Extract Substring","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":2560,"wires":[["2a2b3c4d5e6f700e"]]},{"id":"2a2b3c4d5e6f700e","type":"function","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Prepare string.sub Args","func":"msg.payload = [\"hexstr:060708\"];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":2560,"wires":[["2a2b3c4d5e6f700f"]]},{"id":"2a2b3c4d5e6f700f","type":"redis-lua-script","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","server":"e370dc92b39a7ba4","name":"string.sub Extract Substring","keyval":"0","func":"local foo = string.sub(ARGV[1], -6, -1)\nreturn foo","stored":false,"block":false,"x":1020,"y":2560,"wires":[["2a2b3c4d5e6f7010"]]},{"id":"2a2b3c4d5e6f7010","type":"debug","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":2560,"wires":[]},{"id":"2a2b3c4d5e6f7011","type":"inject","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Query Sorted Set (ZRANGE)","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":2620,"wires":[["2a2b3c4d5e6f7012"]]},{"id":"2a2b3c4d5e6f7012","type":"function","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Prepare ZRANGE Args","func":"msg.payload = [\"device:96a4:1a87:04\"];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":2620,"wires":[["2a2b3c4d5e6f7013"]]},{"id":"2a2b3c4d5e6f7013","type":"redis-lua-script","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","server":"e370dc92b39a7ba4","name":"ZRANGE Query Members","keyval":"1","func":"return redis.call('ZRANGE', KEYS[1], 0, -1)","stored":false,"block":false,"x":1020,"y":2620,"wires":[["2a2b3c4d5e6f7014"]]},{"id":"2a2b3c4d5e6f7014","type":"debug","z":"FFF0000000000001","g":"2a2b3c4d5e6f7082","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":2620,"wires":[]},{"id":"0b0e24520432625b","type":"global-config","env":[],"modules":{"@flowfuse-certified-nodes/redis":"1.0.0"}}] ``` :: # FlowFuse Nodes In Node-RED on the FlowFuse platform, you have access to additional Node-RED nodes that are provided by FlowFuse. These are documented in the below sections as a reference you can use when building integrations and automations. ## Nodes The following documents provide details about the FlowFuse nodes: - [FlowFuse AI Nodes](https://flowfuse.com/docs/flowfuse-nodes/ai/): A set of Node-RED nodes for AI and machine learning, including ONNX model inference and LLM nodes for OpenAI, Anthropic, Google Gemini, and Ollama. - [MCP Nodes](https://flowfuse.com/docs/flowfuse-nodes/mcp/): A set of nodes that enable the creation of MCP (Model Context Protocol) servers in your Node-RED flows for AI-integration. - [FlowFuse Tables](https://flowfuse.com/docs/flowfuse-nodes/flowfuse-tables/): FlowFuse Tables provides managed databases for Node-RED users, offering built-in nodes to query, insert, and manage data easily within FlowFuse flows. - [Edge Certified Nodes](https://flowfuse.com/docs/flowfuse-nodes/edge/): Documentation for FlowFuse Edge Certified Nodes, including nodes for connecting FlowFuse to industrial protocols, PLCs, SCADA systems, and factory-floor equipment. - [Hub Certified Nodes](https://flowfuse.com/docs/flowfuse-nodes/hub/): Documentation for FlowFuse Hub Certified Nodes, including nodes for connecting FlowFuse to IT systems, databases, cloud services, APIs, and enterprise applications. - [MQTT Nodes](https://flowfuse.com/docs/flowfuse-nodes/mqtt/): MQTT In and Out nodes designed for FlowFuse users with automatic configuration. # MCP Nodes This document lists and explains the **MCP nodes** available in FlowFuse. MCP (Model Context Protocol) nodes extend Node-RED to integrate AI models, tools, and resources through the Model Context Protocol framework. Each node helps you connect, configure, and manage AI interactions directly from Node-RED. ## Video introduction ::lite-youtube --- params: rel=0 style: "width: 100%; height: 480px; margin-top: 20px; margin-bottom: 20px;" title: YouTube video player with FlowFuse introduction video to MCP server nodes videoid: troUvaF8V68 --- :: ## Getting Started ### Prerequisites - **A running FlowFuse Enterprise instance.** If you do not have one, [contact us](https://flowfuse.com/contact-us/) to discuss Enterprise options. - **Ensure the `@flowfuse-nodes/nr-mcp-server-nodes` package is installed** in your Node-RED palette. > **Note:** The MCP nodes (@flowfuse-nodes/nr-mcp-server-nodes) are only available on the Enterprise tier. ### Configuring Your MCP Server Before using MCP nodes, you need to configure an MCP Server: 1. Add any **MCP Resource, Tool, or Prompt** node to your workspace 2. Click the **+** button next to Server to create a new configuration 3. Configure the server properties: - **Name**: A descriptive name (e.g., `Node-RED MCP Server`) - **Protocol**: Leave the default `http/sse` - **Path**: Endpoint path for the server (e.g., `/mcp`) 4. Click **Done** to save ### Connecting External Clients or AI Agents Once configured, external AI agents and MCP clients can connect to your server using your instance URL plus the MCP path: **FlowFuse Cloud:** ```text https://your-instance.flowfuse.cloud/mcp ``` **Local Instance:** ```text http://localhost:1880/mcp ``` **Network Instance:** ```text http://192.168.1.100:1880/mcp ``` ### Securing Your MCP Server To protect your MCP server from unauthorized access, enable FlowFuse User Authentication: 1. Navigate to **Settings → Security** in your instance 2. Select **FlowFuse User Authentication** 3. Click **Save Changes**, then **Restart** to apply 4. Click **Add Token** and provide a descriptive name 5. Set an expiry date (recommended for security) 6. Click **Create** and copy the generated token When connecting from external AI agents, include the token in request headers: ```json { "node-red-mcp-server": { "url": "http://:/mcp", "type": "http", "headers": { "Authorization": "Bearer ffhttp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } ``` Replace `ffhttp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx` with your actual token to ensure only authorized clients can access your MCP server resources and tools. ### Using FlowFuse Expert When you have an MCP server built inside your FlowFuse cloud-hosted Node-RED instance, you can use FlowFuse Expert for a simpler and more secure way to interact with it compared to external clients. FlowFuse Expert connects directly to your MCP server, allowing you to query resources and execute tools with built-in role-based access control. > **Note:** FlowFuse Expert currently works with cloud-hosted instances. Support for remote Node-RED instances is planned for future releases. **Getting Started:** You can access FlowFuse Expert in two ways: **1. At the Platform Level:** Open FlowFuse Expert directly from your FlowFuse platform dashboard. ![FlowFuse Expert at Platform Level](https://flowfuse.com/docs/flowfuse-nodes/images/ff-expert-at-platform-level.png) ![FlowFuse Expert Opened at Platform](https://flowfuse.com/docs/flowfuse-nodes/images/ff-expert-opned-platform.png) **2. Within the Node-RED Editor:** To access FlowFuse Expert within your Node-RED instance where you have built your MCP server, open the editor using the **Open Editor** button, then access FlowFuse Expert from there. ![FlowFuse Expert in Editor](https://flowfuse.com/docs/flowfuse-nodes/images/ff-expert-in-editor.png) ![FlowFuse Expert Opened in Editor](https://flowfuse.com/docs/flowfuse-nodes/images/ff-expert-opened.png) Once FlowFuse Expert is open, select your MCP server from the Insights tab, and Expert will automatically discover your resources and tools. You can then ask questions or request actions, and Expert will use your resources and tools based on your role. FlowFuse Expert enforces access control based on the annotations configured in your MCP Tool nodes. Learn more about configuring tool annotations in the [MCP Tool documentation](https://flowfuse.com/docs/flowfuse-nodes/mcp/mcp-tool#annotations). ## Nodes This section lists the **MCP nodes** available in FlowFuse: - [MCP Prompt](https://flowfuse.com/docs/flowfuse-nodes/mcp/mcp-prompt/): The MCP Prompt node allows you to create pre-configured prompt templates that users can easily invoke from their MCP client. - [MCP Resource](https://flowfuse.com/docs/flowfuse-nodes/mcp/mcp-resource/): The MCP Resource node allows you to expose read-only data that AI assistants can access for context. - [MCP Response](https://flowfuse.com/docs/flowfuse-nodes/mcp/mcp-response/): Sends responses back to the MCP client for tools and resources. - [MCP Tool](https://flowfuse.com/docs/flowfuse-nodes/mcp/mcp-tool/): MCP Tool node allows you to create custom tools that FlowFuse Expert can invoke to perform specific tasks. Each listed node provides a unique capability within the MCP ecosystem, from registering tools and resources to managing prompts and responses. Use these nodes to create MCP Servers, provide tools, resources, and prompts to MCP clients, and standardize AI workflows in your Node-RED projects. # MCP Prompt The MCP Prompt node allows you to create pre-configured prompt templates that users can easily invoke from their MCP client. These templates can include variable placeholders that users fill in, making it simple to create consistent, well-structured prompts for common tasks. Unlike tools and resources, prompts don't require flows or response nodes - they simply register the template with the MCP server. ## Flow Requirements **None**, MCP Prompt nodes do not require flows or MCP Response nodes. They only register the prompt template with the MCP server, making it available for users to select and customize in their MCP client. ## Configuration ### Name `string`, *Optional* Optional display name for this node in the flow. This helps identify the node in your Node-RED editor but is not visible to MCP clients. ### Server `mcp-server`, *Required* The MCP server configuration this prompt will be registered with. Select from your configured MCP server instances. ### Prompt ID `string`, *Required* Unique identifier for the prompt used by MCP clients to access this prompt template. Use **snake\_case** for naming. **Examples:** - `holiday_planner` - `code_reviewer` - `bug_report_template` - `meeting_summarizer` ### Title `string`, *Required* Human-readable name shown to users in MCP clients. This is what users see when browsing available prompts. ### Description `string`, *Required* Detailed description of what this prompt does and when to use it. ### Prompt Template `string`, *Required* Define your prompt text using **double curly braces `{{ }}`** for variables. ### Arguments `JSON`, *Required* JSON schema defining the template variables that users can customize. This follows the same **JSON Schema** format used in MCP Tool input schemas. ## Prompt Template Examples ### Basic Example ```text Hello {{ name }}! Welcome to {{ location }}. ``` **Variables:** `name`, `location` ### Complex Example ```text You are a holiday planning agent. You should provide information about {{ location }}. You should also provide rough budget ideas for visiting this place in {{ time_of_year }} for {{ duration }} days. Please breakdown rough ideas for hotels and local tourist hot spots to visit. ``` **Variables:** `location`, `time_of_year`, `duration` ### Multi-Section Example ```text # Code Review Request ## File: {{ filename }} ## Language: {{ language }} Please review the following code: {{ code }} Focus on: - {{ focus_area_1 }} - {{ focus_area_2 }} - {{ focus_area_3 }} Provide feedback on code quality, potential bugs, and suggestions for improvement. ``` **Variables:** `filename`, `language`, `code`, `focus_area_1`, `focus_area_2`, `focus_area_3` ## Argument Schema Examples ### Basic Schema ```json { "type": "object", "properties": { "name": { "type": "string", "description": "Your name", "minLength": 1 }, "location": { "type": "string", "description": "The place you're visiting", "minLength": 1 } }, "required": ["name", "location"] } ``` ### Extended Schema ```json { "type": "object", "properties": { "location": { "type": "string", "description": "A country, city or town somewhere in the world", "minLength": 1 }, "time_of_year": { "type": "string", "description": "A specific date, season or month giving context for the travel period", "minLength": 1 }, "duration": { "type": "number", "default": 7, "description": "The number of days for the trip" } }, "required": ["location", "time_of_year"] } ``` ### Schema with Enums and Defaults ```json { "type": "object", "properties": { "report_type": { "type": "string", "description": "Type of report to generate", "enum": ["daily", "weekly", "monthly", "quarterly"], "default": "weekly" }, "include_charts": { "type": "boolean", "description": "Include visual charts in the report", "default": true }, "detail_level": { "type": "string", "description": "Level of detail for the report", "enum": ["summary", "detailed", "comprehensive"], "default": "detailed" } }, "required": ["report_type"] } ``` ## Example Flow ::render-flow{:height='300'} ```json [{"id":"1dd56abc53f50de0","type":"group","z":"FFF0000000000001","name":"MCP Prompts","style":{"label":true},"nodes":["cc2bedb852d8cea4","241e6041470d735c"],"x":714,"y":819,"w":212,"h":142},{"id":"cc2bedb852d8cea4","type":"mcp-prompt","z":"FFF0000000000001","g":"1dd56abc53f50de0","name":"","server":"28907ed9ddcdd4b9","promptId":"uppercase","title":"Uppercase","description":"This prompt will return the provided content in all uppercase characters","template":"Please return the following in uppercase: {{message}}","arguments":"{\n \"type\": \"object\",\n \"properties\": {\n \"message\": {\n \"type\": \"string\",\n \"description\": \"The text provided by the user\"\n }\n },\n \"required\": [\"message\"]\n}","x":800,"y":920,"wires":[]},{"id":"241e6041470d735c","type":"mcp-prompt","z":"FFF0000000000001","g":"1dd56abc53f50de0","name":"","server":"28907ed9ddcdd4b9","promptId":"holiday_planner","title":"Holiday Planning and Budgeting Agent","description":"This prompt can assist users with creating budgets for their desired holiday destinations. ","template":"You are a holiday planning agent.\nYou should provide information about {{ location }}.\nYou should also provide rough budget ideas for visiting this place in {{ time_of_year }} for {{ duration }} days.\nPlease breakdown rough ideas for hotels and local tourist hot spots to visit.","arguments":"{\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"A country, city or town somewhere in the world.\",\n \"minLength\": 1\n },\n \"time_of_year\": {\n \"type\": \"string\",\n \"description\": \"This can be a specific date, season or month. Used to provide context to the agent as to when in the year the user wants to take their holiday.\",\n \"minLength\": 1\n },\n \"duration\": {\n \"type\": \"number\",\n \"default\": 7,\n \"description\": \"The number of days\"\n }\n },\n \"required\": [\"location\", \"time_of_year\"]\n}","x":820,"y":860,"wires":[]},{"id":"28907ed9ddcdd4b9","type":"mcp-server","name":"My Node-RED MCP Server","protocol":"http","path":"/mcp"},{"id":"89a436fa564e3d58","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-mcp-server-nodes":"0.1.2"}}] ``` :: # MCP Resource The MCP Resource node allows you to expose read-only data that AI assistants can access for context. Resources are designed to provide information without performing actions or causing side effects. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information ## Flow Requirements MCP Resource nodes must be connected to a flow that ends with an **MCP Response** node to send the resource content back to the MCP client. ## Configuration ### Name `string` - Optional Optional display name for this node in the flow. This helps you identify the node in your Node-RED editor but is not visible to MCP clients. ### Server `mcp-server` - Required The MCP server configuration this resource will be registered with. Select from your configured MCP server instances. ### ID `string` - Required Unique identifier for the resource used by MCP clients to access this resource. Should be written in snake\_case. **Examples:** - `user_database` - `config_files` - `api_documentation` - `product_catalog` ### URI `string` - Required URI of the resource. This should be unique for each resource you expose. The URI follows a scheme-based format similar to file paths or URLs. **Static Resource Examples:** - `file://config.json` - `db://schema/users` - `local://documentation/api` - `app://settings/theme` **Dynamic Resource Template Examples:** - `github://repos/{owner}/{repo}` - `local://books/{genre}` - `db://users/{user_id}/orders` - `file://logs/{date}/{level}` ### Title `string` - Required Human-readable name shown to users in MCP clients. This is what users see when browsing available resources. ### MIME Type `string` - Required MIME type of the resource content. This tells the MCP client how to interpret the data you return. **Common MIME Types:** - `application/json` - JSON data - `text/plain` - Plain text - `text/markdown` - Markdown formatted text - `text/html` - HTML content - `application/xml` - XML data - `text/csv` - CSV files - `application/pdf` - PDF documents ### Description `string` - Required Detailed description of what this resource provides and when to use it. ## URI Types ### Static Resources Static resources have a fixed URI and return the same data each time they're accessed. These are ideal for configuration files, schemas, documentation, and other unchanging reference materials. **Example URI:** `file://path/to/config.json` Your flow would always return the content of that specific file. **Use Cases:** - Application configuration files - Database schemas - API documentation - System specifications - Reference data ### Resource Templates Resource templates have URIs with dynamic components based on user-defined input. Variables in the URI are enclosed in curly braces `{}`. **Example URI:** `github://repos/{owner}/{repo}` When an MCP client requests this resource with specific values (e.g., `github://repos/flowfuse/node-red`), the variables are passed to your flow in `msg.payload`, You can then use these variables to fetch and return the appropriate content. **More Examples:** **Single Variable:** ```text local://books/{genre} → msg.payload.genre = "science-fiction" ``` **Multiple Variables:** ```text db://users/{user_id}/orders/{order_id} → msg.payload.user_id = "12345" → msg.payload.order_id = "67890" ``` **Date-based Resources:** ```text file://logs/{date}/{level} → msg.payload.date = "2024-01-15" → msg.payload.level = "error" ``` ## Output ### Static Resources For static resources, your flow simply returns the content in `msg.payload`. The MCP Response node will send it to the client. ```javascript msg.payload = { database: "users", tables: ["users", "profiles", "settings"] }; ``` ### Dynamic Resource Templates For resource templates, the input `msg.payload` contains the variables from the URI. You use these to fetch the appropriate content. **Example:** URI: `local://books/{genre}` Input received: ```javascript msg.payload = { genre: "horror" } ``` Your flow processes this and returns: ```javascript msg.payload = { genre: "horror", books: [ { title: "Dracula", author: "Bram Stoker" }, { title: "Frankenstein", author: "Mary Shelley" } ] }; ``` ## Example Flow ::render-flow ```json [{"id":"00744c2e2a560c36","type":"group","z":"e1ceeedf31ce1ebd","name":"MCP Resources","style":{"label":true},"nodes":["555a96b221f8e2bf","bb258a21622c01b9","066f6c58bc45d617","70e0b93eb88d63d6","5651b0896965ae6a","6bcaef4ed1ab82ae","3d6e0d7e21b44845","350db73b93a26f8d","fac46d1d55d9ca22","84ebc9b12d948173","9d4edda03c19b553","d3aa7bfa90c9e24e","d6f015cb6debe35b"],"x":274,"y":1739,"w":1002,"h":282},{"id":"555a96b221f8e2bf","type":"mcp-resource","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"","server":"28907ed9ddcdd4b9","resourceUri":"local://books/{genre}","resourceId":"my_books","title":"Books Array","description":"JSON Array of books filtered by genre.","mimeType":"application/json","x":400,"y":1920,"wires":[["70e0b93eb88d63d6"]]},{"id":"bb258a21622c01b9","type":"mcp-response","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"","x":1130,"y":1900,"wires":[]},{"id":"066f6c58bc45d617","type":"json","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"To JSON","property":"payload","action":"str","pretty":false,"x":870,"y":1920,"wires":[["350db73b93a26f8d"]]},{"id":"70e0b93eb88d63d6","type":"template","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"library","field":"library","fieldType":"msg","format":"json","syntax":"mustache","template":"[\n {\n \"author\": \"Harper Lee\",\n \"title\": \"To Kill a Mockingbird\",\n \"genre\": \"Fiction\",\n \"year\": 1960\n },\n {\n \"author\": \"J.K. Rowling\",\n \"title\": \"Harry Potter and the Sorcerer's Stone\",\n \"genre\": \"Fantasy\",\n \"year\": 1997\n },\n {\n \"author\": \"George Orwell\",\n \"title\": \"1984\",\n \"genre\": \"Dystopian\",\n \"year\": 1949\n },\n {\n \"author\": \"Jane Austen\",\n \"title\": \"Pride and Prejudice\",\n \"genre\": \"Romance\",\n \"year\": 1813\n },\n {\n \"author\": \"F. Scott Fitzgerald\",\n \"title\": \"The Great Gatsby\",\n \"genre\": \"Classic\",\n \"year\": 1925\n },\n {\n \"author\": \"Toni Morrison\",\n \"title\": \"Beloved\",\n \"genre\": \"Historical Fiction\",\n \"year\": 1987\n },\n {\n \"author\": \"Stephen King\",\n \"title\": \"The Shining\",\n \"genre\": \"Horror\",\n \"year\": 1977\n },\n {\n \"author\": \"Agatha Christie\",\n \"title\": \"Murder on the Orient Express\",\n \"genre\": \"Mystery\",\n \"year\": 1934\n },\n {\n \"author\": \"Gabriel Garcia Marquez\",\n \"title\": \"One Hundred Years of Solitude\",\n \"genre\": \"Magical Realism\",\n \"year\": 1967\n },\n {\n \"author\": \"Mark Twain\",\n \"title\": \"Adventures of Huckleberry Finn\",\n \"genre\": \"Adventure\",\n \"year\": 1884\n },\n {\n \"author\": \"J.R.R. Tolkien\",\n \"title\": \"The Lord of the Rings\",\n \"genre\": \"Fantasy\",\n \"year\": 1954\n },\n {\n \"author\": \"Ernest Hemingway\",\n \"title\": \"The Old Man and the Sea\",\n \"genre\": \"Literary Fiction\",\n \"year\": 1952\n },\n {\n \"author\": \"Charlotte Bronte\",\n \"title\": \"Jane Eyre\",\n \"genre\": \"Gothic\",\n \"year\": 1847\n },\n {\n \"author\": \"Leo Tolstoy\",\n \"title\": \"War and Peace\",\n \"genre\": \"Historical Fiction\",\n \"year\": 1869\n },\n {\n \"author\": \"Emily Bronte\",\n \"title\": \"Wuthering Heights\",\n \"genre\": \"Gothic Romance\",\n \"year\": 1847\n },\n {\n \"author\": \"Ray Bradbury\",\n \"title\": \"Fahrenheit 451\",\n \"genre\": \"Science Fiction\",\n \"year\": 1953\n },\n {\n \"author\": \"Arthur Conan Doyle\",\n \"title\": \"The Adventures of Sherlock Holmes\",\n \"genre\": \"Mystery\",\n \"year\": 1892\n },\n {\n \"author\": \"Margaret Atwood\",\n \"title\": \"The Handmaid's Tale\",\n \"genre\": \"Dystopian\",\n \"year\": 1985\n },\n {\n \"author\": \"Herman Melville\",\n \"title\": \"Moby Dick\",\n \"genre\": \"Adventure\",\n \"year\": 1851\n },\n {\n \"author\": \"Kazuo Ishiguro\",\n \"title\": \"Never Let Me Go\",\n \"genre\": \"Science Fiction\",\n \"year\": 2005\n }\n]","output":"json","x":580,"y":1920,"wires":[["5651b0896965ae6a"]]},{"id":"5651b0896965ae6a","type":"function","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"filter","func":"const books = msg.library\nconst genre = msg.payload.genre\n\nif (!Array.isArray(books)) {\n throw new Error('Payload is not an array of books')\n}\n\nif (typeof genre !== 'string') {\n throw new Error('Genre must be a string')\n}\n\nfunction looseCompareGenre(genre, search) {\n return genre.toLowerCase().includes(search.toLowerCase())\n}\nconst filteredBooks = books.filter(book => looseCompareGenre(book.genre, genre))\n\nmsg.payload = filteredBooks\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":1920,"wires":[["066f6c58bc45d617"]]},{"id":"6bcaef4ed1ab82ae","type":"catch","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"","scope":"group","uncaught":false,"x":860,"y":1980,"wires":[["350db73b93a26f8d"]]},{"id":"3d6e0d7e21b44845","type":"mcp-resource","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"","server":"28907ed9ddcdd4b9","resourceUri":"db://recipes","resourceId":"recipes","title":"Recipes Array","description":"JSON Array of all recipes","mimeType":"application/json","x":380,"y":1820,"wires":[["84ebc9b12d948173"]]},{"id":"350db73b93a26f8d","type":"junction","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","x":1010,"y":1880,"wires":[["bb258a21622c01b9","fac46d1d55d9ca22"]]},{"id":"fac46d1d55d9ca22","type":"debug","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"resource response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1140,"y":1860,"wires":[]},{"id":"84ebc9b12d948173","type":"template","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"db query for recipes","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n \"recipes\": [\n {\n \"name\": \"Spaghetti Aglio e Olio\",\n \"ingredients\": [\n \"Spaghetti\",\n \"Garlic\",\n \"Olive Oil\",\n \"Red Pepper Flakes\",\n \"Parsley\"\n ],\n \"method\": \"Cook spaghetti, sauté garlic in olive oil, add red pepper flakes, toss with cooked spaghetti, garnish with parsley.\",\n \"wine_paring\": \"Pinot Grigio\"\n },\n {\n \"name\": \"Chicken Alfredo Pasta\",\n \"ingredients\": [\n \"Chicken Breast\",\n \"Fettuccine Pasta\",\n \"Heavy Cream\",\n \"Parmesan Cheese\",\n \"Garlic\",\n \"Butter\"\n ],\n \"method\": \"Cook chicken, cook pasta, make alfredo sauce with cream, parmesan, garlic, and butter, combine all.\",\n \"wine_paring\": \"Chardonnay\"\n },\n {\n \"name\": \"Caprese Salad\",\n \"ingredients\": [\n \"Tomatoes\",\n \"Fresh Mozzarella\",\n \"Basil\",\n \"Olive Oil\",\n \"Balsamic Vinegar\",\n \"Salt\",\n \"Pepper\"\n ],\n \"method\": \"Slice tomatoes and mozzarella, layer with basil, drizzle with olive oil and balsamic vinegar, season with salt and pepper.\",\n \"wine_paring\": \"Chianti\"\n },\n {\n \"name\": \"Beef Tacos\",\n \"ingredients\": [\n \"Ground Beef\",\n \"Taco Seasoning\",\n \"Tortillas\",\n \"Lettuce\",\n \"Tomatoes\",\n \"Cheese\",\n \"Sour Cream\"\n ],\n \"method\": \"Cook beef with taco seasoning, assemble tacos with beef, lettuce, tomatoes, cheese, and sour cream.\",\n \"wine_paring\": null\n },\n {\n \"name\": \"Vegetable Stir Fry\",\n \"ingredients\": [\n \"Mixed Vegetables\",\n \"Soy Sauce\",\n \"Garlic\",\n \"Ginger\",\n \"Sesame Oil\",\n \"Rice\"\n ],\n \"method\": \"Stir fry vegetables with soy sauce, garlic, and ginger, finish with sesame oil, serve over rice.\",\n \"wine_paring\": \"Riesling\"\n },\n {\n \"name\": \"Margherita Pizza\",\n \"ingredients\": [\n \"Pizza Dough\",\n \"Tomato Sauce\",\n \"Fresh Mozzarella\",\n \"Basil\",\n \"Olive Oil\"\n ],\n \"method\": \"Top pizza dough with sauce, mozzarella, and basil, drizzle with olive oil, bake until crust is golden.\",\n \"wine_paring\": \"Merlot\"\n },\n {\n \"name\": \"Grilled Salmon\",\n \"ingredients\": [\n \"Salmon Fillet\",\n \"Lemon\",\n \"Garlic\",\n \"Dill\",\n \"Olive Oil\"\n ],\n \"method\": \"Marinate salmon with lemon, garlic, dill, and olive oil, grill until cooked through.\",\n \"wine_paring\": \"Sauvignon Blanc\"\n },\n {\n \"name\": \"Pasta Primavera\",\n \"ingredients\": [\n \"Pasta\",\n \"Assorted Vegetables\",\n \"Cream Sauce\",\n \"Garlic\",\n \"Parmesan Cheese\"\n ],\n \"method\": \"Cook pasta, sauté vegetables, add cream sauce, garlic, and parmesan, toss with cooked pasta.\",\n \"wine_paring\": \"Chardonnay\"\n },\n {\n \"name\": \"Chicken Caesar Salad\",\n \"ingredients\": [\n \"Chicken Breast\",\n \"Romaine Lettuce\",\n \"Caesar Dressing\",\n \"Croutons\",\n \"Parmesan Cheese\"\n ],\n \"method\": \"Grill chicken, chop lettuce, toss with dressing, croutons, and parmesan, top with grilled chicken.\",\n \"wine_paring\": \"Sauvignon Blanc\"\n },\n {\n \"name\": \"Chocolate Chip Cookies\",\n \"ingredients\": [\n \"Flour\",\n \"Butter\",\n \"Sugar\",\n \"Eggs\",\n \"Chocolate Chips\",\n \"Vanilla Extract\",\n \"Baking Soda\"\n ],\n \"method\": \"Cream butter and sugar, add eggs and vanilla, mix in dry ingredients and chocolate chips, bake until golden.\",\n \"wine_paring\": null\n }\n ]\n}","output":"str","x":630,"y":1820,"wires":[["d6f015cb6debe35b"]]},{"id":"9d4edda03c19b553","type":"comment","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"Status Resource Example","info":"","x":420,"y":1780,"wires":[]},{"id":"d3aa7bfa90c9e24e","type":"comment","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","name":"Dynamic Resource Example","info":"","x":430,"y":1880,"wires":[]},{"id":"d6f015cb6debe35b","type":"junction","z":"e1ceeedf31ce1ebd","g":"00744c2e2a560c36","x":930,"y":1820,"wires":[["350db73b93a26f8d"]]},{"id":"28907ed9ddcdd4b9","type":"mcp-server","name":"My Node-RED MCP Server","protocol":"http","path":"/mcp"},{"id":"3d12514448ee3580","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-mcp-server-nodes":"0.1.1"}}] ``` :: # MCP Response Sends responses back to the MCP client for tools and resources. This node should be the final node in any flow that begins with an MCP Tool or MCP Resource node. #### Flow Requirements Must be connected as the final node in flows starting with MCP Tool or MCP Resource nodes. #### Configuration **Name** `string`:br Optional display name for this node in the flow. #### Input The node accepts `msg.payload` containing the data to return to the MCP client. The format depends on the type of request: **For MCP Tool Responses:** - Can be any data type (string, number, object, array) - Will be returned as the tool execution result **For MCP Resource Responses:** - Should match the MIME type specified in the MCP Resource configuration - For `text/plain`: string content - For `application/json`: object or array - For `text/markdown`: markdown formatted string #### Usage ```text [MCP Tool/Resource] → [Your Processing Nodes] → [MCP Response] ``` The MCP Response node completes the request cycle by sending your processed data back to the AI assistant that made the request. # MCP Tool MCP Tool node allows you to create custom tools that FlowFuse Expert can invoke to perform specific tasks. These tools can do anything a Node-RED flow can do - from querying databases and calling APIs to controlling IoT devices and processing data. The FlowFuse Expert decides when to call your tool based on the description and input schema you provide. ## Flow Requirements MCP Tool nodes must be connected to a flow that ends with an **MCP Response** node to send results back to the MCP client. ## Configuration ### Name `string` - Optional Optional display name for this node in the flow. This helps you identify the node in your Node-RED editor but is not visible to MCP clients. ### Server `mcp-server` - Required The MCP server configuration this tool will be registered with. Select from your configured MCP server instances. ### Tool Name `string` - Required Unique identifier for the tool used by MCP clients to call this tool. Should be written in snake\_case. **Examples:** - `get_weather` - `send_email` - `query_database` - `control_lights` ### Title `string` - Required Human-readable name shown to users in MCP clients. This is what users see when browsing available tools. **Examples:** - "Send Email" - "Control Smart Lights" ### Description `string` - Required Detailed description of what this tool does and when to use it. Be specific to help FlowFuse Expert understand when to invoke this tool. ### Annotations `checkboxes` - Optional Annotations help AI clients understand your tool's behavior and control which FlowFuse team members can access it based on their role (Viewer, Member, or Owner). - **Read-Only Hint**: Tool only reads data, doesn't modify anything. Safe for exploratory queries. - **Access**: Viewer role and above - **Destructive Hint**: Tool may delete or irreversibly modify data. Use with caution. - **Access**: Owner role only - **Idempotent Hint**: Calling the tool multiple times with same parameters has the same effect as calling it once. Safe to retry. - **Access**: No effect on roles (only relevant for writing tools, which require Member minimum) - **Open-World Hint**: Tool interacts with external systems or data sources that may change unpredictably. - **Access**: Member role and above > **Note:** These are hints only and do not enforce behavior. The actual behavior of a tool is determined by your Node-RED flow implementation. Annotations are used by FlowFuse for role-based access control (RBAC) and FlowFuse Expert. They are also part of the standard MCP specification and can be consumed by external agents, but their effect ultimately depends on the client's implementation. ### Input Schema `JSON` - Required JSON schema defining the expected arguments for this tool. This tells the FlowFuse Expert what parameters to provide when calling your tool. ## Input Schema The input schema uses JSON Schema format to define the structure and validation rules for tool arguments. ### Basic Example ```json { "type": "object", "properties": { "name": { "type": "string", "description": "The name to greet", "minLength": 1 } }, "required": ["name"] } ``` ### Complete Example with Multiple Types ```json { "type": "object", "properties": { "location": { "type": "string", "description": "City name or ZIP code", "minLength": 1 }, "units": { "type": "string", "description": "Temperature units", "enum": ["celsius", "fahrenheit"], "default": "celsius" }, "days": { "type": "number", "description": "Number of days to forecast", "minimum": 1, "maximum": 7, "default": 1 }, "include_hourly": { "type": "boolean", "description": "Include hourly breakdown", "default": false } }, "required": ["location"] } ``` ## Output When the tool is called by an MCP client, the output `msg.payload` contains the arguments passed according to your input schema. ### Example If your input schema defines: ```json { "type": "object", "properties": { "city": { "type": "string" }, "units": { "type": "string" } } } ``` The FlowFuse Expert calls your tool with: ```json { "city": "London", "units": "celsius" } ``` You can then use these values in subsequent nodes to perform your tool's logic. ## Example Flow ::render-flow ```json [{"id":"4076896ebd9fb8b4","type":"group","z":"e1ceeedf31ce1ebd","name":"MCP Tools","style":{"label":true},"nodes":["670d69227fd02715","d473b3e38011a7d1","8bb8104fab25a772","b4398fb68fb3d363","561f103a1ac605c4","d31cc0acdb813863","3c23f963f4982c1a","644e76704eb56f41","30ead11f52f1bf19","8b5faff4ad12d406","079eacf59ffc7032"],"x":254,"y":1399,"w":892,"h":282},{"id":"670d69227fd02715","type":"mcp-tool","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","server":"28907ed9ddcdd4b9","toolName":"greeting","title":"","description":"Greet person by name","inputSchema":"{\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"The name to greet\",\n \"minLength\": 1\n }\n },\n \"required\": [\"name\"]\n}","x":340,"y":1480,"wires":[["8bb8104fab25a772"]]},{"id":"d473b3e38011a7d1","type":"change","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"\"This response is defined in a Node-RED change node. Hi \" & payload.name","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":1480,"wires":[["079eacf59ffc7032"]]},{"id":"8bb8104fab25a772","type":"delay","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":540,"y":1480,"wires":[["d473b3e38011a7d1"]]},{"id":"b4398fb68fb3d363","type":"mcp-tool","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","server":"28907ed9ddcdd4b9","toolName":"get_iss_position","title":"Get ISS Position","description":"Retrieves the latitude and longitude of the Internanational Space Station","inputSchema":"{}","x":360,"y":1580,"wires":[["561f103a1ac605c4"]]},{"id":"561f103a1ac605c4","type":"http request","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://api.open-notify.org/iss-now.json","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":730,"y":1580,"wires":[["079eacf59ffc7032"]]},{"id":"d31cc0acdb813863","type":"catch","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","scope":"group","uncaught":false,"x":730,"y":1640,"wires":[["079eacf59ffc7032"]]},{"id":"3c23f963f4982c1a","type":"debug","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"tool response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":1520,"wires":[]},{"id":"644e76704eb56f41","type":"mcp-response","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"","x":1020,"y":1560,"wires":[]},{"id":"30ead11f52f1bf19","type":"comment","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"Simple Greeting Tool","info":"","x":370,"y":1440,"wires":[]},{"id":"8b5faff4ad12d406","type":"comment","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","name":"Get IIS Position Tool","info":"","x":370,"y":1540,"wires":[]},{"id":"079eacf59ffc7032","type":"junction","z":"e1ceeedf31ce1ebd","g":"4076896ebd9fb8b4","x":880,"y":1540,"wires":[["644e76704eb56f41","3c23f963f4982c1a"]]},{"id":"28907ed9ddcdd4b9","type":"mcp-server","name":"My Node-RED MCP Server","protocol":"http","path":"/mcp"},{"id":"c05fb032a62e8357","type":"global-config","env":[],"modules":{"@flowfuse-nodes/nr-mcp-server-nodes":"0.1.1"}}] ``` :: # MQTT Nodes This document lists and explains the **MQTT nodes** available in FlowFuse. These nodes are enhanced versions of the standard **MQTT In** and **MQTT Out** nodes in Node-RED, designed for FlowFuse users. They are tightly integrated with the [FlowFuse MQTT Broker Service](https://flowfuse.com/docs/user/teambroker/), a built-in, team-scoped broker managed directly by the FlowFuse platform. When an MQTT node is added to the canvas, the MQTT Broker Client is automatically created and configured. This ensures secure, easy communication without requiring any external broker setup or credentials. ## Nodes This section lists the document of **MQTT nodes** available in FlowFuse: - [MQTT In](https://flowfuse.com/docs/flowfuse-nodes/mqtt/mqtt-in/): Enhanced MQTT In node for FlowFuse with automatic broker setup, dynamic subscriptions, wildcard topic support, and full MQTT v5 compatibility. - [MQTT Out](https://flowfuse.com/docs/flowfuse-nodes/mqtt/mqtt-out/): Enhanced MQTT Out node for FlowFuse with automatic broker setup, dynamic topic control, and full MQTT v5 support. Each node extends standard MQTT functionality with automatic configuration and full MQTT v5 support, making it easier to build reliable and real-time communication flows inside FlowFuse. # MQTT In This is an enhanced version of the standard MQTT In node, designed exclusively for FlowFuse users. The node features automatic configuration upon deployment. The [MQTT broker client](https://flowfuse.com/docs/user/teambroker/) is created automatically alongside the node configuration when added to the canvas. ## Configuration Options ### Server Configuration The server is automatically configured and managed by the FlowFuse platform. All FlowFuse MQTT nodes within an instance share a single broker connection, ensuring efficient resource utilization and consistent connection management across all flows. Access control can be managed through the broker client management interface, where permissions for subscribe and publish operations can be configured. > **Note:** When the first node is added to the canvas, a new **Team Broker User** linked to the FlowFuse instance is automatically created. By default, this user has **subscribe-only** permissions. ### Subscription Mode The node supports two operational modes: - **Single topic mode**: Allows subscription to a fixed topic configured directly in the node settings - **Dynamic subscription mode**: Enables runtime control of subscriptions through input messages ### Topic Configuration When operating in single topic mode, specify the MQTT topic to subscribe to in the node configuration. The topic field supports MQTT wildcard patterns for flexible message routing. ### Quality of Service Select from three QoS levels: - **Level 0**: Fire-and-forget delivery - **Level 1**: At-least-once delivery - **Level 2**: Exactly-once delivery (default) ### Output Format The node can automatically detect the message format or convert it to a specific type including: - Buffer - String - Parsed JSON object - Base64 encoded string ## Message Output Properties Each received message includes the following properties: - `msg.payload`: The message content as string or buffer - `msg.topic`: The MQTT topic from which the message was received - `msg.qos`: The quality of service level (0, 1, or 2) - `msg.retain`: Boolean indicating whether the message was retained on the broker - `msg.responseTopic`: MQTT version 5 response topic for request-response patterns - `msg.correlationData`: MQTT version 5 correlation data for message tracking - `msg.contentType`: MQTT version 5 content type descriptor - `msg.userProperties`: MQTT version 5 custom user properties - `msg.messageExpiryInterval`: MQTT version 5 message expiry time in seconds ## Topic Wildcard Patterns MQTT supports two wildcard characters for flexible topic matching: - **Plus sign (`+`)**: Single-level wildcard - **Hash symbol (`#`)**: Multi-level wildcard > *Note:*\* The `#` wildcard can only be used at the end of a topic when defining subscriptions. ### Example Patterns - `sensors/+/temperature` - Receives temperature readings from all sensor locations - `factory/#` - Receives all messages published under the factory topic hierarchy - `devices/+/status` - Receives status updates from all devices - `building/floor1/#` - Receives all messages from floor 1 of the building ## Version Support This node fully supports MQTT version 5 features including response topics, correlation data, content types, user properties, message expiry intervals, and topic aliases. It maintains backward compatibility with earlier MQTT versions. # MQTT Out This is an enhanced version of the standard MQTT Out node, designed exclusively for FlowFuse users. The node features automatic configuration upon deployment when using within Flowfuse instance. The [MQTT broker](https://flowfuse.com/docs/user/teambroker/) client is created automatically alongside the node configuration when added to the canvas. ## Configuration Options ### Server Configuration The server is automatically configured and managed by the FlowFuse platform. All FlowFuse MQTT nodes within an instance share a single broker connection, ensuring efficient resource utilization and consistent connection management across all flows. Access control can be managed through the broker client management interface, where permissions for subscribe and publish operations can be configured. > **Note:** When the first node is added to the canvas, a new **Team Broker User** linked to the FlowFuse instance is automatically created. By default, this user has **subscribe-only** permissions. ### Topic Configuration Specify the default MQTT topic for message publication. This can be overridden at runtime by setting the `msg.topic` property in the input message. ### Quality of Service Select the QoS level for published messages: - **Level 0**: Fire and forget - **Level 1**: At least once - **Level 2**: Exactly once (default) ### Retain Flag Configure whether messages should be retained on the broker. The default value is `false`. Retained messages are delivered to new subscribers immediately upon subscription. ## Message Input Properties The following properties control message publication: - `msg.payload`: The message content to publish. JavaScript objects are automatically converted to JSON strings, while buffers are transmitted as binary data - `msg.topic`: Overrides the configured topic for this message - `msg.qos`: Overrides the configured quality of service level - `msg.retain`: Overrides the configured retain flag - `msg.responseTopic`: MQTT version 5 response topic for request-response patterns - `msg.correlationData`: MQTT version 5 correlation data for message tracking - `msg.contentType`: MQTT version 5 content type descriptor - `msg.userProperties`: MQTT version 5 custom user properties - `msg.messageExpiryInterval`: MQTT version 5 message expiry time in seconds - `msg.topicAlias`: MQTT version 5 topic alias for bandwidth optimization ## Publishing Messages ### Basic Publishing Send a message to the configured topic: ```javascript msg.payload = "Hello, MQTT!"; return msg; ``` ### Publishing to a Different Topic Override the configured topic: ```javascript msg.topic = "sensors/temperature"; msg.payload = 25.5; return msg; ``` ### Publishing with Custom QoS Override the quality of service level: ```javascript msg.topic = "critical/alert"; msg.payload = "System warning"; msg.qos = 2; // Exactly once delivery return msg; ``` ### Publishing Retained Messages Publish a message that will be retained on the broker: ```javascript msg.topic = "sensors/last-known/temperature"; msg.payload = 25.5; msg.retain = true; return msg; ``` ### Publishing with MQTT v5 Properties Use MQTT version 5 features: ```javascript msg.topic = "sensors/temperature"; msg.payload = { value: 25.5, unit: "celsius" }; msg.contentType = "application/json"; msg.messageExpiryInterval = 60; // Message expires in 60 seconds msg.userProperties = { sensorId: "sensor-001", location: "warehouse" }; return msg; ``` ## Version Support This node fully supports MQTT version 5 features including response topics, correlation data, content types, user properties, message expiry intervals, and topic aliases. It maintains backward compatibility with earlier MQTT versions. # ctrlX Device Agent App **Currently not available - will be available soon** ## Installation Procedure 1. In the ctrlX CORE web interface, navigate to the window **Settings** ➔ **Apps**. 2. Switch the ctrlX device to the **Service** mode. 3. In the app overview, navigate to the category **Available apps**. This category displays all apps saved in the app storage on the ctrlX device and all apps provided via the ctrlX Store. 4. Search for the FlowFuse Device Agent ctrlX App to be installed and click on the Installation button. If multiple app versions are provided for installation, a list of available versions and app sources will be displayed. In this case, select the desired app version from the list to start the installation. If only one app version is provided, the installation will start directly. After the installation, the app will be shown in the app overview, under the category **Installed apps**. 5. Switch the ctrlX device back to the **Operating** mode. ## Device Agent Configuration for ctrlX 1. After successful installation, [generate and download the "Device Credentials" in FlowFuse](https://flowfuse.com/docs/device-agent/register/#manual-setup) 2. In the ctrlX CORE web interface, navigate to the window **Settings** ➔ **Apps** ➔ **Manage App Data** 3. Select the folder "FlowFuse Device Agent" 4. Click on upload file and select the **device.yml** configuration from step 1 5. The Device Agent connects automatically to your FlowFuse instance. Ensure your ctrlX has a network connection to your FlowFuse platform (e.g. FlowFuse Cloud) and to the npmjs registry. # ctrlX Node-RED App Learn more about the Node-RED App in the [ctrlX World Store](https://developer.community.boschrexroth.com/t5/Store-and-How-to/FlowFuse-Node-RED/ba-p/82135){rel=""nofollow""} ## Installation Procedure Follow these steps to install the Rexroth CtrlX App by FlowFuse on your ctrlX device: 1. In the ctrlX CORE web interface, navigate to the window **Settings** ➔ **Apps**. 2. Switch the ctrlX device to the **Service** mode. 3. In the app overview, navigate to the category **Available apps**. This category displays all apps saved in the app storage on the ctrlX device and all apps provided via the ctrlX Store. 4. Search for the `Rexroth CtrlX App` to be installed and click on the Install button. If multiple app versions are provided for installation, a list of available versions and app sources will be displayed. In this case, select the desired app version from the list to start the installation. If only one app version is provided, the installation will start directly. After the installation, the app will be shown in the app overview, under the category **Installed apps**. 5. Switch the ctrlX device back to the **Operating** mode. ## Start and Login 1. Open the Flow Editor from the Node-RED menu in your ctrlX. 2. **Log in using your ctrlX username and password**. 3. A successful login requires a valid license and user permission. 4. Create your Node-RED flow. # Device Agent - Hardware The FlowFuse Device Agent can be installed on any Mac, Linux, or Windows-supported device. If you want to learn more about how to install the Device Agent in general, [click here](https://flowfuse.com/docs/device-agent/install/overview). Nevertheless, this overview provides a guide for specific hardware platforms. ## Official FlowFuse Hardware Partners [![](https://upload.wikimedia.org/wikipedia/commons/0/0d/Logo_of_Bosch_Rexroth_AG.svg){height="75" width="150"}](https://developer.community.boschrexroth.com/t5/Store-and-How-to/FlowFuse-Node-RED/ba-p/82135) - [ctrlX - Node-RED App](https://flowfuse.com/docs/hardware/ctrlx-node-red) - [ctrlX - Device Agent App](https://flowfuse.com/docs/hardware/ctrlx-device-agent) - will be available soon ## Other Hardware Guides [![](https://upload.wikimedia.org/wikipedia/de/c/cb/Raspberry_Pi_Logo.svg){height="30" width="80"}](https://flowfuse.com/docs/hardware/raspbian) - [Raspberry Pi](https://flowfuse.com/docs/hardware/raspbian) # Raspberry Pi The Raspberry Pi section provides instructions for installing and setting up the FlowFuse Device Agent on your Raspberry Pi device, enabling seamless integration for efficient device management and automation. ## Installing the Device Agent FlowFuse provides a script to install Node.JS, npm, and the FlowFuse Device Agent onto a Raspberry Pi. This script won't work on ARMv6 builds as the standard Node.JS builds don't support it, as result Pi Zero's are not supported. ```sh bash <(curl -sL https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/raspbian-install-device-agent.sh) ``` **This script will:** 1. Detect if Node.js is already installed, it will ensure it is at least v14. If less than v14 it will stop. If nothing is found it will install the Node.js 18 LTS release 2. Install the latest version of the FlowFuse Device Agent using npm. 3. Setup the FlowFuse Device Agent to run as a service ## Running as a service You can run the device agent as a service, which means it can run in the background and be enabled to automatically start on boot. The install script will automatically set up the FlowFuse Device Agent to run as a service. The following commands can be useful for controlling the service or changing the default service settings. ### Starting the service on boot (optional) If you want Node-RED to run when the device is turned on, or re-booted, you can enable the service to autostart by running the command: `sudo systemctl enable flowfuse-device-agent.service` To disable the service, run the command: `sudo systemctl disable flowfuse-device-agent.service` ### Controlling the service You can start the service with the command: `sudo systemctl start flowfuse-device-agent` You can check the current status with the command: `sudo systemctl status flowfuse-device-agent` You can stop your with the command: `sudo systemctl stop flowfuse-device-agent` # FlowFuse Documentation Welcome to the documentation for FlowFuse, an open-source, industrial data platform that enables engineers to build, manage, scale, and secure their Node-RED solutions. It covers everything from setup, to usage, and development. All [contributions](https://flowfuse.com/docs/contribute/introduction/) are welcome. ## Getting Started ::div{.ff-offering-tiles.grid-cols-1.sm:grid-cols-2} :::div{.ff-tile.ff-offering-tile} FlowFuse Self-Hosted Run FlowFuse yourself on your own infrastructure. - [Quick Start Instructions :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/quick-start/) - [Install FlowFuse Self-Hosted :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/install/introduction/) - [Upgrade Your FlowFuse Instance :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/upgrade) - [Unlock Enterprise Features :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/upgrade/open-source-to-premium/) - [Administering FlowFuse :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/admin/introduction/) ::: :::div{.ff-tile.ff-offering-tile} FlowFuse Cloud Hosted solution, nothing to install anything, jump straight in. - [Sign Up for Free :icon-chevron-right{.ff-icon.ff-icon-sm}](https://app.flowfuse.com/account/create) - [Upgrading Teams :icon-chevron-right{.ff-icon.ff-icon-sm}]() - [Billing :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/cloud/billing/) - [Single Sign On :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/cloud/introduction/#single-sign-on) ::: :: ## Using FlowFuse ### Getting Started Here are some quick reference links to our most popular topics. You can also view the full documentation available for FlowFuse in our [Getting Started](https://flowfuse.com/docs/user/introduction/){rel=""nofollow""} guide. ::div{.ff-product-feature-tiles.grid-cols-1.md:grid-cols-2} :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/instance-settings/){.ff-tile.ff-product-feature-tile} ::: :::div [Configuring Instances [Understand the options available for your Node-RED Instances]](https://flowfuse.com/docs/user/instance-settings/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/snapshots/){.ff-tile.ff-product-feature-tile} ::: :::div [Snapshots [Version Control for your Node-RED Instances.]](https://flowfuse.com/docs/user/snapshots/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/shared-library/){.ff-tile.ff-product-feature-tile} ::: :::div [Team Library [Centralized management of re-usable flows and functions.]](https://flowfuse.com/docs/user/shared-library/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/snapshots/){.ff-tile.ff-product-feature-tile} ::: :::div [Pipelines [Deploy flows between Test, Staging & Production Environments.]](https://flowfuse.com/docs/user/snapshots/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :::div [Remote Instances [Deploy flows remotely with FlowFuse "Devices".]](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :::div [Device Groups [Manage large numbers of devices together.]](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :: ### Advanced Features Once you're more comfortable with FlowFuse, you may want to explore some of our more advanced features that will help elevate your Node-RED experience even further. ::div{.ff-product-feature-tiles.grid-cols-1.md:grid-cols-2} :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/team/){.ff-tile.ff-product-feature-tile} ::: :::div [Managing Teams [Host your Instances at a custom subdomain]](https://flowfuse.com/docs/user/team/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/custom-hostnames/){.ff-tile.ff-product-feature-tile} ::: :::div [Custom Domains [Host your Instances at a custom subdomain]](https://flowfuse.com/docs/user/custom-hostnames/){.ff-tile.ff-product-feature-tile} ::: :::div{.ff-product-feature-tile-decorator} [](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :::div [High Availability [Automatically distribute workload across multiple copies of your Node-RED Instance.]](https://flowfuse.com/docs/user/device-groups/){.ff-tile.ff-product-feature-tile} ::: :: ## FlowFuse Extras ::div{.ff-offering-tiles.grid-cols-1.sm:grid-cols-2.lg:grid-cols-2} :::div{.ff-tile.ff-offering-tile} FlowFuse Node-RED Nodes A complete set of FlowFuse-maintained nodes for data sharing between instances, MQTT messaging, AI/ONNX models, MCP agent integrations, and enterprise data features. - [FlowFuse Project Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/user/projectnodes/) - [FlowFuse MCP Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/node-red/flowfuse/mcp/) - [FlowFuse Tables Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/node-red/flowfuse/flowfuse-tables/) - [FlowFuse AI Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/node-red/flowfuse/ai/) - [FlowFuse MQTT Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/user/mqtt-nodes/) - [See all the Nodes :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/node-red/flowfuse/) ::: :::div{.ff-tile.ff-offering-tile} FlowFuse Dashboard Create interactive, responsive and secure Dashboards in Node-RED. - [Install FlowFuse Dashboard :icon-chevron-right{.ff-icon.ff-icon-sm}](https://dashboard.flowfuse.com) - [Build Your First Dashboard :icon-chevron-right{.ff-icon.ff-icon-sm}](https://dashboard.flowfuse.com) - [Multi Tenant Dashboards :icon-chevron-right{.ff-icon.ff-icon-sm}](https://dashboard.flowfuse.com/user/multi-tenancy.html) ::: :::div{.ff-tile.ff-offering-tile} FlowFuse Device Agent Manage thousands of Node-RED Instances remotely with the FlowFuse Device Agent. - [Install the Device Agent :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/device-agent/install/) - [Registering Devices in FlowFuse :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/device-agent/register/) - [Deploying Flows to your Device :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/device-agent/deploy/) - [Editing Flows on your Device :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/device-agent/deploy/) ::: :::div{.ff-tile.ff-offering-tile} FlowFuse Assistant AI in the Node-RED Editor to help build your flows. - [Getting Started Guide :icon-chevron-right{.ff-icon.ff-icon-sm}](https://flowfuse.com/docs/user/expert/) ::: :: ## Support - [Troubleshooting](https://flowfuse.com/docs/debugging/) - [FlowFuse Cloud Support](https://flowfuse.com/docs/premium-support/) ## Contributing to FlowFuse - [Useful Information](https://flowfuse.com/docs/contribute/introduction#contributing-to-flowfuse) - Learn the foundational concepts of how FlowFuse is built & structured. - [Development Setup](https://flowfuse.com/docs/contribute/introduction#development-setup) - Configure your local development environment to contribute to FlowFuse. - [Testing](https://flowfuse.com/docs/contribute/introduction#testing) - Understand our testing philosophy at FlowFuse. # Configuring FlowFuse The base configuration of the FlowFuse platform is provided in the file `/opt/flowforge/etc/flowforge.yml`. This assumes the default install location of `/opt/flowforge`. To run a local install, you can use the default options. This section describes the options available in the configuration file. ## Server configuration | Option | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | The address to serve the web ui on. This defaults to `localhost` which means the ui will only be available when browsing from the same server that is running the platform. To make it accessible to other devices on the network, set it to `0.0.0.0`. :br NOTE: If `host` is changed, please also update `base_url` to match e.g. `http://[ip-address-of-host]:3000` | | `port` | The TCP port the platform serves its web ui. Default: `3000` | | `base_url` | The url to access the platform. This defaults to `http://localhost:3000` which means a number of internally generated URLs will only work when browsing on the same device as is running the platform. To be able to access the platform remotely, replace `localhost` with the ip address of the device running FlowFuse. | | `domain` | The domain that instance names will be prepended to on Docker & Kubernetes platforms to create a hostname to access the instance. A wildcard DNS A record should point be configured to point to the FlowFuse entry IP Address. | | `support_contact` | a URL or string with contact details for the administrator e.g `mailto:support@example.com` or `https://support.example.com` . Defaults to the email address of the first admin user or `the administrator` if no email address set. | | `create_admin` | If set to `true` will create a default admin user on first run, the username/password is written to the logs. Default: `false` | | `create_admin_access_token` | If set to `true` an access token (ffpat) is created for the default admin user on first run. Its value is written to the logs. Default: `false` | | `license` | Can be used to pass in a license key for FlowFuse. Default not set | NOTE: Changing the `base_url` and `domain` after Node-RED instances have been created is possible, but the original hostname and domain must remain active in order to access the instances and for an them to be able to access the FlowFuse resources. An example workflow would be: 1. Register new domain 2. Set up DNS entries for: - A record for the forge app - wildcard A record for the domain 3. Leave the existing entries for the old domain in place 4. Stop the forge app 5. Edit the flowforge.yml to set the base\_url and domain entries 6. Restart the forge app ## Database configuration FlowFuse supports `sqlite` and `postgres` databases. | Option | Description | | --------- | ----------------------------------------------- | | `db.type` | The type of database to use. Default: `sqlite`. | ### SQLite configuration | Option | Description | | ------------ | ------------------------------------------------------------------------------------------------ | | `db.storage` | Path to the SQLite Database file to use, relative to `/opt/flowforge/var/`. Default: `forge.db`. | ### Postgres configuration | Option | Description | | ------------- | ------------------------------------------------------- | | `db.host` | Hostname of the Postgres Database. Default: `postgres`. | | `db.database` | Database name on Postgres Server. Default: `flowforge`. | | `db.user` | Username used when connecting to Postgres Server. | | `db.password` | Password used when connecting to Postgres Server. | | `db.ssl` | Client should connect with SSL/TLS. Default: `false` | ## Node-RED Driver configuration This configures how Node-RED instances are run by the platform. | Option | Description | | ------------- | ------------------------------------------------------- | | `driver.type` | The type of deployment model to use. Default: `localfs` | ### Localfs Driver options | Option | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `driver.options.start_port` | The port number to start assigning to Node-RED instances as they are created. Default: `12080` | | `driver.options.node_path` | The path to find the node.js executable - useful if Node.js has been installed with `nvm` so isn't necessarily on the system path. | | `driver.options.logPassthrough` | Prints the Node-RED logs in JSON format to stdout of the nr-launcher process. Default: `false` | | `driver.options.privateCA` | The fully qualified path to a pem file containing locally trusted CA cert chain. Default: not set | ### Docker Driver options | Option | Description | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `driver.options.socket` | The path to the Docker control unix domain socket. Default `/var/run/docker.sock` | | `driver.options.logPassthrough` | Prints the Node-RED logs in JSON format to stdout of the Instance containers. Default: `false` | | `driver.options.privateCA` | The fully qualified path to a pem file on the host machine containing locally trusted CA cert chain. Default: not set | ### Kubernetes Driver options | Option | Description | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `driver.options.namespace` | The namespace to run Node-RED instances in. Default: `flowforge` | | `driver.options.cloudProvider` | Enables specific options for certain platforms e.g. `aws`. Default: not set | | `driver.options.projectSelector` | A YAML object containing node annotations to use to filter which nodes Node-RED instances run on. Default: `role: projects` | | `driver.options.logPassthrough` | Prints the Node-RED logs in JSON format to stdout of the instance pods. This should be set with the `forge.logPassthrough=true` Helm chart value. Default: `false` | | `driver.options.privateCA` | The name of a ConfigMap containing a file called `certs.pem` which holds locally trusted CA cert chain. Default: not set | | `driver.options.customHostname.enabled` | Enables the custom hostname feature. Default: `false` | | `driver.options.customHostname.cnameTarget` | The hostname users should configure their DNS entries to point at. This value is required to enable this feature. Default: not set | | `driver.options.customHostname.ingressClass` | The name of the Ingress Class that should be used for the custom hostname. Default: not set | | `driver.options.customHostname.certManagerIssuer` | The name of the CertManager ClusterIssuer to provision HTTPS certificates for custom hostnames. Default: not set | ## MQTT Broker configuration By default, the platform runs without an MQTT broker. This restricts some features in the platform, such as the Project Nodes, Device Actions and Remote Device Editing. If a broker has been setup in the platform, the following configuration is required: | Option | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `broker.url` | The url for the platform to access the broker. For example: `mqtt://localhost:4800`. | | `broker.public_url` | The url used by devices to connect to the broker, if different to `broker.url`. For example, this may require devices to use WebSockets instead: `ws://localhost:4881`. | ## Email configuration By default, email is disabled. This restricts some features in the platform around inviting new users to join. | Option | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------- | | `email.enabled` | Enables the email sending functionality of the platform. Default: `false` | | `email.from` | Sets the address email will appear from. Default: `"FlowFuse" ` | | `email.smtp.host` | Hostname of the SMTP server to send email through. Default: `localhost` | | `email.smtp.port` | Port of the SMTP server to send email through. Default: `587` if `secure` is `false`, `465` otherwise | | `email.smtp.secure` | Whether to use TLS to connect to the SMTP server. Default: `false` | | `email.smtp.auth.user` | Username to authenticate the connection with. Default: `unset` | | `email.smtp.auth.pass` | Password to authenticate the connection with. Default: `unset` | | `email.debug` | If set to true, it will log the full content of emails it tries to send. Default: `false` | See [here](https://flowfuse.com/docs/install/email-providers) for example configuration with common email providers. ### AWS SES Email There is also support for using AWS SES email, this is mainly intended to be used when deployed on AWS EKS. This assumes that the instance is running with a Service Account that has a AWS Role with SES access enabled. | Option | Description | | --------------------- | ------------------------------------------------------------------------------------------------- | | `email.ses.region` | The AWS region to connect to. Default `unset` | | `email.ses.sourceArn` | The AWS ARN of a SES Identity to send email as. Default: `unset` | | `email.ses.fromArn` | The AWS ARN of a SES Identity to set as the from field. Default to value of `email.ses.sourceArn` | ## Telemetry configuration By default, the platform will send anonymous usage information back to us at FlowForge Inc. This can be disabled via the Admin Settings in the UI, or turned off in the configuration file with the `telemetry.enabled` option. **IMPORTANT: Licensed installations cannot disable telemetry** Additionally, you can configure your own instance of FlowFuse to report back to you on how users are using your instance of FlowFuse. FlowFuse supports integration with two different services: - [PostHog](https://posthog.com/){rel=""nofollow""} *(recommended)*: You will require your own API key to pass into the `yml`, which will begin the logging of user interactions. - [Plausible](https://plausible.io/){rel=""nofollow""}: *(deprecated since 0.9 and will be removed in the future)*: You can setup your own account, and pass the relevant domain to the `yml` in the telemetry configuration For more information about this feature, see [here](https://flowfuse.com/docs/admin/telemetry) | Option | Description | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `telemetry.enabled` | Enables the anonymous usage telemetry of the platform. Default: `true` | | `telemetry.frontend.posthog.apiurl` | The API URL for PostHog, either '{rel=""nofollow""}' or '{rel=""nofollow""}'. Default: `https://app.posthog.com` | | `telemetry.frontend.posthog.apikey` | The API key provided to you from your own PostHog account. Default: `null` | | `telemetry.frontend.posthog.capture_pageview` | FlowFuse is designed as to provide custom posthog `$pageview` events that provide more detail on navigation than the default, and suit a single page application better. As such, we recommend setting this to false in order to prevent duplicate `pageleave`/`pageview` events firing. Default: `true` | | `telemetry.frontend.google.tag` | A Google Analytics (gtag) measurement ID. Default: `null` | | `telemetry.frontend.google.events` | An object with keys matching the names of tag events to be enabled and any payload values. Default `null` | | `telemetry.frontend.google.gtm` | A Google Tag Manager container ID (e.g. `GTM-XXXXXXX`). Loads GTM once cookie consent is accepted. Default: `null` | ## Rate Limiting configuration By default, rate limiting is disabled and the platform will not rate limit any requests. To enable rate limiting, you can set the `rate_limits.enabled` option to `true`. When enabled, all routes will be limited to 1000 requests per 1 minute window. These defaults can be adjusted by setting values in the configuration options listed below. | Option | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `rate_limits.enabled` | Enables rate limiting. Default: `false` | | `rate_limits.global` | Enables rate limiting for all routes. Default: `true` (defaults to all routes being rate limited) | | `rate_limits.timeWindow` | The time window in which requests are counted. Default: `60000` (1 minute) | | `rate_limits.max` | The maximum number of requests allowed in the time window. Default: `1000` | | `rate_limits.maxAnonymous` | The maximum number of requests allowed in the time window for anonymous users. Default: not configured (defaults to `rate_limits.max`) | For additional options, see [fastify-rate-limit](https://github.com/fastify/fastify-rate-limit#options){rel=""nofollow""} documentation. ## Session timeouts Allows control of the maximum user session life. | Option | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `sessions.maxDuration` | The maximum number of seconds a user session can last. Default: `604800` (1 week) | | `sessions.maxIdleDuration` | The maximum number of seconds a session can be idle. Must be less than `sessions.maxDuration`. Default: `115200` (32 hours) | ## Support configuration It is possible to add a [HubSpot Support Widget](https://knowledge.hubspot.com/chatflows/create-a-live-chat){rel=""nofollow""} into FlowFuse. This will appear as a floating chat bubble on the bottom-right corner of the screen. To enable this, you'll need to provide the | Option | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `support.enabled` | Enables the chat support widget in the UI. Default: `false` | | `support.frontend.hubspot.trackingcode` | The numerical identifier within your [HubSpot Tracking Code](https://knowledge.hubspot.com/conversations/chat-widget-is-not-appearing-on-your-pages){rel=""nofollow""}. Default: `null` | ## MQTT Broker configuration The platform uses an MQTT broker to provide real-time messaging between devices, Node-RED instances and the platform. The broker shipped with the platform's Docker Compose and Kubernetes installations is [EMQX](https://www.emqx.io/){rel=""nofollow""}: the Docker Compose installation includes it by default, and the Helm chart deploys it when the broker is enabled (`forge.broker.enabled`), which requires the [EMQX Operator](https://docs.emqx.com/en/emqx-operator/latest/getting-started/getting-started.html#install-emqx-operator){rel=""nofollow""} to be installed on the cluster. This is currently an *optional* component - the platform will work without the broker, but some features will not be available: - Without a broker: Project Nodes, Device Actions and Remote Device Editing are unavailable. - The following features additionally require the platform broker to be EMQX: the [Team Broker](https://flowfuse.com/docs/user/teambroker), [FlowFuse Expert](https://flowfuse.com/docs/user/expert/) and live device log and performance views in the platform UI. [Mosquitto](https://mosquitto.org/){rel=""nofollow""} is supported at a legacy level for existing installations that manage their own broker: core platform messaging works, but the EMQX-dependent features listed above are unavailable. Replacing the platform broker with a different customer-supplied broker is not supported. | Option | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `broker.url` | The full url to the platform broker. This is used by the platform and Node-RED instances to connect to the broker. For example: `mqtt://localhost:1883`. | | `broker.public_url` | If set, this is the url provided to Devices to connect to the broker with. When running in a Docker or K8S environment, this url should be the externally addressable url the broker is provided on. This could be via WebSockets, for example: `ws://example.com:1884` | ## AI Configuration | Option | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ai.enabled` | Enables all AI features on the platform (still requires an Enterprise License). When set to `false`, all AI functionality is disabled regardless of individual feature configuration (assistant, expert, inline completions, snapshot descriptions). Default: `true` | ## FF Tables | Option | Description | | ----------------------- | -------------------------------------------------------------------------------------------------- | | `tables.enables` | Enables the FlowFuse Tables feature. Default: `false` | | `tables.driver.type` | Selects the FF Tables driver to use (`postgres-localfs` or `postgres-supavisor`). Default: not set | | `tables.driver.options` | Configuration options for the driver. Details depend on the driver used. Default: not set | The options for the `postgres-localfs` driver require connection details for an admin user on a Postgres instance e.g. ```text database: user: root password: password host: localhost port: 5432 database: postgres ssl: false ``` ## Logging configuration By default the forge app is set to `info` level logging, with the HTTP routes logged at `warn` | Option | Description | | ---------------- | ------------------------------------------------------------------------------ | | `logging.level` | Change the default logging level. Default: `info` | | `logging.http` | Change the default HTTP route logging level. Default: `warn` | | `logging.pretty` | Enable/Disable pretty-printing of the log output. Default: `false` - see below | Setting `logging.http` to `info` will log every HTTP request and response details. The `pretty` option controls the formatting of the log output. When running in developer mode, (for example, if `NODE_ENV` is set to `developer`), then pretty formatting is enabled by default. This makes the logs more human-readable. Otherwise, the log output is JSON formatted for consumption by other tools. ## File storage FlowFuse includes a service that can be used by Node-RED instances to read and write files in their flows as well as providing persistent storage for flow context information. Details of configuring the File Storage service are available [here](https://flowfuse.com/docs/install/file-storage). The main `flowforge.yml` file needs to contain the following properties so it can access the File server. | Option | Description | | --------------- | ------------------------------------------------------------ | | `fileStore.url` | The URL of the FlowFuse File Server to use. Default: not set | ## Enabling Persistent File Storage - File Nodes These nodes are enabled by default on the FlowFuse Cloud platform. If you're running a self-hosted environment you should follow the next steps. FlowFuse file nodes replace the core Node-RED file nodes. To make use of these nodes, the FlowFuse platform Administrator must ensure the core file nodes are not loaded. This is done by adding `10-file.js` in the **Exclude nodes by filename** section of your instance settings under the **Palette** section. This setting is modifiable only by a Team owner and only if it has not been locked in the [template](https://flowfuse.com/docs/user/concepts#template) by the platform Administrator. [Click here](https://flowfuse.com/docs/user/filenodes), to learn more about the usage of the FlowFuse File Nodes. ## Content Security Policy | Option | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `content_security_policy.enabled` | Enabled `Content-Security-Policy` headers. Default: `false` | | `content_security_policy.directives` | Overrides the default set of directives, supplied as a JSON object defined by HelmetJS [here](https://helmetjs.github.io/#content-security-policy){rel=""nofollow""} | | `content_security_policy.report_only` | Enables reporting only mode. Default: `false` | | `content_security_policy.report_uri` | Provides at URI for reporting to be sent to if enabled | ## NPM Registry These settings enable per team Node-RED private catalogue generation | Option | Description | | ---------------------------- | ----------------------------------------------------- | | `npmRegistry.enabled` | Enables NPM Registry support. Default: `false` | | `npmRegistry.url` | The URL for the Verdaccio NPM Registry. Default: none | | `npmRegistry.admin.username` | Username for Verdaccio admin user | | `npmRegistry.admin.password` | Password for Verdaccio admin user | ## BluePrint Updates For Licenses instances these options control how new BluePrints are imported `blueprintImport.enabled` | Enables the import of new BluePrints from FlowFuse. Default: `true``blueprintImport.export` | Enables the API endpoint to export the local BluePrints. Default: `false``blueprintImport.url` | The URL to import BluePrints from. Default: `https://app.flowfuse.com/api/v1/flow-blueprints/export-public` # DNS Setup When running FlowFuse on Docker or Kubernetes you will need to be able to setup an entry in a DNS server. This is because FlowFuse uses hostname based routing to know which Node-RED instance you want to access. By default the instance name is used in combination with a supplied domain. In this document we will use `example.com` as the domain. (It doesn't need to be a "whole" domain, it could also be a sub domain of one you already own. e.g. `ff.example.com`). If you are running Docker/Kubernetes on the same machine as the DNS server and Web Browser do not use `127.0.0.1` as the IP address to point the wild card domain at. This is because the host names will be looked up by the Web Browser, the Forge Application, and the Node-RED instances. The last 2 are running in containers and `127.0.0.1` will resolve to the container, not the entry point. ## Production For a production deployment you will need to have access to modify DNS, if you are not sure how to set up DNS records talk to whoever manages your DNS. As mentioned earlier you will need them to create a [wildcard DNS](https://en.wikipedia.org/wiki/Wildcard_DNS_record){rel=""nofollow""} entry that points to either the Docker host machine or the Kubernetes Nodes which are running the Ingress Controller. This can be either: - a `A` (and `AAAA` for IPv6) record pointing to an IP address e.g. `*.example.com 8600 A 192.0.2.1` - a `CNAME` record pointing to the hostname of the entry point e.g. `*.ff.example.com 8600 CNAME forge.example.com` ### AWS ALB Ingress When using AWS ALB (Application Load Balancer) as an Ingress Controller for FlowFuse deployed into an EKS cluster then you would create a wildcard CNAME entry pointing to the hostname of the ALB ### Digital Ocean You should create an A record pointing to the public IP address of the Load Balancer created when you install the Traefik Helm Chart. ## Local Testing and Development For development and testing we probably only need to set up DNS entries for the developers local machine. The easiest way to do this is to use an application called dnsmasq. Dnsmasq is a tool that can be used as a DNS caching proxy (and a DHCP server, but we don't need that). We can set it up to point to an upstream DNS server to resolve all normal addresses, but we can also give it a list of hostname/IP address pairs to use locally. ### DNSMasq Setting up dnsmasq is not too complex, what is harder is setting it up in a way that works well with the network configuration on a laptop that might move between different networks and expects to get its default DNS configuration automatically assigned by DHCP. The following headings cover how to do this on a number of different operating systems #### Ubuntu For Docker on Linux you can use `172.17.0.1` as the address for the domain which is the IP address assigned to the `docker0` interface. ```bash sudo apt-get install dnsmasq sudo echo "bind-interfaces" >> /etc/dnsmasq.conf sudo echo "no-resolv" >> /etc/dnsmasq.conf sudo echo "conf-dir=/etc/dnsmasq.d" >> /etc/dnsmasq.conf sudo echo "address=/example.com/172.17.0.1" > /etc/dnsmasq.d/02-flowforge.conf sudo service dnsmasq restart sudo echo "DNS=127.0.0.1" >> /etc/systemd/resolved.conf sudo echo "DOMAINS=~example.com" >> /etc/systemd/resolved.conf sudo service systemd-resolved restart ``` #### Fedora For Docker on Linux you can use `172.17.0.1` as the address for the domain which is the IP address assigned to the `docker0` interface. ```bash sudo dnf install dnsmasq sudo echo "bind-interfaces" >> /etc/dnsmasq.conf sudo echo "no-resolv" >> /etc/dnsmasq.conf sudo echo "conf-dir=/etc/dnsmasq.d" >> /etc/dnsmasq.conf sudo echo "address=/example.com/172.17.0.1" > /etc/dnsmasq.d/02-flowforge.conf sudo systemctl enable dnsmasq.service sudo service dnsmasq restart sudo echo "DNS=127.0.0.1" >> /etc/systemd/resolved.conf sudo echo "DOMAINS=~example.com" >> /etc/systemd/resolved.conf sudo service systemd-resolved restart ``` #### Windows Unfortunately dnsmasq will not run on Windows and I have not found something similar yet. #### MacOS On MacOS you can alias a private IP address to the loop back interface e.g. `10.128.0.1` with ```bash sudo ifconfig lo0 alias 10.128.0.1 ``` You will need install dnsmasq using [homebrew](https://docs.brew.sh/Installation){rel=""nofollow""} ```bash brew install dnsmasq ``` It appears that the install location differs based on the Apple Hardware. For Intel hardware Macs it's in `/usr/local` and for M1 hardware it's `/opt/homebrew`. Please check where it installed things when the previous command has completed. Then edit a configuration file M1 mac ```bash echo "conf-dir=/opt/homebrew/etc/dnsmasq.d" >> /opt/homebrew/etc/dnsmasq.conf echo "address=/example.com/10.128.0.1" > /opt/homebrew/etc/dnsmasq.d/ff.conf ``` Intel mac ```bash echo "conf-dir=/usr/local/etc/dnsmasq.d" >> /usr/local/etc/dnsmasq.conf echo "address=/example.com/10.128.0.1" > /usr/local/etc/dnsmasq.d/ff.conf ``` Set dnsmasq to run as a service ```bash sudo brew services start dnsmasq sudo dscacheutil -flushcache ``` Tell MacOS to use dnsmasq for our test domain ```bash sudo mkdir -p /etc/resolver sudo tee /etc/resolver/example.com > /dev/null < When using automatic TLS certificate generation, the platform will take a few minutes to generate them on the first platform startup. > For a short period of time browsers may report untrusted certificate warning. This is expected behavior and should resolve itself once the certificate is generated. #### Custom TLS Certificate If you have own TLS certificate, you can use it in FlowFuse platform installation as well. As mentioned before, the certificate must be a wildcard one for the domain you are using. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} If your TLS certificate is issued by a private Certificate Authority, additional configuration is required so that Hosted Instances trust the CA. See [What additional configuration is required when the TLS certificate is issued by a private Certificate Authority?](https://flowfuse.com/#what-additional-configuration-is-required-when-the-tls-certificate-is-issued-by-a-private-certificate-authority) for the step-by-step instructions. ::: :: To configure FlowFuse platform with your certificate, you need to have: - certificate key file - certificate's full chain (server certificate and intermediate certificates bundled into single file) To add your certificate to the platform, edit the `.env` file downloaded earlier and set values for `TLS_ENABLED`, `TLS_CERTIFICATE` and `TLS_KEY` variables. `TLS_ENABLED` variable should be set to `true`. `TLS_CERTIFICATE` should contain the full chain of the certificate while `TLS_KEY` should contain the key file. Example of `.env` file with the custom TLS certificate configuration: ```bash TLS_ENABLED=true TLS_CERTIFICATE=" -----BEGIN CERTIFICATE----- MIIFfzCCBKegAwIBAgISA0 ... -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFfzCCBKegAwIBAgISA0 ... -----END CERTIFICATE----- " TLS_KEY=" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD ... -----END PRIVATE KEY----- " ``` ## Start FlowFuse platform **Note: Make sure all configuration are done above before proceeding.** **Note: Commands must be executed within the same directory where the Docker Compose and `.env` files are located.** #### With automatic TLS certificate generation ```bash docker compose --profile autotls -p flowfuse up -d ``` #### In all other scenarios, including custom TLS certificate ```bash docker compose -p flowfuse up -d ``` The platform will take a few minutes to start up. You can check the status of the containers by running: ```bash docker compose -p flowfuse ps ``` Visit `forge.example.com` (replace `example.com` with the domain configured in the `.env` file) in your browser to access the FlowFuse platform. ## First Run Setup The first time you access the platform in your browser, it will take you through creating an administrator for the platform and other configuration options. For more information, follow [this guide](https://flowfuse.com/docs/install/first-run). Once you have finished setting up the admin user there are some Docker specific items to consider. ## Upgrade **Note: If you are upgrading from version `2.10.0` or lower, please follow [this guide](https://github.com/FlowFuse/docker-compose/blob/main/UPGRADE.md){rel=""nofollow""}** 1. Find the Docker Compose project name: ```bash docker compose ls ``` The output will show the project name, as well as the location of Docker Compose file used for creating the project. 2. Stop the existing project (replace `$projectName`with your project name): ```bash docker compose -p $projectName down --rmi all ``` 3. Download the latest Docker Compose files: ```bash curl -L -o docker-compose.yml https://github.com/FlowFuse/docker-compose/releases/latest/download/docker-compose.yml ``` 4. Make sure the `.env` file is present and contains your installation-specific configuration. Download an example `.env`file if needed: ```bash curl -o .env.example https://raw.githubusercontent.com/FlowFuse/docker-compose/refs/heads/main/.env.example ``` 5. Pull the latest version of the default stack container (and any other stacks you have configured) ```bash docker pull flowfuse/node-red:latest ``` 6. Start the project depending on the TLS configuration (replace `$projectName` with your project name): - automatic TLS: ```bash docker compose --profile autossl -p $projectName up -d ``` - any other scenario: ```bash docker compose -p $projectName up -d ``` ## Common Questions ### How to use external database server? FlowFuse platform uses PostgreSQL database to store its data. By default, the database is created and managed by the Docker Compose. If you want to use an external database server, you need to: - on your database server, create `flowforge` and `ff-context` databases as well as a user with access to both of them (see `configs.postgres_db_setup` and `configs.postgres_context_setup` in the `docker-compose.yml` file for the reference) - configure the connection to the database in the `.env` file. Set the `DB_HOST`, `DB_USER`, `DB_PASSWORD` variables to the connection details of the external database server Once ready, [start the application](https://flowfuse.com/#start-flowfuse-platform) . ### How can I provide my own TLS certificate? If you have your own TLS certificate, you can use it in FlowFuse platform installation as well. See [Enable HTTPS](https://flowfuse.com/#enable-https-optional) section for more details. Additionally, if your TLS certificate is issued by a private Certificate Authority, you will need to perform some additional configuration to make hosted Node-RED instances trust the CA. See [What additional configuration is required when the TLS certificate is issued by a private Certificate Authority](https://flowfuse.com/#what-additional-configuration-is-required-when-the-tls-certificate-is-issued-by-a-private-certificate-authority) for the step-by-step instructions. ### What additional configuration is required when the TLS certificate is issued by a private Certificate Authority? When the FlowFuse platform is configured with a TLS certificate issued by a private Certificate Authority (CA), hosted Node-RED instances will not trust the CA by default. As a result, opening the editor for a hosted instance will fail with a certificate trust error. To make hosted Node-RED instances trust the private CA, follow the steps below: 1. **Update the environment file.** In the `.env` file created during the installation process, set `DOCKER_DRIVER_PRIVATE_CA_PATH` to the path of your Certificate Authority certificate file. This must be the path to the file stored on the Docker host server. For example, if the `ca.pem` file is located at `/usr/local/ssl/ca.pem`: ```bash DOCKER_DRIVER_PRIVATE_CA_PATH="/usr/local/ssl/ca.pem" ``` 2. **Modify the Compose file.** In the `docker-compose.yml` file used to create the FlowFuse Docker stack, uncomment the `NODE_EXTRA_CA_CERTS` environment variable and the corresponding volume mount on the `forge` service so the certificate is passed correctly to the platform. You can apply both changes with a single `sed` command: ```bash sed -i.bak -E -e 's/^#([[:space:]]*- "NODE_EXTRA_CA_CERTS=)/\1/' -e 's/^#([[:space:]]*- \$\{DOCKER_DRIVER_PRIVATE_CA_PATH\})/\1/' docker-compose.yml && rm docker-compose.yml.bak ``` 3. **Restart the Forge service** to apply the changes: ```bash docker compose restart forge ``` 4. **Apply changes to running instances.** Once the platform is online, manually suspend and start all running hosted instances so they pick up the new TLS settings. ### I would like to invite my team members to the platform with e-mail, how can I do that? In order to configure FlowFuse platform with external e-mail server, you need to adjust `EMAIL_*` variables in the `.env` file. Find the `.env` file end edit `Email configuration` section with following details: - `EMAIL_ENABLED` - set to `true` to enable e-mail functionality - `EMAIL_HOST` - provide SMTP server host - `EMAIL_PORT` - provide SMTP server port (default is `587`) - `EMAIL_SECURE` - set to `true` if the connection should be secured - `EMAIL_USER` - provide SMTP server username - `EMAIL_PASSWORD` - provide SMTP for the user defined in `EMAIL_USER` Restart the core application to apply the changes: ```bash docker compose restart forge ``` ### Connection Refused error After starting the platform, I can't access it in the browser - I see "Connection Refused error" If you are using the Digital Ocean Docker Droplet to host FlowFuse you will need to ensure that port 80 & 443 are opened in the UFW firewall before starting. FlowFuse platform is running on ports 80 and 443, so you need to open these ports in the firewall. Below are examples of commands to open these ports: Ubuntu: ```bash sudo ufw apply http sudo ufw apply https ``` CentOS: ```bash sudo firewall-cmd --zone=public --add-service=http --permanent sudo firewall-cmd --zone=public --add-service=https --permanent sudo firewall-cmd --reload ``` Windows (command prompt): ```bash netsh advfirewall firewall add rule name="Open Port 80" dir=in action=allow protocol=TCP localport=80 netsh advfirewall firewall add rule name="Open Port 443" dir=in action=allow protocol=TCP localport=443 ``` Windows (PowerShell): ```powershell New-NetFireWallRule -DisplayName 'WSL 8080TCP' -Direction Inbound -LocalPort 8080 -Action Allow -Protocol TCP New-NetFireWallRule -DisplayName 'WSL 8080TCP' -Direction Outbound -LocalPort 8080 -Action Allow -Protocol TCP ``` ### I installed FlowFuse on Windows with WSL2, application is running but I can't access it in the browser Next to [opening the ports in the firewall](https://flowfuse.com/#connection-refused-error), you need to configure port forwarding from Windows host to WSL2 server. To forward traffic from an external IP to your container, run the following PowerShell command (administrator privileges required): ```powershell netsh interface portproxy add v4tov4 listenport=80 listenaddress=0.0.0.0 connectport=80 connectaddress=127.0.0.1 ``` This command forwards traffic from port 80 on your external IP address to port 80 on your localhost, where the Nginx Proxy container is listening for connections. ### How can I enable persistent storage for Node-RED instances? Node-RED instances running in Docker do not have direct access to a persistent file system to store files or use for storing context data. FlowFuse includes a File Storage service that can be enabled to provide persistent storage. To disable the default File nodes, edit the Template and add `10-file.js,23-watch.js` to the "Exclude nodes by filename" section ![](https://flowfuse.com/docs/install/images/file-node-template.png){width="500"} FlowFuse Docker Compose files includes FlowFuse File Storage component by default and starts it along with the platform. Full details on configuring the File Storage service are available [here](https://flowfuse.com/docs/install/file-storage). ## Uninstall - Bring the services down with `docker compose -p flowfuse down -v` (note the extra `-v` to delete all the volumes, only include this if you do not want to reuse this install) - Use `docker images` to list container images - Use `docker rmi [imagename]:[tag]` to remove all images that start with `flowfuse/` # Docker Stacks A Stack defines a set of platform configuration options that will get applied to each Node-RED instance when created. For container based deployment models, this covers three things: - `memory` - the amount of memory (in MB) to limit container to. Recommended minimum: `256`. - `cpu` - a value between 1 and 100 that is the % of a CPU core the container should be allowed to consume. - `container location` - this is the fully qualified name of the container to use. The default container built when following the install instructions is named `flowfuse/node-red:latest` If you wish to use different Node-RED version, you need to specify the name of the container and the version you want to use. For example, if you want to use Node-RED v3.1.x, you should enter `flowfuse/node-red:latest-3.1.x` in the `container location` section of the Stack configuration. Full list of available pre-built containers can be found on [Docker Hub](https://hub.docker.com/r/flowfuse/node-red/tags){rel=""nofollow""}. ## Creating Own Containers As mentioned in the previous paragraph, we encourage to use our pre-built containers in your stacks. However, if you want to create your own container, you can do so by creating a `Dockerfile` and `package.json` files. There is an example `Dockerfile` and `package.json` in the [node-red-container](https://github.com/FlowFuse/docker-compose/tree/main/node-red-container){rel=""nofollow""} directory of the [docker-compose](https://github.com/FlowFuse/docker-compose){rel=""nofollow""} project. This will start with `nodered/node-red:latest` as it's base and then add the required FlowFuse components. Builds of this container for amd64, arm64 and armv7 are built for every release and published to Docker hub as [flowfuse/node-red](https://hub.docker.com/r/flowfuse/node-red){rel=""nofollow""}. These can be used as a base to build custom stacks. If you wanted to pin at Node-RED v3.0.2 you would change the first line to: ```docker FROM nodered/node-red:3.0.2 ARG REGISTRY RUN if [[ ! -z "$REGISTRY" ]] ; then npm config set @flowfuse:registry "$REGISTRY"; fi COPY package.json /data ... ``` To add nodes to the default image you can extend the supplied container. The following Dockerfile will install the node-red-dashboard ```docker FROM flowfuse/node-red WORKDIR /usr/src/node-red RUN npm install node-red-dashboard WORKDIR /usr/src/flowforge-nr-launcher ``` To build the container run the following: ```shell docker build node-red-container/Dockerfile-dashboard -t flowfuse/node-red-dashboard:3.0.2 ``` You would then enter `flowfuse/node-red-dashboard:3.0.2` in the `container` section of the Stack configuration. Stacks can be changed on a per instance basis, see also the [user stack documentation](https://flowfuse.com/docs/user/changestack). # Using Docker on Windows While Docker is inherently a Linux-based technology and we recommend running FlowFuse on Linux when ever possible, there are several ways to run Docker on Windows. Below, we outline the primary methods available, along with recommendations based on specific needs and use cases. ### Docker Desktop [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/){rel=""nofollow""} is the most straightforward option for running Docker on Windows, offering a complete Docker environment with GUI support. It’s well-suited for users seeking ease of setup and use. **Recommendation:** Use Docker Desktop if licensing is not a constraint for your organization, as it provides a user-friendly, fully-integrated Docker experience on Windows. ### Rancher Desktop [Rancher Desktop](https://rancherdesktop.io/){rel=""nofollow""} is a free, open-source alternative to Docker Desktop, providing a similar experience but without the licensing concerns. Rancher Desktop includes both container management and Kubernetes support, making it a flexible choice for containerized workloads on Windows. **Recommendation:** Consider Rancher Desktop if you prefer an open-source tool with no licensing restrictions. ### Windows Subsystem for Linux (WSL2) [WSL2](https://docs.microsoft.com/en-us/windows/wsl/install){rel=""nofollow""} enables users to run a Linux environment directly on Windows. WSL2 supports running Docker Engine without the need for a GUI, making it ideal for headless configurations or for those looking to operate Docker from a Linux command line on Windows. **Recommendation:** WSL2 is best suited for advanced users who are comfortable with command-line tools and who want to run Docker Engine directly within a Linux environment on Windows. For the installation of Docker Engine using WSL2, refer to the next paragraph. ## How to Install Docker Engine (Docker CE) on Windows using WSL2 This guide explains how to install Docker Engine (Docker CE) on Windows using Windows Subsystem for Linux (WSL2). ### Prerequisites Ensure your system meets the following Windows Subsystem for Linux v2 requirements: - Windows Server 2022 - Windows 10 version 2004 and higher (Build 19041 and higher) - Windows 11 ### Step 1: Install Windows Subsystem for Linux Open PowerShell as an administrator and run the following commands: 1. Install WSL and the default Linux distribution (Ubuntu) using the following command: ```powershell wsl --install ``` 2. Reboot your system to apply changes ```powershell shutdown -r -t 5 ``` After the reboot, WSL will automatically start installing the Ubuntu Linux distribution. You will be prompted to create a new UNIX account. Follow the instructions to create a new user account and set a password. ![wsl-unix-user-creation](https://flowfuse.com/docs/install/images/wsl-unix-user.png) Once completed, you will be dropped into a new Ubuntu shell. ![wsl-install-complete](https://flowfuse.com/docs/install/images/wsl-install-complete.png) 3. Confirm using proper WSL version, by running (in Powershell window): ```powershell wsl --status ``` ### Step 2: Install Docker on Ubuntu Once the Ubuntu system is ready, follow these steps to install Docker: 1. **Remove Conflicting Packages**:br First, remove any existing Docker packages that might conflict with the installation: ```bash for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done ``` 2. **Add Docker’s Official GPG Key and APT Repository**:br Next, update your package list and add Docker's official GPG key and APT repository: ```bash sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc ``` :brAdd the Docker repository to your APT sources: ```bash echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update ``` 3. **Install Docker Packages**:br Now, install Docker and its associated packages: ```bash sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` 4. **Add user to Docker group**:br Add your user to the `docker` group to run Docker commands without `sudo`: ```bash sudo usermod -aG docker ubuntu ``` 5. **Start the Docker Service**:br Start Docker using the following command: ```bash sudo /etc/init.d/docker start ``` 6. **Verify Docker Installation**:br Run a test Docker container to verify that Docker is installed and running correctly: ```bash sudo docker run hello-world ``` :brIf Docker is installed correctly, you should see a similar output: :br![wsl-docker-installation-complete](https://flowfuse.com/docs/install/images/wsl-docker-complete.png) Once Docker is installed, you can [install the FlowFuse platform using docker compose](https://flowfuse.com/docs/install/docker). # Example configuration for common email platforms ## GMail ```yaml email: enabled: true debug: false smtp: host: smtp.gmail.com port: 465 secure: true auth: user: [USER]@gmail.com pass: [PASSWORD] ``` Note: Gmail may require an app specific password to be created if you are using 2FA on the account you can set that up [here](https://security.google.com/settings/security/apppasswords){rel=""nofollow""} ## Office365 ```yaml email: enabled: true debug: false smtp: host: smtp.office365.com, secure: false tls: ciphers: "SSLv3", rejectUnauthorized: false auth: user: [USERNAME] pass: [PASSWORD] ``` # FlowFuse File Storage FlowFuse has two storage-related features for Node-RED instances in container-based deployments. They are not mutually exclusive, and some deployments use both. - **Persistent Storage** mounts a persistent volume into the Node-RED instance at `/data/storage`. This was introduced in FlowFuse v2.6.0 for Kubernetes. - **The File Storage service** provides the FlowFuse File nodes and Persistent Context. The rest of this page is structured around those two features so it is clearer which configuration you need. ## Which configuration do I need? Use the following guide when choosing what to configure: | Requirement | FlowFuse version | What to configure | | ----------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Files written directly by the Node-RED instance must survive restart, suspend/resume, or stack upgrades | v2.6.0 or later | Configure [Persistent Storage for Node-RED instances](https://flowfuse.com/#persistent-storage-for-node-red-instances) | | | Before v2.6.0 | Configure [The File Storage service](https://flowfuse.com/#the-file-storage-service) | | You want to use [FlowFuse Persistent Context](https://flowfuse.com/docs/user/persistent-context/){rel=""nofollow""} | v2.6.0 or later | Configure [The File Storage service](https://flowfuse.com/#the-file-storage-service) and the [Persistent Context configuration](https://flowfuse.com/#persistent-context-configuration). If the instance also needs persistent filesystem access, configure [Persistent Storage for Node-RED instances](https://flowfuse.com/#persistent-storage-for-node-red-instances) as well. | | | Before v2.6.0 | Configure [The File Storage service](https://flowfuse.com/#the-file-storage-service) and the [Persistent Context configuration](https://flowfuse.com/#persistent-context-configuration). The File Storage service also provides persistent storage for the instance. | ## Persistent Storage for Node-RED instances As part of the FlowFuse v2.6.0 release, a new Persistent Storage approach was implemented for Kubernetes. Docker support will follow. This mounts a Persistent Volume into the container running the Node-RED instance on `/data/storage`. Files written to this location are preserved for the life of the instance and across suspend/resume operations and stack upgrades. If you are using FlowFuse version lower than v2.6.0, you will need to use the File Storage service to provide persistent storage to Node-RED or upgrade to v2.6.0 or later to use this feature. ### Configuring Create a Kubernetes `StorageClass` that allows dynamic provisioning of `PersistentVolumes` from `PersistentVolumeClaims`, for example the [AWS EFS CSI driver](https://github.com/kubernetes-sigs/aws-efs-csi-driver){rel=""nofollow""}. Then pass the following values to the FlowFuse Helm Chart when upgrading. ```yaml forge: persistentStorage: enabled: true storageClass: '' size: '5Gi' ``` Where `size` is the default size for the volume. #### Azure If you are using the `azurefile-csi` Persistent Storage driver then we recommend adding the following to the `StorageClass mountOptions`: ```yaml mountOptions: - dir_mode=0777 - file_mode=0777 - mfsymlinks - nobrl ``` See [the Azure Kubernetes documentation](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/storage/mountoptions-settings-azure-files){rel=""nofollow""} for more details. ## The File Storage service The FlowFuse Platform includes a File Storage service that provides: - A set of custom File nodes that behave the same way as the standard Node-RED File nodes - An optional Persistent Context store for storing context data within flows. This feature is only available for platforms running with a premium license. On FlowFuse v2.6.0 or later, the File Storage service is used exclusively for Persistent Context. Instance-level Persistent Storage is handled separately via [Persistent Storage for Node-RED instances](https://flowfuse.com/#persistent-storage-for-node-red-instances). Before FlowFuse v2.6.0, this service was also the primary way to provide persistent storage to Node-RED in container-based environments, in addition to Persistent Context. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} The File Storage service is only required in Docker or Kubernetes environments. If you are using the LocalFS platform driver, Node-RED already has direct access to the local filesystem. ::: :: ### Configuring The File Storage server has its own configuration file: `etc/flowforge-storage.yml`. - **Docker Compose** - edit the file directly before starting the service - **Kubernetes/Helm** - include the options in your `customization.yml`, using `forge.fileStore.*` as the property name prefix. You must also set `forge.fileStore.enabled` to `true` to tell Helm to deploy the service. There are three parts to the configuration: - [Platform Configuration](https://flowfuse.com/#platform-configuration) - how to access the main FlowFuse platform application - [File Storage service configuration](https://flowfuse.com/#file-storage-service-configuration) - what storage to use for the File nodes - [Persistent Context configuration](https://flowfuse.com/#persistent-context-configuration) - what storage to use for Persistent Context ### Platform Configuration | Option | Description | | ---------- | ------------------------------------------------------------------------------------ | | `host` | Where to listen for incoming connections. Default: `0.0.0.0`. | | `port` | The port to listen on. Default: `3001` | | `base_url` | The url to access the FlowFuse platform on. This defaults to `http://localhost:3000` | ### File Storage service configuration The File Storage configuration determines where the files used by the Node-RED File nodes are stored. You can configure it to store files either on the local filesystem of the File Storage server or in an AWS S3-compatible service. #### LocalFS Stores the files locally, for example by using a volume mounted into the File Storage server container. | Option | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------- | | `driver.type` | `localfs` | | `driver.quota` | A per-instance quota for how much data will be stored - in bytes. If this is not set, no limit will be applied | | `driver.options.root` | The root path under which Node-RED instance data should be stored. | The following shows an example configuration using the `localfs` file driver. ```yaml driver: type: localfs quota: 104857600 options: root: var/root ``` #### S3 Compatible Storage Stores the files in an external service using the AWS S3 API. | Option | Description | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `driver.type` | `s3` | | `driver.quota` | A per-instance quota for how much data will be stored - in bytes. If this is not set, no limit will be applied | | `driver.options.bucket` | Name of the S3 bucket to use (required) | | `driver.options.region` | Name of AWS region of the bucket (required) | | `driver.options.endpoint` | S3 ObjectStore Endpoint, if not using AWS S3 | | `driver.options.forcePathStyle` | `true` | | `driver.options.credential.accessKeyId` | Account ID / Username | | `driver.options.credential.secretAccessKey` | Secret Key / Password | The full list of valid options under `driver.options` is available in the [AWS S3Client documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html){rel=""nofollow""}. For example: ```yaml driver: type: s3 quota: 104857600 options: bucket: flowforge-files credentials: accessKeyId: XXXXXXXXXXXXXXXXXXX secretAccessKey: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX forcePathStyle: true region: us-east-1 ``` #### Enabling the FlowFuse File Nodes The FlowFuse File nodes have been written to be direct replacements for the Node-RED core file-in and file-out nodes. This means that only one version of these nodes can be active in Node-RED at a time. The FlowFuse File nodes will automatically disable themselves if the core nodes are present. This means to enable the nodes you need to exclude the code nodes. This can be done in the FlowFuse Template. ![](https://flowfuse.com/docs/install/images/file-node-template.png){width="500"} Adding `10-file.js` to the list of "Excluded nodes by filename" section will ensure that the core file nodes are not loaded by Node-RED. ### Persistent Context configuration The Context Storage configuration determines where Node-RED Context data is stored. This feature is only available when running with a FlowFuse Premium license. Due to the different access patterns for context data, this requires a separate storage configuration to the File store. It can use either an SQLite or PostgreSQL database. #### SQLite | Option | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------- | | `context.type` | `sequelize` | | `context.quote` | A per-instance quota for how much data will be stored - in bytes. If this is not set, no limit will be applied | | `context.options.type` | `sqlite` | | `context.options.storage` | Path to the sqlite database file to use | For example: ```yaml context: type: sequelize quota: 1048576 options: type: sqlite storage: ff-context.db ``` #### PostgreSQL | Option | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------------- | | `context.type` | `sequelize` | | `context.quote` | A per-instance quota for how much data will be stored - in bytes. If this is not set, no limit will be applied | | `context.options.type` | `postgres` | | `context.options.host` | The hostname of the database server | | `context.options.port` | The port of the database server | | `context.options.database` | The name of the database to store context data in | | `context.options.username` | The username to access to the database with | | `context.options.password` | The password to access to the database with | For example: ```yaml context: type: sequelize quota: 1048576 options: type: postgres host: flowforge-postgresql port: 5432 database: ff-context username: postgres password: password ``` ## Working with FlowFuse Devices The FlowFuse Device Agent does *not* use the File Storage service. Any flows using the file nodes or persistent context that are deployed to a Device will use the local filesystem directly. # First-run Setup Following a successful install, you will be able to access the platform to go through the initial setup. ## 1. Start setup ### - LocalFS Open FlowFuse in your browser `http://localhost:3000` ### - Docker or Kubernetes Open FlowFuse in your browser `http://forge.example.com` (Change `.example.com` to match the domain you set up in DNS) Click the **START SETUP** button ![](https://flowfuse.com/docs/install/images/setup-01.png){width="500"} ## 2. Create an Administrator The first user you create will be an Administrator. They will have full access to the platform, be able to set platform-wide configuration and manage users and teams. **Note**: with the 0.3 release, it is possible to reset your password *if* you have email configured and enabled the `user:reset-password` option in Admin settings. Otherwise, make sure you make a note of the password you set. We will provide tools to manage passwords outside of the platform in a future release. ![](https://flowfuse.com/docs/install/images/setup-02-user.png){width="500"} ## 3. Upload a license FlowFuse Community Edition is Open Source and can be used freely without a license. If you have a FlowFuse Enterprise Edition license you can upload it here. ![](https://flowfuse.com/docs/install/images/setup-03-license.png){width="500"} ## 4. Finish setup Once you complete the setup, you will be able to log in as the Administrator user you created and start using the platform. You can setup your Team and create your first Node-RED instance. More information about using the FlowFuse platform is available in the main [user guide](https://flowfuse.com/docs/user/introduction). # Installing FlowFuse FlowFuse can be installed to run in Docker or Kubernetes based environments. - **Docker:** - [Quick Start Guide](https://flowfuse.com/docs/quick-start) - [Full Install](https://flowfuse.com/docs/install/docker/) - **Kubernetes:** - [Install Guide](https://flowfuse.com/docs/install/kubernetes/) We also provide one-click installs of the Docker version: - [Digital Ocean Docker Install Guide](https://flowfuse.com/docs/install/docker/digital-ocean) - [AWS Docker Install Guide](https://flowfuse.com/docs/install/docker/aws-marketplace) Whichever installation you choose, the platform needs outbound network access to a set of hostnames. If it runs behind a firewall or proxy, see [Networking requirements](https://flowfuse.com/docs/install/networking-requirements). ## Upgrading FlowFuse If you are upgrading FlowFuse, please refer to the [Upgrade Guide](https://flowfuse.com/docs/upgrade/) for any specific actions required. ## Do You Need Help? Installation Service If you need assistance, request our complimentary Installation Service, and we will help you install FlowFuse. # AWS EKS Specific details This document includes details of installing FlowFuse on AWS EKS. The following assumptions have been made in the examples: 1. The user has the correct AWS IAM policy access to complete all tasks 2. All AWS services are running in `eu-west-1` ## Prerequisites ### AWS Cli This is used to interact with the whole AWS environment {rel=""nofollow""} From here onwards this document assumes that you have configured the AWS CLI tools with a user that has permission to carry out the steps. This document does not include details of how to configure such a user in AWS IAM. Please show this document to you AWS Account Admin if you need help. ### eksctl This tool is used to create/modify AWS EKS Clusters, it uses the credentials from the AWS Cli. {rel=""nofollow""} ## Setup a new domain on Route53 Do this in the AWS Console/Your DNS provider ## Create an AWS Certificate Request a certificate for `*.[DOMAIN]` from Amazon Certificate Manager Do this in AWS Console, with Route53 validation ## Create EKS Cluster Edit the `cluster.yml` file in `aws_eks` to set your preferred instance type and count along with AWS Region ```bash eksctl create cluster -f cluster.yml ``` Example cluster.yml (Please visit [eksctl.io](https://eksctl.io/usage/creating-and-managing-clusters/#using-config-files){rel=""nofollow""} to be sure you understand what this does.) ```yaml apiVersion: eksctl.io/v1alpha5 kind: ClusterConfig metadata: name: FlowFuse region: eu-west-1 iam: withOIDC: true addons: - name: aws-ebs-csi-driver resolveConflicts: overwrite nodeGroups: - name: management labels: role: "management" instanceType: t2.small desiredCapacity: 1 volumeSize: 20 ssh: allow: false iam: withAddonPolicies: ebs: true - name: instance labels: role: "projects" tags: k8s.io/cluster-autoscaler/enabled: "true" k8s.io/cluster-autoscaler/flowforge: "owned" instanceType: t2.small desiredCapacity: 2 volumeSize: ssh: allow: false ``` ## Ingress Controller ### Traefik It is recommended to run the [Traefik](https://doc.traefik.io/traefik/) even on AWS EKS (The AWS ALB load balancer currently appears to only support up to 100 Ingress Targets which limits the number of Hosted Instances that can be run). Create a `traefik-values.yaml` file to pass the values to the Traefik helm file. ```bash touch traefik-values.yaml ``` Fill the `traefik-values.yaml` file with the following content. Replace `` with the certificate ARN created earlier ```yaml service: enabled: true type: LoadBalancer annotations: service.beta.kubernetes.io/aws-load-balancer-type: "nlb" service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "" service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443" service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp" service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "120" service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: "proxy_protocol_v2.enabled=true" spec: externalTrafficPolicy: Cluster deployment: replicas: 2 ports: web: port: 8000 expose: default: true exposedPort: 80 protocol: TCP forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" websecure: port: 8443 expose: default: true exposedPort: 443 protocol: TCP http: middlewares: - traefik-force-https@kubernetescrd - traefik-large-body@kubernetescrd # Disable TLS since NLB handles termination tls: enabled: false forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" ingressClass: enabled: true isDefaultClass: false name: traefik additionalArguments: - "--entryPoints.web.proxyProtocol.insecure=true" - "--entryPoints.websecure.proxyProtocol.insecure=true" - "--entryPoints.web.forwardedHeaders.insecure=true" - "--entryPoints.websecure.forwardedHeaders.insecure=true" providers: kubernetesIngress: enabled: true kubernetesCRD: enabled: true api: dashboard: false insecure: false logs: access: enabled: true fields: headers: defaultMode: keep extraObjects: - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: force-https namespace: traefik spec: headers: customRequestHeaders: X-Forwarded-Proto: "https" - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: large-body namespace: traefik spec: buffering: maxRequestBodyBytes: 10485760 ``` Install the Traefik with the following command: ```bash helm repo add traefik https://traefik.github.io/charts helm repo update helm upgrade --install traefik traefik/traefik \ --create-namespace \ -n traefik \ -f traefik-values.yaml \ --wait \ --atomic ``` ### References {rel=""nofollow""} ## AWS ALB Ingress AWS ALB has a hard limit of 100 Ingress endpoints which limits the number of Projects/Instances that can be deployed. ## Setup AWS SES for email {rel=""nofollow""} Setup identity to match sending domain (requires DNS entries) Setup email identity to send test emails to Request move to production from sandbox (need to include examples of emails being sent and why/when those emails will be sent should only need this for prod) `ses_policy.json` (with suitable aws id, aws region and domain modifications): ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "ses:SendEmail", "ses:SendTemplatedEmail", "ses:SendRawEmail" ], "Resource": "arn:aws:ses:[aws region]:[aws id]:identity/[domain name]" } ] } ``` ```bash IAM_POLICY_ARN=$(aws iam create-policy --policy-name FlowForgeSendEmail --policy-document file://ses_policy.json --output json | jq -r .Policy.Arn) ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) OIDC_PROVIDER=$(aws eks describe-cluster --name flowforge --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///") read -r -d '' TRUST_RELATIONSHIP < trust.json aws iam create-role --role-name flowforge_service_account_role --assume-role-policy-document file://trust.json --description "Role to bind to flowforge service account" aws iam attach-role-policy --role-name flowforge_service_account_role --policy-arn=$IAM_POLICY_ARN ``` Make a note of the ARN for the IAM Role (flowforge\_service\_account\_role) is needed in the helm chart values yaml file. `aws iam get-role --role-name flowforge_service_account_role` ### References Create a IAM Role to bind IAM Policies to the service account {rel=""nofollow""} Create IAM Policy to allow sending emails (example: {rel=""nofollow""}) ## Use AWS RDS PostgreSQL instance The following script creates a AWS RDS PostgreSQL instance, it also sets up some network access rules so only the FlowFuse app can access it from inside the cluster (and not the Node-RED instances). Please read it carefully before running it to ensure you understand it. A copy of this file can be found [here](https://github.com/FlowFuse/flowforge/blob/f8c06e3cea0ffb539350797af429f1a0366243f1/docs/install/kubernetes/setup-rds.sh){rel=""nofollow""} Run the following command ```bash ./setup-rds.sh ``` Make a note of the postgres hostname ```bash aws rds describe-db-instances | jq .DBInstances[].Endpoint.Address ``` ### References {rel=""nofollow""} # FlowFuse-oriented infrastructure in AWS using Terraform and Helm This step-by-step guide will help you use the Terraform modules available in the FlowFuse repository to setup resources required to run FlowFuse platform on AWS. After following the commands in this documentation, the following resources will be created: - [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html) - [EKS](https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html) (Elastic Kubernetes Service) - [RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Welcome.html) (Relational Database Service) - [VPC Peering](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-peering.html) - [Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html) domain zone - [AWS Certificate](https://docs.aws.amazon.com/acm/latest/userguide/acm-overview.html) - [SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/Welcome.html) (Simple Email Service) The full list of the resources created by each module can be found in the documentation of the respective modules. To ensure proper infrastructure deployment, these modules must be executed in a specific order: `vpc`, `eks`, `rds`, `vpc-peering`, `route53` and `ses`. Additionally, a shared variables file (`terraform.tfvars`) will be created to manage configuration used by all modules. While Terraform supports nested modules for complex infrastructure, this guide will treat each module as a root module for simplicity. All modules are configurable, allowing you to customize the infrastructure to meet your specific needs. Detailed information on configuring each module can be found in its documentation. This guide will uses minimal configuration to demonstrate the basic setup. ## Prerequisites - [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli), [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), [Helm](https://helm.sh/docs/helm/helm_install/) and [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your machine. - Access to an AWS account through [AWS access key](https://repost.aws/knowledge-center/create-access-key), that allows the creation of new resources. The specific resources created by each module are detailed in the documentation of the respective modules. ## Step 1: Clone the Repository First, clone the FlowFuse Terraform repository to your local machine: ```bash git clone https://github.com/FlowFuse/terraform-aws-flowfuse.git cd terraform-aws-flowfuse ``` ## Step 2: Set AWS Environment Variables Set the following environment variables to authenticate Terraform with your AWS account: ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_REGION= ``` Replace ``, ``, and `` with your actual AWS credentials and desired AWS region. All resources must be created in the same region. For example, if you want to create resources in `us-west-2`, set the region as follows: ```bash export AWS_REGION=us-west-2 ``` Ensure that these environment variables are set for the duration of the session or included in your shell profile to persist across sessions. ## Step 3: Create the `terraform.tfvars` File Create a `terraform.tfvars` file in the root directory of the repository. This file will contain the shared variables for all the modules. ```bash touch terraform.tfvars ``` Edit the `terraform.tfvars` file and add the following content. Replace `` with the [ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) of the AWS user that will have access to the EKS cluster. This user will be granted the `ClusterAdmin` role in the EKS cluster. ```hcl namespace = "my-company" stage = "production" route53_zone_name = "my-domain.com" eks_access_entry_map = { "" = { access_policy_associations = { ClusterAdmin = {} } } } ``` ## Step 4: Create AWS Resources **The order of creating resources is important. The documentation specifies the correct order. Attempts to create resources in a different order may fail.** ### 1. VPC Module To create basic networking, initialize the VPC module: ```bash terraform -chdir=vpc init ``` Apply the VPC module using the shared variables file: ```bash terraform -chdir=vpc apply -var-file=../terraform.tfvars ``` ### 2. EKS Module To create EKS cluster, initialize the EKS module: ```bash terraform -chdir=eks init ``` Apply the EKS module using the shared variables file: ```bash terraform -chdir=eks apply -var-file=../terraform.tfvars ``` ### 3. RDS Module To create RDS database, initialize the RDS module: ```bash terraform -chdir=rds init ``` Apply the RDS module using the shared variables file: ```bash terraform -chdir=rds apply -var-file=../terraform.tfvars ``` ### 4. VPC Peering Module To create a peering between EKS and RDS networks, initialize the VPC Peering module: ```bash terraform -chdir=vpc-peering init ``` Apply the VPC Peering module using the shared variables file: ```bash terraform -chdir=vpc-peering apply -var-file=../terraform.tfvars ``` ### 5. Route53 Module To create a domain and certificate, initialize the Route53 module: ```bash terraform -chdir=route53 init ``` Apply the Route53 module using the shared variables file: ```bash terraform -chdir=route53 apply -var-file=../terraform.tfvars ``` Remember to change NS records in your domain registrar to the ones provided by the Route53 module. NS records are printed after the Route53 module is applied. They can be also printed using the following command: ```bash terraform -chdir=route53 output domain_dns_records ``` ### 6. SES Module To create an email service, initialize the SES module: ```bash terraform -chdir=ses init ``` Apply the SES module using the shared variables file: ```bash terraform -chdir=ses apply -var-file=../terraform.tfvars ``` To get the IAM role ARN created by the SES module and required during [FlowFuse platform configuration](https://flowfuse.com/docs/install/configuration#aws-ses-email), run the following command: ```bash terraform -chdir=eks output flowfuse_role_arn ``` ## Step 5: Deploy Traefik It is recommended to run the [Traefik](https://doc.traefik.io/traefik/) even on AWS EKS (The AWS ALB load balancer currently appears to only support up to 100 Ingress Targets which limits the number of Hosted Instances that can be run). Create a `traefik-values.yaml` file for Traefik configuration: ```bash touch traefik-values.yaml ``` Get the certificate ARN (``) from the `route53` module outputs: ```bash terraform -chdir=route53 output acm_certificate_arn ``` Fill the `traefik-values.yaml` file with the following content. Replace `` with the certificate ARN. ```yaml service: enabled: true type: LoadBalancer annotations: service.beta.kubernetes.io/aws-load-balancer-type: "nlb" service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "" service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443" service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp" service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "120" service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: "proxy_protocol_v2.enabled=true" spec: externalTrafficPolicy: Cluster deployment: replicas: 2 ports: web: port: 8000 expose: default: true exposedPort: 80 protocol: TCP forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" websecure: port: 8443 expose: default: true exposedPort: 443 protocol: TCP http: middlewares: - traefik-force-https@kubernetescrd - traefik-large-body@kubernetescrd # Disable TLS since NLB handles termination tls: enabled: false forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" ingressClass: enabled: true isDefaultClass: false name: traefik additionalArguments: - "--entryPoints.web.proxyProtocol.insecure=true" - "--entryPoints.websecure.proxyProtocol.insecure=true" - "--entryPoints.web.forwardedHeaders.insecure=true" - "--entryPoints.websecure.forwardedHeaders.insecure=true" providers: kubernetesIngress: enabled: true kubernetesCRD: enabled: true api: dashboard: false insecure: false logs: access: enabled: true fields: headers: defaultMode: keep extraObjects: - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: force-https namespace: traefik spec: headers: customRequestHeaders: X-Forwarded-Proto: "https" - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: large-body namespace: traefik spec: buffering: maxRequestBodyBytes: 10485760 ``` Update your kubeconfig file to point to the EKS cluster and set the correct context ```bash aws eks update-kubeconfig --name $(terraform -chdir=eks output -raw cluster_name) ``` Install the Traefik Ingress controller with the following command: ```bash helm repo add traefik https://traefik.github.io/charts helm repo update helm upgrade --install traefik traefik/traefik \ --create-namespace \ -n traefik \ -f traefik-values.yaml \ --wait \ --atomic ``` ## Conclusion You have successfully set up the necessary AWS infrastructure to run the FlowFuse platform and performed minimal EKS cluster configuration required. With this infrastructure in place, your environment is now ready for the installation of the FlowFuse platform. For detailed installation instructions, please visit: [FlowFuse Kubernetes Installation Guide](https://flowfuse.com/docs/install/kubernetes). # Installing FlowFuse on a Digital Ocean Kubernetes cluster ## Prerequisites ### Digital Ocean Account You will need an active Digital Oceans Account, you can sign up for an account [here](https://cloud.digitalocean.com/registrations/new){rel=""nofollow""} ### Utilities - kubectl - {rel=""nofollow""} - helm - {rel=""nofollow""} ### DNS You will need a domain that FlowFuse will run on and access to configure a wildcard entry for that domain in it's root DNS server. In this guide I will use `example.com` as the domain, remember to substitute your domain. ## Create Cluster - Click on the big green "Create" button at the top of the screen - Select "Kubernetes" from the list - Pick a suitable region (normally the one physically closest to you) - Reduce the number of nodes from 3 to 2 - Reduce the "Node Plan" to 1GB Ram/2vCPU - Change the cluster name to `k8s-flowforge` - Hit "Create Cluster" button When the cluster has finished provisioning you should be able to download the `k8s-flowforge-kubeconfig.yaml` file which will allow you to connect to the cluster. ## Install Traefik Prepare values for the Traefik Helm chart by creating a file called `traefik-values.yaml` with the following content: ```yaml service: enabled: true type: LoadBalancer annotations: service.beta.kubernetes.io/do-loadbalancer-enable-proxy-protocol: "true" spec: externalTrafficPolicy: Cluster deployment: replicas: 2 ports: web: port: 8000 expose: default: true exposedPort: 80 protocol: TCP forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" websecure: port: 8443 expose: default: true exposedPort: 443 protocol: TCP http: middlewares: - traefik-force-https@kubernetescrd - traefik-large-body@kubernetescrd # Disable TLS since NLB handles termination tls: enabled: false forwardedHeaders: trustedIPs: - "10.0.0.0/8" proxyProtocol: trustedIPs: - "10.0.0.0/8" ingressClass: enabled: true isDefaultClass: false name: traefik additionalArguments: - "--entryPoints.web.proxyProtocol.insecure=true" - "--entryPoints.websecure.proxyProtocol.insecure=true" - "--entryPoints.web.forwardedHeaders.insecure=true" - "--entryPoints.websecure.forwardedHeaders.insecure=true" providers: kubernetesIngress: enabled: true kubernetesCRD: enabled: true api: dashboard: false insecure: false logs: access: enabled: true fields: headers: defaultMode: keep extraObjects: - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: force-https namespace: traefik spec: headers: customRequestHeaders: X-Forwarded-Proto: "https" - apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: large-body namespace: traefik spec: buffering: maxRequestBodyBytes: 10485760 ``` Install the Traefik with the following command: ```bash helm repo add traefik https://traefik.github.io/charts helm repo update helm --kubeconfig=./k8s-flowforge-kubeconfig.yaml upgrade --install traefik traefik/traefik \ --create-namespace \ -n traefik \ -f traefik-values.yaml \ --wait \ --atomic ``` ### Setup DNS Run the following to get the external IP address of the Traefik controller ```bash kubectl --kubeconfig=./k8s-flowforge-kubeconfig.yaml \ -n traefik get service traefik ``` You will need to update the entry in your DNS server to point `*.example.com` to the IP address listed under `EXTERNAL-IP` ## Install Cert-manager This will use LetsEncrypt to issue certificates for both the FlowFuse application but also for the Node-RED instances. ```bash helm repo add jetstack https://charts.jetstack.io helm repo update helm install \ --kubeconfig=./k8s-flowforge-kubeconfig.yaml \ cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --version v1.13.3 \ --set installCRDs=true ``` After installing you will need to create a `ClusterIssuer` to access LetsEncrypt. Create the following YAML file called `letsencrypt.yml`, please replace `user@example.com` with your email address. ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: # The ACME server URL server: https://acme-v02.api.letsencrypt.org/directory # Email address used for ACME registration email: user@example.com # Name of a secret used to store the ACME account private key privateKeySecretRef: name: letsencrypt-prod # Enable the HTTP-01 challenge provider solvers: - http01: ingress: ingressClassName: traefik ``` Then use `kubectl` to install this ```bash kubectl --kubeconfig=./k8s-flowforge-kubeconfig.yaml apply -f letsencrypt.yml ``` ## Install FlowFuse Then setup the FlowFuse Helm repository ```bash helm repo add flowforge https://flowfuse.github.io/helm helm repo update ``` Now create a `customizations.yml` file. This is how we configure the FlowFuse instance. The following is the bare minimum to get started: ```yaml forge: domain: example.com https: true localPostgresql: true projectSelector: managementSelector: broker: enabled: true postgresql: global: storageClass: do-block-storage ingress: certManagerIssuer: letsencrypt ``` Again, please replace `example.com` with the domain you configured earlier in the [Setup DNS](https://flowfuse.com/#setup-dns) section Then we use this to install FlowFuse ```bash helm upgrade --install --kubeconfig ./k8s-flowforge-kubeconfig.yaml \ flowforge flowforge/flowforge -f customizations.yml \ --wait ``` Once complete you should be able to sign into the FlowFuse setup wizard at `http://forge.example.com` # Kubernetes Install This guide walks you through a detailed set up of FlowFuse Platform on a container environment managed by Kubernetes. Typically suited for large on premise deployments or deployment in Cloud infrastructure. By the end, you will have a fully functioning FlowFuse instance running on a Kubernetes cluster. # Checklist ::div{.grid.grid-cols-2.gap-8} :::div{.checklist} Prerequisites ::::div :checklist-item{task="Domain Name"} :checklist-item{task="Kubernetes cluster"} :checklist-item{task="Setup Dedicated Database" type="recommended"} :checklist-item{task="Prepare TLS Certificates" type="recommended"} :::: ::: :::div{.checklist} Installation ::::div :checklist-item{task="Download FlowFuse"} :checklist-item{task="Configure FlowFuse"} :checklist-item{task="Enable HTTPS" type="recommended"} :::: ::: :: ## Prerequisites Before you begin, ensure you have the following: 1. **Domain Name & DNS:** A domain name that you own and can configure DNS settings for (explained in [DNS](https://flowfuse.com/#dns)) 2. **kubectl:** To manage a Kubernetes cluster you will need a copy of the `kubectl` utility. See the [kubectl install docs](https://kubernetes.io/docs/tasks/tools/){rel=""nofollow""} 3. **Helm:** FlowFuse provides the Helm chart to manage platform deployment. Install it by following the instructions on [their website](https://helm.sh){rel=""nofollow""} 4. **Kubernetes Cluster:** The deployment has currently been tested on the following environments: - [AWS EKS](https://flowfuse.com/docs/install/kubernetes/aws-terraform) - [Digital Ocean](https://flowfuse.com/docs/install/kubernetes/digital-ocean) - MicroK8s 5. **Ingress Controller:** Install [Traefik](https://doc.traefik.io/traefik/){rel=""nofollow""} on the cluster. 6. **Cert-Manager:** EMQX requires the CRDs from cert-manager. See the [installation instructions](https://cert-manager.io/docs/installation/){rel=""nofollow""} for details. 7. **EMQX Operator:** This installs the operator that deploys the platform's MQTT broker, which is required whenever the broker is enabled. You must install exactly version 2.2.29 — later versions are not supported. Follow the [installation instructions](https://docs.emqx.com/en/emqx-operator/latest/getting-started/getting-started.html#install-emqx-operator){rel=""nofollow""} and pin the version by adding `--version 2.2.29` to the install command. 8. **Outbound network access:** The platform and the Node-RED instances it hosts need outbound access to a set of hostnames. See [Networking requirements](https://flowfuse.com/docs/install/networking-requirements). For a production-ready environment, we also recommend: - **Database:** Prepare dedicated database on an external database server (see [FAQ](https://flowfuse.com/#how-to-use-external-database-server) for more details) - **TLS Certificate:** Prepare a TLS certificate for your domain and configure FlowFuse platform to use it (see [Enable HTTPS](https://flowfuse.com/#i-would-like-to-secure-the-platform-with-https-how-can-i-do-that)) ### Hardware requirements For a Kubernetes-based deployment, resource requirements depend on the number of FlowFuse and Node-RED instances running. As a baseline, we suggest: Control Plane: At least 2 vCPUs, 4 GB RAM Worker Nodes: Minimum 2 vCPUs, 4 GB RAM per node, 2 nodes for high availability Storage: 20Gb of host storage (for container images), StorageClass of your choice available for Hosted Node-RED instances (optional) Each Node-RED instance you host will use 0.1 CPU cores and 256 MB of memory by default. These parameters can be adjusted in admin area of FlowFuse platform. Keep this in mind when sizing your hardware, especially if you plan to create multiple hosted instances. ### DNS You will need a [wildcard DNS entry](https://en.wikipedia.org/wiki/Wildcard_DNS_record){rel=""nofollow""} pointing to the domain that is used for the project instances. This will need to point to the kubernetes Ingress controller. For example if you want projects to be accessible as `[instance-name].example.com` you will need to ensure that `*.example.com` is mapped to the IP address used by your Kubernetes cluster's Ingress controller. By default the FlowFuse application will be mapped to `forge.example.com` assuming that you set the domain to `example.com`. Notes on how to setup DNS can be found [here](https://flowfuse.com/docs/install/dns-setup). ## Installing FlowFuse ### Add FlowFuse Helm Repository ```bash helm repo add flowfuse https://flowfuse.github.io/helm helm repo update ``` ### Customize Helm Chart All the initial configuration is handled by the Helm chart. This is done by creating a `customization.yml` file that will be passed to the Helm along with the chart. To create `customization.yml` file with a minimal required configuration (replace `example.com` with your domain): ```bash cat < customization.yml forge: entryPoint: forge.example.com domain: example.com https: false localPostgresql: true EOF ``` A full list of all the configuration options can be found in the [Helm Chart README](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#configuration-values){rel=""nofollow""}. ### Label Nodes By default FlowFuse platform expects that Kubernetes nodes have specific labels applied. The main reason behind this approach is to separate core application components from Node-RED instances. You will need to label at least one node to run the management application and one to run the Node-RED Projects: List all nodes in the cluster: ```bash kubectl get nodes ``` Label management nodes: ```bash kubectl label node role=management ``` Label project nodes: ```bash kubectl label node role=projects ``` To override this behavior, you can remove the node selectors with the following entry in the `customization.yml` file which will mean that all pods can run on any nodes. ```yaml [customization.yml] forge: projectSelector: managementSelector: ``` ## Start FlowFuse Platform Once you have the `customization.yml` file created, you can install FlowFuse using our Helm chart. This will automatically create all required objects and start services: ```bash helm upgrade --atomic --install --timeout 10m flowfuse flowfuse/flowfuse -f customization.yml ``` ## First Run Setup The first time you access the platform in your browser, it will take you through creating an administrator for the platform and other configuration options. For more information, follow [this guide](https://flowfuse.com/docs/install/first-run). Once you have finished setting up the admin user there are some [Kubernetes specific items to consider](https://flowfuse.com/#common-questions). ## Upgrade All technical aspects of the upgrade process of Flowfuse application running on Kubernetes and managed by Helm chart are maintained in our repository. Please refer to the [Flowfuse Helm Chart documentation](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#upgrading-chart){rel=""nofollow""} for more details about the upgrade process. ## Common Questions ### I would like to secure the platform with HTTPS, how can I do that? In cloud environments, it is recommended to use a Load Balancer to terminate SSL traffic. However, if you want to use SSL termination on the Kubernetes Ingress Controller, this is possible by using the [Cert-Manager](https://cert-manager.io/docs/){rel=""nofollow""} tool (not part of the FlowFuse Helm chart). Once you have Cert-Manager installed, you can enable TLS support in the `customization.yml` file by specifying the [ClusterIssuer](https://cert-manager.io/docs/configuration/#cluster-resource-namespace){rel=""nofollow""} name: ```yaml [customization.yml] {2} ingress: clusterIssuer: ``` Apply changes with [platform startup command](https://flowfuse.com/#start-flowfuse-platform). #### Using Internal/Private Certificate Authorities If you are issuing your own HTTPS certificates (rather than using a Public Certificate Authority) you will need to tell the Hosted Node-RED Instances to trust this CA. To do this you need to pass base64 encoded CA certificate chain to the helm chart in the `customization.yml`. Details are in the Helm chart [README.md](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#private-certificate-authority){rel=""nofollow""}. ```yaml [customization.yml] {3} forge: privateCA: certs: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk... ``` Assuming the certificate chain is held in `chain.pem` the value would be created by running `base64 -w 0 certs.pem` ### I use Kubernetes Network Policies, how can I configure them? If your cluster uses Network Policies to restrict traffic between namespaces, you'll need to create appropriate policies. Here's an example Network Policy that allows traffic from the `flowforge` namespace (default namespace for Node-RED instances) to the `flowfuse` namespace: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-flowforge-to-flowfuse namespace: flowfuse spec: podSelector: {} ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: flowforge policyTypes: - Ingress ``` You may need to adjust this policy based on your specific network requirements and namespace configuration. ### How to use external database server? FlowFuse platform uses PostgreSQL database to store its data. By default, the Helm chart creates and manages the internal database instance. If you want to use an external database server, you need to edit `customization.yml` file and provide the database connection details: ```yaml [customization.yml] {2} forge: localPostgresql: false # Disable internal database postgresql: host: port: auth: username: password: database: ``` Apply changes with [platform startup command](https://flowfuse.com/#start-flowfuse-platform). Check the [FlowFuse Helm chart documentation](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse#postgresql){rel=""nofollow""} for more details about the parameters that can be configured for the PostgreSQL database. ### How to backup embedded database? If you are using the internal database (value `forge.localPostgresql` set to `true`), you can use Kubernetes [CronJobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/){rel=""nofollow""} to backup the database. Apply below `CronJob` and `PersistentVolumeClaim` definitions to create a backup job which will be executed every day at 23:05 and store the backup in a PVC named `db-backup-pvc`: ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: db-backup-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi --- apiVersion: batch/v1 kind: CronJob metadata: name: postgres-backup spec: schedule: "5 23 * * *" jobTemplate: spec: ttlSecondsAfterFinished: 60 template: metadata: labels: app: flowforge spec: containers: - name: backup image: postgres env: - name: PGPASSWORD valueFrom: secretKeyRef: name: flowfuse-postgresql key: postgres-password command: - /bin/sh - -c - | pg_dump -h flowfuse-postgresql -U postgres -d flowforge -F c -b -v -f /backup/db_backup.dump volumeMounts: - name: backup-volume mountPath: /backup restartPolicy: OnFailure volumes: - name: backup-volume persistentVolumeClaim: claimName: db-backup-pvc ``` ### I would like to invite my team members to the platform with e-mail, how can I do that? FlowFuse platform allows you to invite team members to the platform using their e-mail addresses. To enable this feature, you need to configure the e-mail settings in the `customization.yml` file. Check this [page](https://flowfuse.com/docs/install/configuration#email-configuration) for more details about the parameters. Check [FlowFuse Helm chart documentation](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse#email){rel=""nofollow""} for information where configuration values should be placed in `customization.yml` file. If you use AWS EKS (Elastic Kubernetes Service) and want to use AWS SES (Simple Email Service) for sending e-mails, you need to provide the IAM role with the required permissions to use SES. ```yaml [customization.yml] {6} forge: entryPoint: forge.example.com domain: example.com cloudProvider: aws aws: IAMRole: arn:aws:iam:::role/flowforge_service_account_role email: ses: region: eu-west-1 ``` Apply changes with [platform startup command](https://flowfuse.com/#start-flowfuse-platform). ### I would like to use embedded MQTT broker, how can I do that? Click to expand The FlowFuse Helm chart provides the MQTT broker service. To enable the MQTT broker you need to add the following to the \`customization.yml\` file: \`\`\`yaml \[customization.yml] forge: broker: enabled: true \`\`\` Apply changes with \[platform startup command]\(#start-flowfuse-platform). Check the \[FlowFuse Helm chart documentation]\(https\://github.com/FlowFuse/helm/tree/main/helm/flowfuse#mqtt-broker) for more details about the parameters that can be configured for the MQTT broker. ### I would like to use Kubernetes Persistent storage to store data, how can I do that? Starting with the `2.6.0` release the Pods running the Node-RED Instances have a Persistent Volume mounted on `/data/storage` in which files can be written. These files will persist for the lifetime of the Instance including across Suspend/Resume and Stack upgrades. To enable this feature the following configuration needs to be added to the `customization.yml` file (replace ' :storage-class-name[' with the name of the StorageClass you have in the cluster):] ```yaml [customization.yml] {5} forge: persistentStorage: enabled: true size: 5Gi storageClass: ``` Apply changes with [platform startup command](https://flowfuse.com/#start-flowfuse-platform). ### I would like to use FlowFuse File Storage to store context data, how can I do that? To enable the FlowFuse File Storage component add the following to the `customization.yml` file: ```yaml [customization.yml] forge: fileStore: enabled: true ``` Apply changes with [platform startup command](https://flowfuse.com/#start-flowfuse-platform). Check the [FlowFuse Helm chart documentation](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse#file-storage){rel=""nofollow""} for more details about the parameters that can be configured for the File Storage. ### I would like to run FlowFuse on AWS EKS. Do you have any guidance? Yes, we have a dedicated guide on how to deploy FlowFuse on AWS EKS. You can find it [here](https://flowfuse.com/docs/install/kubernetes/aws). Furthermore, we also provide terraform scripts to automate the deployment process of all required AWS service. You can find the guide [here](https://flowfuse.com/docs/install/kubernetes/aws-terraform). # Ingress Controller Migration ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} This guide should not be treated as a one-size-fits-all solution. Consider it as a blueprint and adapt it to your specific Kubernetes cluster setup. Test the migration in a testing/staging environment before applying it to production. If you have any questions about the migration, please contact . ::: :: This document describes how to migrate a FlowFuse Platform Kubernetes deployment from the NGINX Ingress Controller to Traefik. The main reason for this migration is [the retirement of Ingress NGINX](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/){rel=""nofollow""} by the Kubernetes project due to long-term maintenance and security sustainability challenges. Best-effort maintenance continues only until March 2026, after which there will be no further releases, bug fixes, or security updates. It describes a DNS-based migration approach where the actual traffic switch happens at the domain level by changing DNS records to point to the new ingress controller. It is based on the FlowFuse Ingress Migration Tool and using the ingress classes separation capability provided by the FlowFuse Helm chart: - Hosted Instances (project ingresses) via `forge.projectIngressClassName` - Core application components via `ingress.className` That separation allows you to migrate project traffic and core application traffic in stages. ## Prerequisites Before you begin, ensure you have the following: 1. A working FlowFuse deployment installed with the `flowfuse/flowfuse`Helm chart - note down the FlowFuse Platform Helm release name and namespace, guide uses `` and `` placeholders for these values. 2. Access to the Helm values file used for your deployment - note down the path to the values file, guide uses `` placeholder as a reference 3. `kubectl` and `helm` configured for the target cluster 4. A new Traefik ingress controller installed alongside the existing NGINX ingress controller. Check our [ingress installation guide](https://flowfuse.com/docs/install/kubernetes/aws/#ingress-controller){rel=""nofollow""} for instructions on how to do this. 5. Access to the DNS records for: - the FlowFuse application domain - the wildcard domain used by Hosted Instances 6. A maintenance window We also recommend the following before starting the migration: - Reduce DNS TTL values in advance to minimize propagation delay - Do not create new Hosted Instances during the migration window - Test the procedure in a non-production cluster first ## Important notes The FlowFuse Helm chart provides an ingress migration job controlled by the `ingressMigration` values block. The migration job operates on: - the project namespace (`forge.projectNamespace`) - the release namespace, but only in `copy` mode This guide therefore separates the process into two phases: 1. Copy existing ingress resources to the new ingress class 2. Clean up old ingress resources after traffic has moved ## Step 1: Install Traefik alongside NGINX Install Traefik without removing the existing NGINX ingress controller. Check our [ingress installation guide](https://flowfuse.com/docs/install/kubernetes/aws/#ingress-controller){rel=""nofollow""} for instructions on how to do this. Before continuing, verify that both ingress controllers are running. Also, write down the ingress class name used by Traefik, as you will need it for the migration configuration: ```bash kubectl get ingressclass ``` ## Step 2: Record the Traefik LoadBalancer address You will need the public address of the new ingress controller before changing DNS. For example, list services in the Traefik namespace: ```bash kubectl get svc -n traefik ``` Record the external hostname or IP address of the Traefik LoadBalancer service. ## Step 3: Run the migration job in dry-run mode Start by enabling the migration tool in `copy` mode with `dryRun: true`. Update your values file: ```yaml ingressMigration: enabled: true mode: copy dryRun: true newIngressClassName: traefik nameSuffix: -tfk ``` Then run the Helm upgrade: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` After the upgrade completes, inspect the Job created by the chart in the release namespace. The Job name is generated by Helm, so first list the Jobs: ```bash kubectl get jobs -n ``` Then inspect the logs of the migration job: ```bash kubectl logs -n job/ ``` Verify that the dry run identifies all Ingress resources. ## Step 4: Optionally scale down Traefik before copying resources If your environment allows it, and especially if you have many ingress resources, you can temporarily scale Traefik down before executing the ingress resources copy step. This is optional, but it can reduce repeated Traefik configuration reloads during the migration. Example: ```bash kubectl -n traefik scale deployment traefik --replicas 0 ``` Only do this if your Kubernetes cluster hosts the FlowFuse Platform and stopping Traefik will not cause a downtime for other applications. ## Step 5: Copy ingress resources to the new ingress class Once the dry run has been validated, disable dry-run mode and run the actual ingress resources copy. Update your values file: ```yaml ingressMigration: enabled: true mode: copy dryRun: false newIngressClassName: traefik nameSuffix: -tfk ``` Run the Helm upgrade again: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` This creates new Ingress resources that point to the new ingress class and use the configured suffix. ## Step 6: Scale Traefik back up If you scaled Traefik down in step 4, scale it back up now. Example: ```bash kubectl -n traefik scale deployment traefik --replicas 2 ``` Use the replica count that matches your environment. ## Step 7: Change DNS to point to Traefik Update the DNS records used by the FlowFuse Platform so that incoming traffic is sent to the Traefik LoadBalancer address recorded earlier. This includes: - the FlowFuse application hostname (e.g. `forge.example.com`) - the wildcard DNS record used by Hosted Instances (e.g. `*.example.com`) Do not proceed to cleanup until you have confirmed that DNS is resolving to the new ingress controller. ## Step 8: Make new Hosted Instances use Traefik After the copied ingress resources are in place, update the project ingress class so that newly created Hosted Instances use Traefik. Add or update the following value in the FlowFuse Helm values file: ```yaml forge: projectIngressClassName: traefik ``` Apply the change: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` ## Step 9: Wait for DNS propagation and validate traffic Allow time for DNS propagation according to your TTL settings. During this period, validate the migration by checking: - the FlowFuse Platform is reachable - Hosted Instances are reachable - TLS certificates are served correctly - Traefik logs and metrics do not show routing errors - Ingress NGINX logs do not show incoming traffic Do not continue to cleanup until traffic is consistently served by Traefik. Otherwise, you risk potential downtime of the FlowFuse Platform and Hosted Instances. ## Step 10: Move core application components to Traefik Once you are confident that the Traefik ingress controller is handling all the incoming traffic, update the ingress class used by the core application components. Add or update the following value in the FlowFuse Helm values file: ```yaml ingress: className: traefik ``` Apply the change: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` ## Step 11: Optionally scale down NGINX If the NGINX ingress controller is still used for resources outside FlowFuse, leave it in place. If it is no longer needed, you can scale it down after you have confirmed that FlowFuse Platform traffic is fully handled by Traefik. Example: ```bash kubectl -n scale deployment --replicas 0 ``` If your Ingress-Nginx installation created admission webhooks and you are about to retire that controller entirely soon, remove those webhook resources. Examples: ```bash kubectl delete validatingwebhookconfiguration kubectl delete mutatingwebhookconfiguration --ignore-not-found ``` ## Step 12: Clean up old ingress resources After traffic has fully moved to Traefik, switch the migration tool to `cleanup` mode. In this mode, the tool removes old ingress resources and renames the migrated ones back to their original names. This step can cause a short interruption while Kubernetes updates the resources. Update your values file: ```yaml ingressMigration: enabled: true mode: cleanup dryRun: false oldIngressClassName: nginx newIngressClassName: traefik nameSuffix: -tfk ``` Run the Helm upgrade again: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` If you want to validate the cleanup plan first, run the same configuration with `dryRun: true` before applying it. ## Step 13: Remove any remaining suffixed ingress resources from the release namespace The migration job in `copy` mode creates new ingress resources with a suffix in the release namespace. In `cleanup` mode, the job deletes old ingress resources and renames the new ones to match the original names, but it does not remove the suffixed copies from the release namespace. This is to avoid any potential issues with the release namespace if it contains non-ingress resources or custom configurations. Because of that, after cleanup you should inspect the release namespace and remove any remaining suffixed ingress resources that were copied there. List ingress resources in the release namespace: ```bash kubectl get ingress -n ``` If any copied ingress resources with the migration suffix still exist (`-tfk`), delete them manually: ```bash kubectl delete ingress -n ``` ## Step 14: Disable the migration job After the migration and cleanup are complete, disable the migration tool: ```yaml ingressMigration: enabled: false ``` Apply the change one final time: ```bash helm upgrade --install flowfuse/flowfuse -n -f ``` ## Related configuration values This guide uses the following FlowFuse Helm chart values: - `ingress.className` - `forge.projectIngressClassName` - `ingressMigration.enabled` - `ingressMigration.mode` - `ingressMigration.dryRun` - `ingressMigration.newIngressClassName` - `ingressMigration.oldIngressClassName` - `ingressMigration.nameSuffix` For the full list of ingress migration options, refer to the [FlowFuse Helm chart README](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#ingress-migration-tool){rel=""nofollow""}. ## Need help? If you would like guidance on planning this migration, we can provide migration consultation. To discuss this option, contact . ## Resources - [Ingress Nginx retirement announcement](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/){rel=""nofollow""} - [Zero-downtime Ingress controller migration guide](https://georg-schwarz.com/blog/zero-downtime-ingress-controller-migration-kubernetes/){rel=""nofollow""} - [Migrate from Ingress NGINX Controller to Traefik](https://doc.traefik.io/traefik/migrate/nginx-to-traefik/){rel=""nofollow""} # OpenShift Install This guide walks you through detailed set up of FlowFuse Platform on a container envoronment managed by OpenShift. Typically suited for large on premise deployments or deployment in Cloud infrastructure. By the end, you will have a fully functioning FlowFuse instance running on a OpenShift cluster. # Checklist ::div{.grid.grid-cols-2.gap-8} :::div{.checklist} Prerequisites ::::div :checklist-item{task="Domain Name"} :checklist-item{task="OpenShift cluster"} :checklist-item{task="FlowFuse License"} :checklist-item{task="Setup Dedicated Database" type="recommended"} :checklist-item{task="Prepare TLS Certificates" type="recommended"} :::: ::: :::div{.checklist} Installation ::::div :checklist-item{task="Download FlowFuse"} :checklist-item{task="Configure FlowFuse"} :checklist-item{task="Enable HTTPS" type="recommended"} :::: ::: :: ## Prerequisites Before you begin, ensure you have the following: 1. **Domain Name & DNS:** A domain name that you own and can configure DNS settings for (explained in [DNS](https://flowfuse.com/#dns)) 2. **oc:** To manage a OpenShift cluster you will need a copy of the `oc` utility. Instructions on how to install `oc` can be found [here](https://docs.openshift.com/container-platform/4.17/cli_reference/openshift_cli/getting-started-cli.html){rel=""nofollow""} 3. **Helm:** FlowFuse provides the Helm chart to manage platform deployment. Installation can be done through the instructions on [their website](https://helm.sh){rel=""nofollow""} 4. **OpenShift Cluster:** an OpenShift cluster instance with at least two worker nodes 5. **Ingress Controller:** [The Traefik](https://doc.traefik.io/traefik/){rel=""nofollow""} installed on the cluster. 6. **FlowFuse License:** A valid FlowFuse license key is required to run on OpenShift. You can request a quote [here](https://flowfuse.com/pricing/request-quote/){rel=""nofollow""} For a production-ready environment, we also recommend: - **Database:** Prepare dedicated database on a external database server (see [FAQ](https://flowfuse.com/docs/install/kubernetes#how-to-use-external-database-server) for more details) - **TLS Certificate:** Prepare TLS certificate for your domain and configure FlowFuse platform to use it (see [Enable HTTPS](https://flowfuse.com/docs/install/kubernetes#i-would-like-to-secure-the-platform-with-https-how-can-i-do-that)) ### DNS A [wildcard DNS entry](https://en.wikipedia.org/wiki/Wildcard_DNS_record){rel=""nofollow""} will be needed to point to the domain that is used for the project instances. This will need to point to the Ingress controller. For example if you want projects to be accessible as `[instance-name].example.com` you will need to ensure that `*.example.com` is mapped to the IP address used by your OpenShift clusters's Ingress controller. By default the FlowFuse application will be mapped to `forge.example.com` assuming that you set the domain to `example.com`. Notes on how to setup DNS can be found [here](https://flowfuse.com/docs/install/dns-setup). ## Installing FlowFuse ### Create project in the OpenShift cluster To maintain a clean environment, it is recommended to create a new project for the FlowFuse platform: ```bash oc new-project flowfuse --description="FlowFuse Platform" --display-name="FlowFuse" ``` Describe the project to get the SCC information: ```bash oc describe project flowfuse ``` Note the `openshift.io/sa.scc.uid-range` and `openshift.io/sa.scc.supplemental-groups` values. You will need to use these values when customizing the FlowFuse platform installation. In example, if the `openshift.io/sa.scc.uid-range` value is `1000710000/10000`, the `` value will be `1000710000`. ### Add FlowFuse Helm Repository ```bash helm repo add flowfuse https://flowfuse.github.io/helm helm repo update ``` ### Customize Helm Chart All the initial configuration is handled by the Helm chart. This is done by creating a `customization.yml` file that will be passed to the Helm along with the chart. To create `customization.yml` file with a minimal required configuration (replace `example.com` with your domain and `` with the value from the project description collected on [project creation step](https://flowfuse.com/#create-project-in-the-openshift-cluster)): ```bash cat < customization.yml forge: entryPoint: forge.example.com domain: example.com https: false localPostgresql: true cloudProvider: openshift podSecurityContext: runAsUser: runAsGroup: fsGroup: postgresql: primary: podSecurityContext: fsGroup: containerSecurityContext: runAsUser: EOF ``` A full list of all the configuration options can be found in the [Helm Chart README](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#configuration-values){rel=""nofollow""}. ### Label Nodes By default FlowFuse platform expects that worker nodes have specific labels applied. The main reason behind this approach is to separate core application components from Node-RED instances. You will need to label at least one node to run the management application and one to run the Node-RED Projects: List all nodes in the cluster: ```bash oc get nodes ``` Label management nodes: ```bash oc label node role=management ``` Label project nodes: ```bash oc label node role=projects ``` To override this behavior, you can remove the node selectors with the following entry in the `customization.yml` file which will mean that all pods can run on any nodes. ```yaml forge: projectSelector: managementSelector: ``` ## Start FlowFuse Platform Once you have the `customization.yml` file created, you can install FlowFuse using our Helm chart. This will automatically create all required objects and start services: ```bash helm upgrade --atomic --install --timeout 10m flowfuse flowfuse/flowforge -f customization.yml ``` ## First Run Setup The first time you access the platform in your browser, it will take you through creating an administrator for the platform and other configuration options. For more information, follow [this guide](https://flowfuse.com/docs/install/first-run). Once you have finished setting up the admin user there are some [Kubernetes specific items to consider](https://flowfuse.com/#common-questions). ## Upgrade All technical aspects of the upgrade process of Flowfuse application running on Kubernetes and managed by Helm chart are maintained in our repository. Please refer to the [Flowfuse Helm Chart documentation](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#upgrading-chart){rel=""nofollow""} for more details about the upgrade process. ## Common Questions For non-OpenShift specific questions, please refer to the [main kubernetes documentation](https://flowfuse.com/docs/install/kubernetes#common-questions). ### I would like to use embeded MQTT broker, how can I do that? Click to expand The FlowFuse Helm chart provides the MQTT broker service. To enable the MQTT broker you need to add the following to the \`customization.yml\` file (replace the \` :project-uid[\` with the value from the project description collected on \[project creation step\](#create-project-in-the-openshift-cluster)): \`\`\`yaml forge: broker: enabled: true podSecurityContext: runAsUser: :project-uid[runAsGroup: :project-uid[fsGroup: :project-uid[\`\`\` Apply changes with \[platform startup command\](#start-flowfuse-platform). Check the \[FlowFuse Helm chart documentation\](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse#mqtt-broker) for more details about the parameters that can be configured for the MQTT broker. ]]]] ### I would like to use FlowFuse File Storage to store context data, how can I do that? Click to expand To enable the FlowFuse File Storage component add the following to the \`customization.yml\` file (replace the \` :project-uid[\` with the value from the project description collected on \[project creation step\](#create-project-in-the-openshift-cluster)): \`\`\`yaml forge: fileStore: enabled: true podSecurityContext: runAsUser: :project-uid[runAsGroup: :project-uid[fsGroup: :project-uid[\`\`\` Apply changes with \[platform startup command\](#start-flowfuse-platform). Check the \[FlowFuse Helm chart documentation\](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse#file-storage) for more details about the parameters that can be configured for the File Storage. ]]]] # Kubernetes Stacks A Stack defines a set of platform configuration options that will get applied to each Node-RED instance when created. For container based deployment models, this covers three things: - `memory` - the amount of memory (in MB) to limit container to. Recommended minimum: `256`. - `cpu` - a value between 1 and 100 that is the % of a CPU core the container should be allowed to consume. - `container location` - this is the fully qualified name of the container to use. The default container built when following the install instructions is named `flowfuse/node-red:latest` If you wish to use different Node-RED version, you need to specify the name of the container and the version you want to use. For example, if you want to use Node-RED v3.1.x, you should enter `flowfuse/node-red:latest-3.1.x` in the `container location` section of the Stack configuration. Full list of available pre-built containers can be found on [Docker Hub](https://hub.docker.com/r/flowfuse/node-red/tags){rel=""nofollow""}. ## Creating Own Containers As mentioned in the previous paragraph, we encourage to use our pre-built containers in your stacks. However, if you want to create your own container, you can do so by creating a `Dockerfile` and `package.json` files. There is an example `Dockerfile` and `package.json` in the [node-red-container](https://github.com/FlowFuse/helm/tree/main/node-red-container){rel=""nofollow""} directory of the [helm](https://github.com/FlowFuse/helm){rel=""nofollow""} project. This will start with `nodered/node-red:latest` as it's base and then add the required FlowFuse components. Builds of this container for amd64, arm64 and armv7 are built for every release and published to Docker hub as [flowfuse/node-red](https://hub.docker.com/r/flowfuse/node-red){rel=""nofollow""}. These can be used as a base to build custom stacks. If you wanted to pin at Node-RED v3.0.2 you would change the first line to: ```docker FROM nodered/node-red:3.0.2 ARG REGISTRY RUN if [[ ! -z "$REGISTRY" ]] ; then npm config set @flowfuse:registry "$REGISTRY"; fi COPY package.json /data ... ``` To add nodes to the default image you can extend the supplied container. The following Dockerfile will install the node-red-dashboard ```docker FROM flowfuse/node-red WORKDIR /usr/src/node-red RUN npm install node-red-dashboard WORKDIR /usr/src/flowforge-nr-launcher ``` To build the container run the following: ```shell docker build node-red-container/Dockerfile-dashboard -t [your.container.registry]/flowfuse/node-red-dashboard:3.0.2 docker push [your.container.registry]/flowfuse/node-red-dashboard:3.0.2 ``` You would then enter `[your.container.registry]/flowforge/node-red-dashboard:3.0.2` in the `container` section of the Stack configuration. Stacks can be changed on a per Node-RED instance basis, see also the [user stack documentation](https://flowfuse.com/docs/user/changestack). # Networking requirements This page lists the outbound access a self-hosted FlowFuse installation needs. It applies to both the Docker and Kubernetes installations. The access is not all needed from the same place. Each table names who has to reach the destination: - **Platform** is the host, or cluster, running the FlowFuse application. - **Instances** are the hosted Node-RED instances and the remote instances running the Device Agent. - **Editor browsers** are the machines people open the Node-RED editor on. Two related lists live elsewhere: - Inbound access to the platform is ports `80` and `443`. See [Docker install](https://flowfuse.com/docs/install/docker#requirements) and [DNS setup](https://flowfuse.com/docs/install/dns-setup). - The Device Agent has its own list, which includes FlowFuse Cloud endpoints. See [Device Agent networking requirements](https://flowfuse.com/docs/device-agent/install/overview#networking-requirements). All destinations below are outbound TCP. ## Always required | Destination | Port | Needed by | Purpose | | ---------------------------------------------------------------------------- | ---- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `registry.npmjs.org` | 443 | Instances | Installing Node-RED and node packages | | `catalogue.nodered.org` | 443 | Editor browsers | The node catalogue listed in the editor palette. The editor fetches it directly, the platform does not | | `registry-1.docker.io`, `auth.docker.io`, `production.cloudflare.docker.com` | 443 | Platform | Pulling the FlowFuse and Node-RED container images, at install and at every upgrade. Not needed if you pull the images from an internal registry or a pull-through proxy instead | ## Required for specific features | Destination | Port | Needed by | Required when | | ------------------------------------------------------------ | ---------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `registry.flowfuse.com`, `ff-certified-nodes.flowfuse.cloud` | 443 | Editor browsers and instances | FlowFuse Certified Nodes are in use, which needs a certified nodes token set on the platform | | `app.flowfuse.com` | 443 | Platform | The public blueprint library is imported daily, which is the default on a licensed install (`blueprintImport.enabled`) | | `expert.flowfuse.com` | 443 | Platform | FlowFuse Expert or the in-editor assistant is enabled (`ai.enabled`). Both are proxied by the platform, so instances and browsers do not need this | | `expert-broker.flowfuse.com` | 8883 | Platform | FlowFuse Expert is enabled on Kubernetes, where the Helm chart sets this broker as the default. On Docker there is no default, the broker has to be set explicitly | | `github.com`, `api.github.com` | 443 | Platform | A GitOps pipeline pushes to GitHub | | `dev.azure.com` | 443 | Platform | A GitOps pipeline pushes to Azure DevOps | | Your own Git server | 443 | Platform | A GitOps pipeline pushes to any other HTTPS Git server, for example GitLab, Bitbucket, Gitea or a self-hosted one | | `www.googleapis.com` | 443 | Platform | Google SSO is configured | | `acme-v02.api.letsencrypt.org` | 443 | Platform | Certificates are issued automatically, rather than supplied by you | | Your SMTP relay | 587 or 465 | Platform | Email is configured, for invitations, password resets and notifications | | `ping.flowfuse.com` | 443 | Platform | Anonymous usage telemetry is enabled, which is the default and [cannot be disabled on a licensed installation](https://flowfuse.com/docs/admin/telemetry#configuring-telemetry) | Blocking telemetry does not stop the platform. The post runs as a background task, once a day and once shortly after startup, and a failure is written to the platform log and left until the next run. ## Restricted networks A hostname allowlist covers most Node-RED nodes, but not all of them. A minority of packages download native components while installing, from hosts such as GitHub releases or S3 buckets, which cannot be listed in advance. In practice you find out which packages do this when an install fails. There is no small workaround for those packages. The options are an intercepting proxy, or building the packages and their components and hosting them yourself. Both are a significant piece of work. Plan for a small number of nodes being unavailable rather than for full coverage. If installs have to come from inside your own network, point FlowFuse at your own services: - An internal npm registry and your own node catalogue. See [3rd party npm registries](https://flowfuse.com/docs/user/custom-npm-packages#npm-registries). - An internal container registry, or a pull-through proxy, for the FlowFuse and Node-RED images. On Kubernetes, also see [Network Policies](https://flowfuse.com/docs/install/kubernetes#i-use-kubernetes-network-policies-how-can-i-configure-them). # Migrating a Node-RED application to FlowFuse This guide will help you to move existing Node-RED instances into FlowFuse. When migrating your Node-RED instances into FlowFuse, you'll export the flows, credentials, and environment variables. Before you start ensure you can log in to FlowFuse Cloud or your own FlowFuse server, and that you have created your Node-RED instance that you wish to move into FlowFuse. If you have not yet created a Node-RED instance that you want to manage within FlowFuse, you can create the new instance within FlowFuse directly, and the following instructions will not apply in your case. ## Migrating the flows and credentials Install the Node-RED tools plugin as explained [in the documentation](https://flowfuse.com/docs/migration/node-red-tools). After you created a snapshot for the Node-RED instance you wish to move, you'll have copied over the flows and credentials. ## Migrating Environment Variables You can use [this flow](https://flows.nodered.org/flow/8ebfe9ae218aa5105e7da13db14ac272){rel=""nofollow""} to dump a list of your environment variables into the debug window. For each variable needed for the flow should be added on FlowFuse under Instance > Settings > Environment tabs. ## Starting the snapshot Under the snapshots tab, click 'Restore Snapshot' in the kebab menu. The migrated flows will now be started with the modules installed. ## Limitations ### Static Files Check your `settings.js` file to see if `httpStatic` has been set, if so then check for any files in this path. The files in this path need to be manually migrated to [FlowFuse's Static Assets](https://flowfuse.com/docs/user/static-asset-service/) service. # FlowFuse Node-RED Tools plugin The Node-RED Tools Plugin is a module you can install into any Node-RED instance running outside of FlowFuse, that gives you the ability to work on your flows locally. The current version of the plugin allows you to create a new Instance Snapshot using the flows you have locally and push them into an instance on FlowFuse. This can make it easier to develop hardware-specific flows locally, that can then be pushed out to your devices through FlowFuse. ## Install This plugin can be installed through the Manage Palette option by searching for `@flowfuse/nr-tools-plugin` in the Node-RED editor, or on the command-line: ```bash cd ~/.node-red npm install @flowfuse/nr-tools-plugin ``` This assumes the default location of the Node-RED user directory. If you are not sure where that is, check the log output when Node-RED starts as it will log the full path to the `User directory`. ## Usage This initial version of the plugin allows you to create a snapshot of your locally developed flows and push them into one of your instances running inside FlowFuse. ### Connecting to FlowFuse Before you can do anything, you need to connect the plugin to a FlowFuse platform. 1. In the Node-RED editor, open the FlowFuse Tools sidebar and click on the cog icon to open the settings panel. 2. Enter the url of your FlowFuse platform. For example, if you have signed-up to [FlowFuse Cloud](https://app.flowfuse.com/){rel=""nofollow""} then use the URL `https://app.flowfuse.com`. 3. Click connect. This will open another window where you can log in to FlowFuse and give permission for the plugin to connect to your account. 4. Once connected, close the settings panel. ### Working with Snapshots The FlowFuse Tools sidebar allows you to browse the teams you are a member of and their instances. When you select an instance, the sidebar lists its snapshots. You can then create a new snapshot using the flows you have running locally. ### Create a Snapshot 1. Click the `+ snapshot` button to open the Create Snapshot dialog. 2. In the dialog, enter a name for the snapshot and an optional description. 3. The dialog lists the modules your flows are using, along with the version number. This information is included in the snapshot when sent back to the FlowFuse instance. Check the notes below on how this is handled within FlowFuse. 4. Click 'Create Snapshot' At this point, a new snapshot will be created in FlowFuse. You can then switch to the FlowFuse platform and from the Snapshots menu either select the 'Restore Snapshot' option to deploy that snapshot, or set it as the Device Target to deploy it to your devices. We'll be working on improving this workflow in future releases of the plugin - to allow you to manage more from within the Node-RED plugin. #### Adding modules to a snapshot The snapshot created by the sidebar includes a list of the modules used by the flows. If there are any modules included that have not already been added to your instance you will need to manually add them via the `Instance Settings -> Palette -> Installed Modules` view in the platform. #### Defining Environment Variables We do not currently support defining environment variables for the flows from within the Node-RED Plugin. This means that when you create a snapshot from the plugin, the platform will automatically merge in the currently defined environment variables for that instance. To manage your instance's environment variables, use the `Instance Settings -> Environment` view in the platform. # Foundations The handful of concepts you need to build with Node-RED, and how they fit together. You wire pre-built nodes into flows and spend your effort on the logic — what the system should do — while the platform handles the syntax and the connectivity. Learn these and you can build real integrations in Node-RED with only a little JavaScript — the structure carries most of the weight. ## How it fits together ::flow-diagram --- edges: - msgin>transform - transform>route - route>sink - from: route to: context dir: both accent: green label: read / write state nodes: - id: msgin label: Message in sub: http in / inject - id: transform label: Node sub: transform - id: route label: Node sub: route - id: sink label: Sink sub: no routing - id: context label: Context sub: shared state accent: green col: 3 row: 2 --- :: A **message** enters a **flow** and passes from **node** to node, transformed along the way, until it reaches a sink that sends and routes nothing. Reuse comes from **subflows** and **link nodes**; shared state lives in **context**; new capabilities come from installing nodes off the **palette**. ## What it connects to Node-RED's reach comes from its nodes: install one for a protocol or service and the instance can talk to it. The same runtime — a Hosted Instance in the cloud or a Remote Instance on your own hardware — reaches field hardware, databases, message buses, cloud services and other systems. And **any instance can serve its own Dashboard** for the people who use it. ::arch-diagram --- edges: - from: inst to: dash label: serves - from: inst to: db dir: both - from: inst to: mqtt dir: both - from: inst to: cloud dir: both - from: inst to: plc dir: both - from: inst to: io dir: both - from: inst to: gw dir: both - from: inst to: api dir: both nodes: - id: dash label: Dashboard sub: its own operator UI accent: blue col: 1 row: 1 - id: db label: Databases sub: SQL · time-series accent: green col: 2 row: 1 - id: mqtt label: Brokers sub: MQTT · UNS accent: teal col: 3 row: 1 - id: cloud label: Cloud services sub: AWS · Azure · GCP accent: blue col: 4 row: 1 - id: inst label: Instance sub: hosted or remote — same runtime accent: indigo span: 2 col: 2 row: 2 - id: plc label: PLCs sub: controllers col: 1 row: 3 - id: io label: Sensors & IO sub: signals col: 2 row: 3 - id: gw label: Gateways sub: protocol bridges col: 3 row: 3 - id: api label: HTTP / APIs sub: REST · services accent: blue col: 4 row: 3 --- :: ## The core pieces - **Node** — A single processing block: it receives a message, does one thing — read, transform, call, route — and passes it on. - **Flow** — Nodes wired left-to-right on a tab; a message enters, is transformed, and exits. One working unit of automation. - **Message (msg)** — The object that travels the wires, carrying **msg.payload** plus metadata between nodes. - **Subflow** — A block you define once and drop in many places, with its own inputs, outputs and per-instance config. - **Link nodes** — link in / link out route messages across tabs with no visible wires; **link call** is the one that *returns* — your in-process service call. - **Context** — Storage that keeps state between messages — flow and global scope, in memory or persisted. - **Palette** — The library of installable nodes (npm) you add new capabilities from. - **Editor & runtime** — The browser editor where you wire flows, and the runtime that executes them continuously. ::callout{icon="i-lucide-book-open"} **In the Node-RED docs** — that's the working model. For the full glossary — every core term and node type, straight from the Node-RED project — see the [official Node-RED documentation](https://nodered.org/docs/user-guide/concepts){rel=""nofollow""} instead of a glossary here. :: # Designing Node-RED flows Turn an architecture sentence into a clean flow shape you can read at a glance. **Node-RED — start here** Node-RED is a visual programming platform for integration and logic: you build by wiring pre-built nodes into flows. The connectors and the syntax are handled for you, so your effort goes into what the system should do — the logic — not the plumbing to connect things or the boilerplate of a language. ::callout{icon="i-lucide-flag"} **New here? Start with the [Foundations →](https://flowfuse.com/docs/node-red-guide/foundations/)** :: ::callout{icon="i-lucide-sparkles"} **Writing flows? Keep to [Good form →](https://flowfuse.com/docs/node-red-guide/patterns/good-form/)** — the habits that keep a flow readable and out of spaghetti: call shared things, one path per beginning, decouple UI from logic, catch errors where you can see them. :: ## The patterns, by family ### [Design patterns](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/) - **Find the seams** — Name the three or four components hiding in a flow. - **Levels of reuse** — Reuse each piece at the lightest rung: link in/out, link call, subflow, or packaged node. ### [Handling data (PLC)](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/) - **Classify the data** — Name each signal by shape, purpose, and direction. - **Separate the paths** — Give telemetry and control their own routes. - **Batch / rate-limit** — Pace a fast source into a slow sink so memory doesn't blow. - **Hold state in context** — Keep a logical object in context, not threaded through wires. - **Config** — Static in env vars, runtime in persisted context. ### [Worked examples](https://flowfuse.com/docs/node-red-guide/worked-examples/) - **OEE - Edge Aggregator** — the edge piece as a reusable subflow: poll, read, compute, publish. - **OEE - Central Dashboard** — subscribe, compute, dashboard, fanning out via link out to a second link in for batched history. ::callout{icon="i-lucide-arrow-right"} **Ready to build one?** These are the shapes a flow should take. [Using FlowFuse](https://flowfuse.com/docs/user/) covers working with instances, snapshots and the editor itself. :: # Design patterns **Design patterns — pick how a flow is structured and reused** A design pattern is a structural choice you make for a piece of a flow. Building a flow you'll select one or more: first **find the seams** — the components the flow really breaks into — then, for each piece worth reusing, pick the **level of reuse** that fits. Reuse is a ladder — link in/out → link call → subflow → palette node — and each rung up buys more reuse but costs more to build and maintain, so climb only when the rung below won't do. ::guide-tabs :::guide-tab{label="Find the seams"} **Name the pieces first** — before any reuse, find the components hiding in the flow and draw a box around each. Most spaghetti is three or four well-defined components that were never named. A giant flow is bad because it has no seams: you can't reuse, test, or hand off part of a blob, and any edit means reading the whole tab. Naming the structure is the fix — not tidier wires. ::::flow-diagram --- edges: - ingest>normalize - normalize>enrich - enrich>publish nodes: - id: ingest label: Ingest accent: indigo - id: normalize label: Normalize accent: indigo - id: enrich label: Enrich accent: indigo - id: publish label: Publish accent: indigo --- :::: Look for one of these and box it off: - **A repeated cluster** — the same three or four nodes appearing in more than one place. - **A logical stage** — ingest, normalize, enrich, publish; each a bounded step with a clear input and output. - **A bounded responsibility** — one thing the piece owns from end to end. - **A reuse magnet** — a piece other flows will obviously want. Once the seams are named, each becomes a candidate for reuse — and *how* you reuse it is the choice in the tabs that follow. A piece you'll only ever use once still earns its box; it just stays a plain seam. ::: :::guide-tab{label="Link in / out"} **Route within one instance** — *local · organization, not reuse.* Link in and link out route messages between points and tabs inside a single Node-RED instance, without dragging a wire across the canvas. One link out can feed many link ins — **one producer, many consumers** — so it's the clean way to fan a message out to several independent paths. ::::flow-diagram --- edges: - prod>lout - from: lout to: lin1 dashed: true label: link - from: lout to: lin2 dashed: true - from: lout to: lin3 dashed: true - lin1>c1 - lin2>c2 - lin3>c3 legend: - line: neutral dashed: true label: link out → link in nodes: - id: prod label: producer sub: one source accent: indigo col: 1 row: 2 - id: lout label: link out sub: broadcast accent: indigo col: 2 row: 2 - id: lin1 label: link in accent: indigo col: 3 row: 1 - id: c1 label: consumer A accent: slate col: 4 row: 1 - id: lin2 label: link in accent: indigo col: 3 row: 2 - id: c2 label: consumer B accent: slate col: 4 row: 2 - id: lin3 label: link in accent: indigo col: 3 row: 3 - id: c3 label: consumer C accent: slate col: 4 row: 3 align: left --- :::: Nothing is packaged or reused, though — the message just teleports to the matching link. **Select it when** — you need to tidy wiring, route between tabs, or fan one message out to several consumers. It's the bottom rung: routing and organization, no reuse of logic. **Not this when** — you're trying to share *work*. The moment more than one path needs the same logic or resource, climb to a link call. **Stays local** — link in/out never leave the instance and distribute nothing. ::: :::guide-tab{label="Link call"} **A shared, returning service** — *local · one instance.* Turn a link-in / link-out pair into a callable service: a piece of work exposed once that any path calls and that returns the result to whoever called it, with no central router to build. It's the workhorse for shared services inside one instance — a database pool, a model call, a broker connection, a common transform. ::::flow-diagram --- edges: - in>lc - lc>resp - lin>pool - pool>lout - from: lc to: lin dashed: true label: call accent: teal - from: lout to: lc dashed: true label: return accent: teal groups: - id: svc label: Shared service · every path calls this one link in accent: green nodes: - lin - pool - lout legend: - line: teal dashed: true label: call / return nodes: - id: in label: http in sub: a request accent: indigo col: 1 row: 1 - id: lc label: link call sub: calls the service accent: teal col: 2 row: 1 - id: resp label: http response sub: the end · sends result accent: indigo col: 3 row: 1 - id: lin label: link in sub: service entry accent: green col: 1 row: 2 - id: pool label: SQL pool sub: one shared connection accent: green col: 2 row: 2 - id: lout label: link out sub: return mode accent: green col: 3 row: 2 align: left --- :::: **Select it when** — more than one path needs the same resource or logic and you don't want a copy per path. Each path calls the one shared service, the result comes back, and the path finishes at its own sink — an http response here. No funnel: the shared service has exactly one wire in. **The trade-off** — every caller shares *one* configuration. That's the point when the config is fixed; it's the limit when each use needs its own settings — which is when you climb to a subflow. **Stays local** — reuse within one instance; it doesn't package or distribute. ::: :::guide-tab{label="Subflow"} **A packaged set of actions, with its own config** — *cross-instance.* A subflow bundles a *set of actions* — several nodes — into one reusable node you can drop into a flow many times, each instance carrying its own configuration. Unlike a link call, which shares one fixed config, a subflow is a packaged assembly that varies per drop, and it travels between Node-RED instances. ::::flow-diagram --- edges: - a1>a2 - a2>a3 - from: def to: uses dashed: true label: packaged as one node groups: - id: def label: Subflow · a set of actions packaged into one node, authored once accent: teal nodes: - a1 - a2 - a3 - id: uses label: Reused · that one node dropped in, each with its own config accent: indigo nodes: - n1 - n2 - n3 nodes: - id: a1 label: validate col: 1 row: 1 - id: a2 label: transform col: 2 row: 1 - id: a3 label: publish col: 3 row: 1 - id: n1 label: subflow sub: config A accent: indigo col: 1 row: 2 - id: n2 label: subflow sub: config B accent: indigo col: 2 row: 2 - id: n3 label: subflow sub: config C accent: indigo col: 3 row: 2 align: left --- :::: **Select it when** — a piece needs per-instance config (the same logic, different settings each place), or you need to reuse it across other Node-RED instances. Start as a link call and promote to a subflow only once it earns that. **The trade-off** — heavier than a link call: each instance duplicates its connections, and defaulting to subflows for everything adds indirection. Use it for genuine per-instance reuse, not as the default. ::: :::guide-tab{label="Palette node"} **Real packaged code** — *distributed.* The top rung: package a piece as an installable palette node — real code, versioned and installed like any library dependency. It's the same **custom node** the [FlowFuse app delivery methods](https://flowfuse.com/docs/application-guide/app-delivery-methods/) publish for reuse across apps, and the point where Node-RED reuse hands off to the FlowFuse guide's distribution story. **Select it when** — a piece is used across many projects or teams and deserves to be versioned, installed, and upgraded like a dependency — not copied, and beyond what a shared subflow can carry. **The trade-off** — it's real code with a real release cycle: a package to build, test, and maintain. The most powerful reuse and the most to own. Don't take this rung until link call and subflow genuinely can't cover it. ::: :: # Good form **Good form — how not to make spaghetti** A clean flow isn't luck — it's a handful of habits. The golden rule: **shared things are *called* rather than funneled into, and every beginning is its own left-justified path.** Everything below follows from that. ::guide-tabs :::guide-tab{label="Wire for reading"} **Call shared things; don't funnel into them** — many paths wiring into one shared node is the core spaghetti anti-pattern. ::::flow-diagram --- edges: - a>call - b>call - c>call - from: call to: svc dir: both label: result back nodes: - 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 / broker / model sub: one dependency accent: green col: 3 row: 2 align: left --- :::: When many paths share a dependency — a DB pool, a broker, a model call — don't wire them all *into* one node (a funnel). Expose it once as a service each path **calls** with a `link call`; the result returns and per-path handling happens after. The shared node keeps exactly one wire in and stays reusable. **The rules** - **No funnels into a shared dependency** — when many paths need the same service, they should *call* it, not all wire into it. (Aggregators like a `join` node that legitimately gather many inputs are the exception — that's their job, not a funnel.) - **Each beginning is its own path** — one straight, left-justified path per entry; don't merge them through a shared front door. - **Flow left → right** — beginning → prep → service call → format → sink. A backward (right-to-left) wire reads as tangle. - **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. ::: :::guide-tab{label="Lay it out"} **One column per role, everything on the grid** — layout is what makes a flow scannable at a glance. - **One entry column** — every beginning (inject, http in, mqtt in) shares one left edge. That column is the visual anchor. - **Columns by role** — beginning → prep → service call → format → sink, left to right, each role in its own column. - **On the grid, no overlaps** — snap to the 20px grid, leave a gap between neighbours, and never let two nodes (or two groups) overlap. - **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. - **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. ::::flow-diagram --- edges: - begin>prep - prep>call - call>fmt - fmt>sink nodes: - 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 align: left --- :::: ::: :::guide-tab{label="Decouple UI from logic"} **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. ::::flow-diagram --- edges: - from: fe to: be label: intent · { action, payload } - from: be to: fe label: view-model · display-ready dashed: true - be>db legend: - line: neutral label: intent · frontend → backend - line: neutral dashed: true label: view-model · backend → frontend nodes: - 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 --- :::: 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. - **Reads** — the backend sends a finished, display-ready view-model. Templates bind and display; they never fetch, transform, or decide. - **Writes** — the widget emits one consistent intent message: an action plus a payload. The backend decides what it means. - **One place for state** — hold state in one shared `global` object the widgets read. The wire carries events, not fat objects, and never a live subscription per widget. - **`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 (`height="0"`) so content isn't clipped, and remember tables **replace, not append**. - **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. ::: :::guide-tab{label="Catch errors visibly"} **Every work path has a catch** — otherwise errors drop silently and you're debugging blind. ::::flow-diagram --- edges: - 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 legend: - line: neutral label: success - line: red dashed: true label: Catch scope · not a wire nodes: - 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 --- :::: 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.) - **No work without a catch** — a tab with function / request / DB / link-call / AI nodes and no Catch node drops its errors silently. - **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. - **In a shared service, format and return the error** — return it via `link out` in return mode so the caller sees `msg.error` and a bad call never hangs. ::: :::guide-tab{label="Keep data on a contract"} **Swap a source by keeping the message shape** — good seams mean a data change touches one node, not the whole flow. - **Stable msg contract** — a query returns rows on the same property; the broker path is `msg.topic` + `msg.payload`. Keep the shape and only one node changes when you swap the source behind it. - **SQL goes on the property your node reads** — the Postgres query node reads `msg.query` (with `msg.params` for parameters); the mysql / sqlite nodes read `msg.topic`. Either way it's never `msg.payload` — put SQL on the wrong property and the query silently runs empty. - **Parameterize** — use parameterized queries and quote case-sensitive identifiers; don't string-build SQL into the payload. - **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 `msg` through. ::: :: ::callout{icon="i-lucide-check"} 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. :: # Handling data **Handling data — classify what you've got, then pick your methods** A flow's data isn't one thing. First **classify** each signal — by shape, purpose, and direction — then select the handling methods each kind needs. Building a flow you'll usually reach for more than one: separate the paths, pace the flow, hold state in context, manage config. Start in the first tab; the rest are the methods you choose from. ::guide-tabs :::guide-tab{label="Classify the data"} **Know what kind of data you're handling** — before you pick any method, name each signal by its shape, its purpose, and its direction. What a signal *is* decides how you move it, how fast, and where it's stored. Mixing kinds — and polling everything at the fastest rate — is what creates load a controller can't sustain. Every point you touch is some combination of the three: **By shape — is it an event or a stream?** - **Event** — something happened at a moment: a button press, a state change, a fault, a batch complete. Discrete and irregular, and each one matters on its own. You handle it *when it fires* — you don't poll for it. - **Stream** — a continuous series of readings sampled on a clock: temperature, flow, level, vibration. Regular and high-volume, where the latest value usually matters more than any single earlier one. You read it *at a cadence* and often only keep the trend. **By purpose — what is the value for?** ::::flow-diagram --- edges: - from: src to: telemetry - from: src to: control - from: src to: config nodes: - id: src label: a signal sub: event or stream accent: slate col: 1 row: 2 - id: telemetry label: Telemetry sub: observe & trend · latency-tolerant accent: green col: 2 row: 1 - id: control label: Control sub: drives a decision · time-critical accent: red col: 2 row: 2 - id: config label: Config sub: shapes the app · written rarely accent: slate col: 2 row: 3 align: left --- :::: - **Telemetry** — readings you observe and trend. Continuous and latency-tolerant: a second late only moves a timestamp. - **Control** — a value that drives a decision or an actuator. Small and time-critical: a second late changes an outcome. - **Config** — a setting that shapes how the app runs. Written rarely, read often. **By direction — read or write?** Reading a value out of a device and writing one back into it are not the same cost or risk. Writes touch the process; treat them with more care and a tighter path than the reads you take for observation. **Put it together** — for each point, ask: event or stream? Does a delay change a decision or only a timestamp? Read or write? Those three answers set its rate, its path, and its store — and point you at the methods in the next tabs. The common mistake is treating every point as one population and polling all of it at the fastest rate; usually only a handful genuinely need the fast path, and the rest just starve the ones that do. ::: :::guide-tab{label="Separate the paths"} **Telemetry one way, control another** — give each kind its own route so the fast path never inherits the slow one's load. *Select when — a flow carries both telemetry and control (from Classify).* This split is the foundational method; the pacing and storage choices all follow from it. ::::flow-diagram --- edges: - from: plc to: nodered label: reads - from: nodered to: broker label: live - from: nodered to: ctrl label: control write accent: red - from: nodered to: sqldb label: history, batched nodes: - id: plc label: PLC sub: reads · writes col: 1 row: 2 - id: nodered label: Node-RED sub: splits by kind accent: slate col: 2 row: 2 - id: broker label: Broker sub: live telemetry · minimal set accent: teal col: 3 row: 1 - id: ctrl label: actuator / setpoint sub: control · lean, dedicated accent: red col: 3 row: 2 - id: sqldb label: SQL database sub: history telemetry · batched accent: green col: 3 row: 3 align: left --- :::: Telemetry and control have different timing needs. Share one route and one protocol, and the fast, time-critical path inherits the load of the slow, high-volume one — a burst of telemetry can delay a control decision. **How** — buffer telemetry in the source and pull it at its real cadence, timestamped at acquisition, then batch it into history. Keep the control path to the minimum tag set on a dedicated, faster route. Reads you take for observation and writes that touch the process never share a lane. **The payoff** — the fast path stays lean no matter how much telemetry flows, and each kind can be paced and stored on its own terms. ::: :::guide-tab{label="Batch / rate-limit"} **Pace a fast source into a slow sink** — add the brake that a fast-in / slow-out path doesn't have on its own. *Select when — a fast or bursty source (a stream, or high-rate events) feeds a slower sink.* This is the #1 event-driven failure, so reach for it whenever input can outrun output. ::::flow-diagram --- edges: - from: mqtt to: pace label: many msg/s accent: indigo dashed: true - from: pace to: db label: steady rate legend: - line: indigo dashed: true label: fast in - line: neutral label: paced out nodes: - id: mqtt label: MQTT in sub: fast · bursty accent: teal col: 1 - id: pace label: rate limit / batch sub: delay · queue · drop col: 2 - id: db label: DB write sub: slow · the bottleneck accent: green col: 3 --- :::: A fast source (an MQTT topic, a tight poll) feeding a slow sink (a DB write, a remote API) has no natural brake. Messages pile up in memory faster than they drain, and the runtime eventually runs out of heap and dies. **How** — a delay node in rate-limit mode; batching (join into chunks), which also cuts per-write overhead; or dropping stale readings when only the latest matters. Watch heap and the node's queue — if it only grows, you have backpressure, not a spike. **The tell** — a backend holding a live connection (an MQTT subscription) can't be pooled away; pace at the source or offload the heavy work. ::: :::guide-tab{label="Hold state in context"} **One place for state** — keep a logical object in context instead of threading it through wires. *Select when — you're passing the same object through many nodes just to move it, or state must survive a restart.* Messages are verbs; context is nouns. ::::flow-diagram --- edges: - event>node - from: node to: store label: write - from: store to: reader label: read the one key it needs dashed: true legend: - line: neutral label: write / flow - line: neutral dashed: true label: read nodes: - id: event label: event sub: a reading arrived col: 1 row: 1 - id: node label: node sub: recompute col: 2 row: 1 accent: indigo - id: store label: context store sub: assets., oee.line1 col: 3 row: 1 accent: green - id: reader label: another node sub: reads one key col: 2 row: 2 --- :::: Context is shared memory with a defined scope. It holds a logical object in one place instead of threading it through fifteen nodes just to carry it — wire gymnastics to avoid storing a value is the real anti-pattern. **How** — store the object once under a namespaced key at the narrowest scope that works. **Node** scope is private to that one node, so anything *shared* between nodes starts at **flow** scope (then **global** only if it must cross tabs). Each node reads the one key it needs, and a persistent store holds anything that must survive a restart. **Watch out** — one writer per key, serialize concurrent updates, and keep enough on the wire to stay debuggable. ::: :::guide-tab{label="Config"} **Static in env vars, runtime in persisted context** — decide which kind each setting is, then store it accordingly. *Select when — the flow has settings.* The question for each one: does it change per environment at deploy, or while running, by a user? ::::flow-diagram --- edges: - from: ui to: store label: intent - store>flowrun groups: - label: Env var — set at deploy nodes: - broker - label: Persisted context — changed by a user accent: green nodes: - ui - store - flowrun nodes: - id: broker label: BROKER_HOST sub: baked, read-only — edit env + redeploy col: 1 row: 1 accent: slate - id: ui label: UI edits sub: a form or button col: 2 row: 1 accent: indigo - id: store label: context store sub: live config col: 3 row: 1 accent: green - id: flowrun label: flow at run sub: reads current col: 4 row: 1 --- :::: **Static config** changes per environment and is set at deploy — broker host, DB connection. It's resolved at deploy time and read-only to the running flow. **Runtime config** changes while running, by a user, with no redeploy. **How** — keep static config in env vars or a config node; put user-editable config in persisted context (or a config file), edited through the UI via an intent message and read by the flow at execution time. **The rule** — if a value would ever be changed through a button or form, it is not an env var. ::: :: # Patterns Once you have the foundations, these are the moves that turn an architecture into a clean, reusable flow. Work through them in order, or jump to the one you need. - **[Design patterns](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/)** — find the seams, then reuse each piece at the lightest level that solves it: link in/out, link call, subflow, or packaged node. - **[Handling data](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/)** — classify data by what it's for, treat telemetry and control on their own paths, pace fast inputs, and hold state and config in context. - **[Good form](https://flowfuse.com/docs/node-red-guide/patterns/good-form/)** — the habits that keep a flow readable and out of spaghetti: call shared things, decouple UI from logic, catch errors where you can see them. # Worked examples An **app** is a job to do — with a delivery method and a pattern, decided in the [FlowFuse guide](https://flowfuse.com/docs/application-guide/worked-examples/). The **solution** is a Node-RED flow — and an app isn't always a single one. A bigger app is **one or more flows working together**. Each worked example takes a real app and turns it into a flow — showing the [design pattern(s)](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/) and [data-handling method(s)](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/) it selects, and why. The app can be **built entirely by FlowFuse, built together with your team, or built by you** from these examples. The build is the same either way. ## From app to flow Turn the app into a flow the same way each time — two selections, then keep to good form: 1. **Start from the app** — its one job, and (from the [FlowFuse guide](https://flowfuse.com/docs/application-guide/app-delivery-methods/)) how it's delivered and what shape it takes. 2. **Break it into flows** — most apps are a single flow; a bigger one is a few, each doing a clear part. 3. **Select the [design pattern(s)](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/)** — find the seams, then pick a reuse level for each piece: link in/out, link call, subflow, or packaged node. A flow often uses more than one — **or none: a simple, one-path flow may need no pattern at all, and that's fine.** 4. **Select the [data-handling method(s)](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/)** — this part is never skippable. Even a simple flow with no pattern still has to **classify** what it's getting (event or stream?) and decide whether it needs **batching / rate-limit**, separate paths, context, or config. Then keep it in [good form](https://flowfuse.com/docs/node-red-guide/patterns/good-form/) — the general habits (call shared things, one path per beginning, a single sink, catch errors) that keep any flow readable. ## The examples The FlowFuse [OEE worked example](https://flowfuse.com/docs/application-guide/worked-examples/oee/) breaks that use case into two apps. Here's how each one looks as a Node-RED flow, shaped by the patterns it uses. ::callout{icon="i-lucide-arrow-right"} **[OEE - Edge Aggregator →](https://flowfuse.com/docs/node-red-guide/worked-examples/oee-edge-aggregator/)** — the edge piece, packaged as a reusable subflow: poll → read → compute → publish, one per line. :: ::callout{icon="i-lucide-arrow-right"} **[OEE - Central Dashboard →](https://flowfuse.com/docs/node-red-guide/worked-examples/oee-central-dashboard/)** — the cloud app: subscribe → compute → link out, fanning to a link in for the dashboard and a second link in for batched history, with the UI decoupled from the logic. :: More examples will land here — each one takes an app and turns it into a flow, move by move. # OEE - Central Dashboard The cloud app from the FlowFuse [OEE worked example](https://flowfuse.com/docs/application-guide/worked-examples/oee/) — it subscribes to every line's state, computes OEE, shows it live, and writes each reading to history. It runs on one Hosted Instance. Unlike the Edge Aggregator, this one earns a design pattern and more than one data-handling method. ## Definition - **The app** — subscribes to every line's state, computes OEE, shows it live, and writes each reading to history. - **Design pattern** — [link out / link in](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/): the OEE result leaves the calc through one link out; a link in on the dashboard tab and a separate link in on the history tab pick it up — a clean fan-out across tabs with no cross-tab wire. - **Data handling** — [classify](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/) (a live event stream, plus history) → the two link-ins **[separate the paths](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/)** (live vs history) → **batch** the history writes. - **Runs on** — one Hosted Instance. - **Why this shape** — a user-facing app driven by live data, with history kept off the live path so the dashboard never waits on the database. ## The flow ::flow-diagram --- edges: - sub>oee - oee>lout - from: lout to: lindash dashed: true label: link - from: lout to: lindb dashed: true label: link - lindash>dash - lindb>batch - batch>db legend: - line: neutral dashed: true label: link out → link in nodes: - id: sub label: MQTT in sub: subscribe · line state accent: indigo col: 1 row: 1 - id: oee label: compute OEE sub: availability × perf × quality col: 2 row: 1 - id: lout label: link out sub: broadcast the result accent: indigo col: 3 row: 1 - id: lindash label: link in sub: dashboard tab accent: indigo col: 3 row: 2 - id: dash label: dashboard sub: live OEE per line accent: indigo col: 4 row: 2 - id: lindb label: link in sub: history tab accent: indigo col: 3 row: 3 - id: batch label: join / batch sub: N readings or T secs col: 4 row: 3 - id: db label: time-series DB sub: external · history accent: green col: 5 row: 3 align: left --- :: ## Why these choices - **Link out, two link ins on separate tabs** — the OEE result leaves the calc once through a link out; the dashboard tab and the history tab each pick it up through their own link in. Nothing crosses tabs, so each path reads on its own and the split is named — not a wire snaking across the canvas. (For two consumers on the *same* tab a plain node with two outputs would do; the link out/in earns its keep because these live on separate tabs.) - **The link-ins are the path split** — the live dashboard rides one, batched history the other; neither waits on the other. - **Batch the writes** — the history link in feeds a join / batch node, so readings land in the time-series DB in batches, not one insert per reading — cutting per-write overhead. - **Decouple UI from logic** — the compute path builds a display-ready view-model; the widgets render it and emit intent. That's [good form](https://flowfuse.com/docs/node-red-guide/patterns/good-form/), not a selection — it applies to every dashboard. ::callout{icon="i-lucide-check"} **In one line** — subscribe → compute → link out; one link in to the dashboard tab, a separate link in to a batch node then history. :: # OEE - Edge Aggregator The edge app from the FlowFuse [OEE worked example](https://flowfuse.com/docs/application-guide/worked-examples/oee/) — it reads a line's machine signals and publishes its state, one Remote Instance per line. The flow logic is a simple straight line; the choices that matter are packaging it for reuse and treating the data as a stream. ## Definition - **The app** — reads a line's machine signals and publishes its state. - **Design pattern** — [subflow](https://flowfuse.com/docs/node-red-guide/patterns/design-patterns/), applied to the *whole* flow (not to extract an internal seam): the straight-line flow is packaged as one subflow and dropped onto every line, each configured with its own line's PLC tags as per-instance config — the Node-RED side of the [Configurable App](https://flowfuse.com/docs/application-guide/app-delivery-methods/hardware-apps/) pattern. - **Data handling** — [classify](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/) (the signals are a telemetry **stream**, read at the poll cadence) → hold the running run/stop counts in **[context](https://flowfuse.com/docs/node-red-guide/patterns/handling-data/)**. - **Runs on** — a Remote Instance, one per line. - **Why this shape** — the same complete job runs identically on every line, right next to the equipment, and keeps working if the link drops. ## The flow ::flow-diagram --- edges: - from: ui to: cfg label: save tags - from: cfg to: getcfg dashed: true label: load tags - tick>getcfg - getcfg>read - read>calc - calc>pub - from: read to: catch dashed: true accent: red label: catches (scope) groups: - label: Edge Aggregator · one subflow, dropped on every line accent: teal nodes: - ui - cfg - tick - getcfg - read - calc - pub legend: - line: neutral dashed: true label: context read - line: red dashed: true label: Catch scope · not a wire nodes: - id: ui label: config UI sub: set this line's tags accent: blue col: 2 row: 1 - id: cfg label: context sub: tag config accent: green col: 3 row: 1 - id: tick label: inject / timer sub: poll tick accent: indigo col: 1 row: 2 - id: getcfg label: get config sub: load the tags col: 2 row: 2 - id: read label: PLC read sub: reads those tags col: 3 row: 2 - id: calc label: compute state sub: counts in context col: 4 row: 2 - id: pub label: MQTT out sub: publish line state accent: indigo col: 5 row: 2 - id: catch label: Catch sub: scoped to the read accent: red col: 3 row: 3 align: left --- :: ## Why these choices - **Subflow, applied whole-flow** — the flow itself is a straight line with no seams to extract, so there's no *internal* pattern to reach for. But because the same complete flow runs on every line, it's packaged as a subflow — reuse across instances is exactly the subflow rung. The pattern is applied to the whole flow, not to a piece inside it. - **Configured per line — why it's a Configurable App** — a config UI node sets this line's PLC tag names into context; a *get config* node in front of the PLC read loads them, so the one subflow reads different tags on every line without changing the build. That per-line configuration is exactly what makes it a Configurable App. - **Classify: it's a stream** — the signals are telemetry read at the poll cadence, so the latest value matters more than any single earlier one. One read → one compute → one publish per tick; the poll interval already paces it, so there's no fast-in / slow-out to rate-limit. - **Context for the counts** — running run/stop counts live in flow context, not threaded through the wires. - **Catch the read** — a PLC read can time out; a Catch scoped to the read handles a dropped read so it doesn't stall the publish. A Catch node isn't wired *from* the read — it registers to catch errors in its scope, so the dashed line shows that scope, not a connection. Catching errors is [good form](https://flowfuse.com/docs/node-red-guide/patterns/good-form/), not a per-flow choice. ::callout{icon="i-lucide-check"} **In one line** — a subflow per line: set the tags in a config UI, load them, then poll → read → compute → publish (with a scoped catch); the data call is treating it as a stream and holding the counts in context. :: # Catch Catches and handles errors that occur in flows. ## Where and why do we use the Catch node? The Catch node handles errors within your flows, preventing crashes and enabling graceful error recovery. When an error occurs in any node, the Catch node intercepts it and lets you respond appropriately - whether that's logging the error, retrying the operation, sending alerts, or providing user feedback. This is essential for building robust flows that can handle unexpected situations without failing completely. **Note:** Some third-party nodes have their own error-handling mechanisms, such as updating status or sending custom error messages, which may not properly inform the runtime about errors occurring. The Catch node cannot capture or handle these errors. ## Modes of operation The Catch node can be configured to catch errors from different scopes: ### All Nodes Captures errors from all nodes in the same tab or flow. This is useful for implementing flow-wide error handling where you want a single catch-all error handler. ### Same Group Limits error capture to nodes within the same group as the Catch node. Use this when you want isolated error handling for specific sections of your flow that are grouped together. ### Selected Nodes Captures errors from specific nodes you choose. This gives you fine-grained control over which nodes' errors are handled by this particular Catch node, useful when different nodes need different error handling strategies. ## How the node handles messages When an error occurs in a monitored node, the Catch node emits a message object containing error information. The node doesn't modify or stop the original error - it creates a new message flow that you can use to respond to the error. The message object emitted by the Catch node contains: - **payload** - the payload that was passed to the node which threw the error - **error.message** - the error message text - **error.source**- object containing information about the node that logged the error: - **id** - the source node id - **type** - the type of the source node - **name** - the name, if set, of the source node - **count** - how many times this message has been thrown by this node This information lets you implement sophisticated error handling logic, including conditional responses based on which node failed or what type of error occurred. ## Examples ### Error handling for external integrations The Catch node handles errors when interacting with APIs, including network issues, response errors, server unavailability, database connection losses, timeout errors, and MQTT broker disconnections. It can also trigger retry actions as necessary. In this example, the inject node sets a request timeout of 2000 milliseconds. The HTTP request node calls a mock URL with a 3-second delay parameter, simulating a delayed response. This causes a timeout error that the Catch node intercepts. After catching the error, the flow retries the request after a 5-second delay. ::render-flow ```json [{"id":"aa3e542e2b379d9e","type":"catch","z":"dff6fa938cfda5c8","name":"","scope":null,"uncaught":false,"x":140,"y":420,"wires":[["f52165d6428bf401","7c3d8d927c2b87d9"]]},{"id":"d6d87d75c231118a","type":"inject","z":"dff6fa938cfda5c8","name":"Send request","props":[{"p":"requestTimeout","v":"2000","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":370,"y":220,"wires":[["2a684bfe86bf3597"]]},{"id":"2a684bfe86bf3597","type":"http request","z":"dff6fa938cfda5c8","name":"","method":"GET","ret":"txt","paytoqs":"query","url":"https://reqres.in/api/users?delay=3","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":550,"y":280,"wires":[["df1c4cba60624654"]]},{"id":"df1c4cba60624654","type":"debug","z":"dff6fa938cfda5c8","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":740,"y":280,"wires":[]},{"id":"f52165d6428bf401","type":"debug","z":"dff6fa938cfda5c8","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":360,"y":440,"wires":[]},{"id":"7c3d8d927c2b87d9","type":"delay","z":"dff6fa938cfda5c8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":360,"y":380,"wires":[["2a684bfe86bf3597"]]},{"id":"c2deb182e2c24e98","type":"comment","z":"dff6fa938cfda5c8","name":"Send request with timeout of 2000 mil seconds","info":"","x":290,"y":160,"wires":[]},{"id":"303cbead6a589517","type":"comment","z":"dff6fa938cfda5c8","name":"Set a delay of 3 seconds for the response.","info":"","x":660,"y":220,"wires":[]},{"id":"828fb1a255aaeb65","type":"comment","z":"dff6fa938cfda5c8","name":"Retry request after 5 seconds if request timeout","info":"","x":240,"y":320,"wires":[]}] ``` :: ### User input validation In applications with user input, validation errors can disrupt the flow. The Catch node handles these errors, providing feedback or corrective actions to guide users. In this example, the function node attempts to sort input data received from an inject node using the sort method, which only works with arrays. Sending other data types causes an error that the Catch node catches, which then sends a validation message to the user. ::render-flow ```json [{"id":"5b5392bda3519ca5","type":"inject","z":"a2240ea952051e81","name":"Send invalid input","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"This is string","payloadType":"str","x":240,"y":160,"wires":[["2c3f517165f7ee37"]]},{"id":"f63239dbca38b4bd","type":"catch","z":"a2240ea952051e81","name":"","scope":null,"uncaught":false,"x":200,"y":400,"wires":[["222121f5280a68d8","d0f062a664420050"]]},{"id":"222121f5280a68d8","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":380,"y":380,"wires":[]},{"id":"c79bf5f53f284338","type":"inject","z":"a2240ea952051e81","name":"Send valid input","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,3,4,5,6,7,8,9,10]","payloadType":"json","x":240,"y":240,"wires":[["2c3f517165f7ee37"]]},{"id":"2c3f517165f7ee37","type":"function","z":"a2240ea952051e81","name":"Sort data array","func":"let data = msg.payload;\ndata = data.sort((a,b)=>a+b)\nmsg.payload = data\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":200,"wires":[[]]},{"id":"7c0aadd1766c2909","type":"debug","z":"a2240ea952051e81","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":600,"y":440,"wires":[]},{"id":"d0f062a664420050","type":"change","z":"a2240ea952051e81","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"please enter valid input ","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":440,"wires":[["7c0aadd1766c2909"]]},{"id":"51f9a4431f667dd4","type":"comment","z":"a2240ea952051e81","name":"Send alert when invalid input recieved","info":"","x":410,"y":300,"wires":[]},{"id":"a66c2f72744d7b33","type":"comment","z":"a2240ea952051e81","name":"Function sorts data array recived by inject node","info":"","x":440,"y":80,"wires":[]}] ``` :: ::node-red-help --- category: common file: 25-catch name: Catch node: catch --- :: # Comment Adds documentation and explanatory notes directly within your flows. ## Where and why do we use the Comment node? The Comment node helps document your flows by adding explanatory text that appears on the canvas. This is essential for maintaining flows over time, especially when working in teams or returning to flows after extended periods. By documenting the purpose, requirements, and logic of flow sections, you make flows more understandable, reduce interpretation errors, and speed up troubleshooting and modifications. Comments are particularly valuable for complex flows with link nodes, multi-tab workflows, or business logic that isn't obvious from the nodes alone. > If you need help documenting your flows, [FlowFuse Assistant](https://flowfuse.com/docs/user/expert/) can automatically generate documentation with Comment nodes. Simply select your flow and click the "Explain Flow" button, the AI will analyze your flow and create comprehensive documentation that you can add directly to your canvas. ## Modes of operation The Comment node provides flexible documentation capabilities: ### Canvas Labels Display brief explanatory text directly on the canvas. The node shows a single line of text in its name field, providing quick context without cluttering the visual layout. This is useful for labeling flow sections, marking decision points, or highlighting important considerations. ### Detailed Documentation Store longer explanations in the node's info panel. Double-clicking the Comment node reveals a WYSIWYG editor where you can write detailed documentation, including formatting, lists, and even ASCII diagrams. This detailed content doesn't take up canvas space but remains accessible when needed. ### Group Annotations Place Comment nodes within flow groups to document the group's purpose and functionality. This creates self-contained, well-documented sections of your flow that are easier to understand and maintain. It's recommended to add comments to all flow groups for comprehensive documentation. ## How the node handles messages The Comment node is purely documentary and doesn't participate in message flows. It has no input or output connections and doesn't process, modify, or generate messages. You can place Comment nodes anywhere on the canvas without affecting flow execution or performance. Because Comment nodes don't impact runtime behavior, you can be as detailed and explicit as needed in your documentation. Multiple Comment nodes can be used throughout a flow to explain different sections, decision logic, or implementation details. ## Examples ### Documenting flow sections Add comments to explain what groups of nodes accomplish and why they're structured that way. This example shows a comment documenting an input validation section, helping future developers understand the requirements and logic. ::render-flow ```json [{"id":"comment-validation","type":"comment","z":"a1b2c3d4e5f6g7h8","name":"Input Validation Section","info":"This section validates incoming sensor data before processing.\n\nRequirements:\n- Temperature must be between -50 and 100°C\n- Humidity must be between 0 and 100%\n- Both values must be present\n\nInvalid data is logged and discarded to prevent\ndownstream processing errors.","x":210,"y":140,"wires":[]},{"id":"input-inject","type":"inject","z":"a1b2c3d4e5f6g7h8","name":"Sensor Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"temperature\":22,\"humidity\":65}","payloadType":"json","x":190,"y":200,"wires":[["validate-temp"]]},{"id":"validate-temp","type":"switch","z":"a1b2c3d4e5f6g7h8","name":"Check Temperature","property":"payload.temperature","propertyType":"msg","rules":[{"t":"btwn","v":"-50","vt":"num","v2":"100","vt2":"num"}],"checkall":"true","repair":false,"outputs":1,"x":400,"y":200,"wires":[["validate-humidity"]]},{"id":"validate-humidity","type":"switch","z":"a1b2c3d4e5f6g7h8","name":"Check Humidity","property":"payload.humidity","propertyType":"msg","rules":[{"t":"btwn","v":"0","vt":"num","v2":"100","vt2":"num"}],"checkall":"true","repair":false,"outputs":1,"x":620,"y":200,"wires":[["output-debug"]]},{"id":"output-debug","type":"debug","z":"a1b2c3d4e5f6g7h8","name":"Valid Data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":200,"wires":[]}] ``` :: ::node-red-help --- category: common file: 90-comment name: Comment node: comment --- :: # Complete ## What is the Complete Node? The Complete Node in Node-RED is used to trigger a flow after a specified node completes its execution or a certain task. This is achieved by informing the Node-RED runtime about the completion of a task performed by the node itself. In custom nodes, this support is typically implemented by calling the `done()` callback function after the execution of the task. This signals to the runtime that the task has been completed and triggers the Complete Node. **Note:** While this node is supported by all nodes, only nodes that have implemented support by informing the runtime about the completion of a certain task can utilize it. This node must be configured to handle events for selected nodes; it does not provide an option to enable event handling from all nodes automatically. For notifying task completion in the middle of a function, you can use node.call in a function node. ## Use Cases - **Asynchronous Task Completion:** Suppose you have a flow where one node performs an asynchronous task, such as fetching data from an API. You can use the Complete Node to trigger the next set of actions in the flow only after the data has been successfully fetched. - **Long-running Process Completion:** For processes that take a significant amount of time to complete, such as batch jobs, data transformations, or machine learning tasks, the Complete Node can be used to mark the end of these processes and trigger follow-up actions or notifications. - **Batch Processing:** For batch processing tasks, the Complete Node can be used to signal the completion of a batch process. This could be useful in data processing workflows where data is processed in batches, and you need to know when each batch is finished before starting the next one. - **Output-less Node:** In Node-RED, certain nodes like WebSocket-out and MQTT-out do not have outputs to connect with. The Complete node in Node-RED can be helpful when you need to know when a process is done by those nodes. ## Example 1. In the example flow below, we have a WebSocket server, the Inject node sends data to the WebSocket server, and upon successful transmission to the WebSocket server, a Complete node handles the event. ::render-flow ```json [{"id":"c10651a30bf2d6ab","type":"inject","z":"a2240ea952051e81","name":"send data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":220,"wires":[["915f186a0a2b9663"]]},{"id":"915f186a0a2b9663","type":"websocket out","z":"a2240ea952051e81","name":"websocket server","server":"65bb0cfe75e94539","client":"","x":590,"y":220,"wires":[]},{"id":"ad8b360ab6cfeb02","type":"complete","z":"a2240ea952051e81","name":"complete","scope":["915f186a0a2b9663"],"uncaught":false,"x":280,"y":340,"wires":[["3a1e38c46e9e2e47"]]},{"id":"3a1e38c46e9e2e47","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":640,"y":340,"wires":[]},{"id":"a8bc28796b265405","type":"comment","z":"a2240ea952051e81","name":"Sending data to websocket server","info":"","x":420,"y":160,"wires":[]},{"id":"40320b4bdb0a294d","type":"comment","z":"a2240ea952051e81","name":"Upon successful data transmission to the WebSocket server, the Complete node handles the event.","info":"","x":460,"y":280,"wires":[]},{"id":"65bb0cfe75e94539","type":"websocket-listener","path":"/ws/response","wholemsg":"false"}] ``` :: In the example flow below, we have an inject node sending an HTTP GET request to a mock API. After successful completion of the request, the complete node will handle the event. ::render-flow ```json [{"id":"1be36b7e2b60d224","type":"inject","z":"a2240ea952051e81","name":"Get Todolist ","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":290,"y":240,"wires":[["9467be1692a1ae7b"]]},{"id":"93eedc09f6c55a7a","type":"complete","z":"a2240ea952051e81","name":"","scope":["9467be1692a1ae7b"],"uncaught":false,"x":290,"y":360,"wires":[["5f1a90e93fad1567"]]},{"id":"5f1a90e93fad1567","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":360,"wires":[]},{"id":"7412de7c14e16942","type":"comment","z":"a2240ea952051e81","name":"Sending get request to API ","info":"","x":410,"y":180,"wires":[]},{"id":"6668136d53f3c650","type":"comment","z":"a2240ea952051e81","name":"Handle the request completion event.","info":"","x":410,"y":300,"wires":[]},{"id":"9467be1692a1ae7b","type":"http request","z":"a2240ea952051e81","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://jsonplaceholder.typicode.com/todos","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":570,"y":240,"wires":[[]]}] ``` :: ## Output Message: When a task is completed by a specified node in the Complete Node, it emits the same message object emitted by that specified node. ::node-red-help --- category: common file: 24-complete name: Complete node: complete --- :: # Debug Displays messages in the Debug sidebar for monitoring and troubleshooting flows. ## Where and why do we use the Debug node? The Debug node helps you understand the messages and data traveling through your flows. It's essential during development for monitoring data transformations, verifying logic, and troubleshooting issues. By adding Debug nodes at key points in your flow, you gain visibility into what's being passed between nodes, making it easier to identify problems and validate that your flow works as expected. ## Modes of operation The Debug node can output message data in several ways: ### Message Property Output Output any property of the message object, such as `msg.payload`, `msg.topic`, or any custom property like `msg.my_property`. This is the most common mode for inspecting specific data as it flows through your nodes. ### Complete Message Object Output the entire message object with all its properties. This reveals the full structure of the message, including metadata and properties you might not know exist. This mode is valuable when you need to understand the complete context available to downstream nodes. ### JSONata Expression Use [JSONata](https://jsonata.org/){rel=""nofollow""} expressions to transform and format message properties into more readable output. This lets you extract nested values, perform calculations, or format data in ways that make debugging easier. ## Output destinations The Debug node can send output to multiple locations: ### Debug Sidebar Messages appear in the Debug sidebar panel on the right side of the Node-RED editor. This is the primary debugging interface where you can inspect messages, expand objects, and use helper features like Copy Path and Copy Value. ### System Console Output is sent to the terminal or console where Node-RED is running. This is useful for debugging when the Node-RED editor is not open or when you need persistent logs. ### Node Status Output appears as status text below the node in the flow editor. This provides at-a-glance information without opening the Debug sidebar. You can display the same content as the debug output, show different properties, use JSONata expressions, or display a message counter. ## How the node handles messages The Debug node passes messages through unchanged to any connected nodes. It's purely observational and doesn't modify the message flow. This means you can add Debug nodes anywhere in your flow without affecting the behavior. When displaying output, the Debug node truncates very large messages to prevent performance issues. You can expand truncated sections in the Debug sidebar to see the full content. The node can be enabled or disabled without modifying the flow. A disabled Debug node (shown greyed out) still passes messages through but doesn't output anything, useful for reducing console noise in production. ## Helper features ### Copy Path The Copy Path feature lets you quickly copy the property path of any value in the debug output. This is a real time saver when building Change or Function nodes, helping you avoid typos and errors. Simply click the small icon next to any property in the debug output to copy its full path. :video{ariaLabel="Copy Path helper" autoPlay="true" height="397" loop="true" muted="true" playsInline="true" preload="none" width="906"} ### Copy Value Copy Value gives you an exact copy of any property value to use in Inject, Change, or Function nodes. This is extremely useful when you need to simulate real data or when sharing example flows with others for troubleshooting. Click the value in the debug output and select Copy Value from the menu. :video{ariaLabel="Copy Value helper" autoPlay="true" height="655" loop="true" muted="true" playsInline="true" preload="none" width="825"} ### Pin Open When debug output contains many nested properties, Pin Open helps you keep specific items expanded while collapsing others. This makes it easy to focus on the data you care about without losing your place as new messages arrive. :video{ariaLabel="Pin Open helper" autoPlay="true" height="655" loop="true" muted="true" playsInline="true" preload="none" width="825"} ## Examples This example demonstrates common ways to use the Debug node for visualizing data and assisting development. It shows outputting to the sidebar, displaying node status, and using JSONata expressions. ![Debug Nodes](https://flowfuse.com/docs/node-red/core-nodes/images/debug-examples.png) ::node-red-help --- category: common file: 21-debug name: Debug node: debug --- :: # Common The **Common** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [Inject](https://flowfuse.com/docs/node-red/core-nodes/common/inject/): Guide on the Node-RED core node that injects a message into a flow - [Debug](https://flowfuse.com/docs/node-red/core-nodes/common/debug/) - [Complete](https://flowfuse.com/docs/node-red/core-nodes/common/complete/) - [Catch](https://flowfuse.com/docs/node-red/core-nodes/common/catch/) - [Status](https://flowfuse.com/docs/node-red/core-nodes/common/status/) - [Link](https://flowfuse.com/docs/node-red/core-nodes/common/link/) - [Comment](https://flowfuse.com/docs/node-red/core-nodes/common/comment/) - [Unknown](https://flowfuse.com/docs/node-red/core-nodes/common/unknown/) # Inject Triggers flows by injecting messages manually or automatically. ## Where and why do we use the Inject node? The Inject node starts flows either manually by clicking the button on its left side or automatically on a schedule. This makes it essential for testing and debugging flows, triggering automated tasks, initializing system state on startup, or running periodic data processing jobs. It's often the first node in a flow, providing the initial message that kicks off the entire process. ## Modes of operation The Inject node can trigger flows in several different ways: ### Manual Trigger Click the button on the left side of the node to send a message on demand. This is the most common mode for testing, debugging, or manually initiating processes. The message can contain any configured payload and properties. ### On Startup Configure the node to inject a message automatically when Node-RED starts or when flows are deployed. This is useful for initializing flow state, setting default values, or starting background processes. You can add a delay before the injection occurs. When configured to inject once on start, a small '1' appears after the label inside the node. ### Interval Send messages repeatedly at a fixed time interval. Set the interval in seconds, minutes, hours, or days. The interval must be greater than 1 and less than 2^31. When the repeat value is 0 or below, Node-RED will not display an error but the interval won't function. ### Scheduled Inject messages at specific times using cron-like scheduling. This allows complex schedules like "every Monday at 9am" or "the first day of each month at midnight". Make sure to set the correct timezone in the [editor settings](https://flowfuse.com/docs/user/instance-settings/#editor). ## How the node handles messages The Inject node creates a new message object with configured properties. By default, it sets `msg.payload` to the current timestamp and `msg.topic` to an empty string, but you can configure any message properties. Message properties can be set to: - Static values (strings, numbers, booleans, JSON objects) - Flow or global context variables - Environment variables - JSONata expressions for dynamic values - Current timestamp - Empty values The node can set multiple message properties at once, allowing you to construct complete message objects that subsequent nodes need. ## Examples ### Inject on Node-RED start To setup state when starting Node-RED, the inject node can be set to trigger a flow once with minimal delay. This example injects a timestamp immediately after deployment or restart. ::render-flow ```json [{"id":"73cc510bee68600f","type":"inject","z":"80987f27785245a7","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.1","topic":"","payload":"","payloadType":"date","x":190,"y":200,"wires":[["7f83bf24bdf7bc68"]]},{"id":"7f83bf24bdf7bc68","type":"debug","z":"80987f27785245a7","name":"Output once","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":200,"wires":[]}] ``` :: ### Run a flow daily at midnight By selecting "at a specific time" in the Repeat section, the inject node can generate a message at set times. This example triggers at 23:59 (11:59 PM) every day, useful for daily data processing tasks. ::render-flow ```json [{"id":"998e844a7e50e275","type":"inject","z":"80987f27785245a7","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"59 23 * * *","once":false,"onceDelay":"0","topic":"","payload":"","payloadType":"date","x":190,"y":320,"wires":[["1e80f5229516e910"]]},{"id":"1e80f5229516e910","type":"debug","z":"80987f27785245a7","name":"Output daily at night","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":400,"y":320,"wires":[]}] ``` :: ### Inject a static string The Inject node can set the payload to static data like strings, numbers, or JSON objects. This example injects the string "Hello FlowFuse!" when triggered manually. ::render-flow ```json [{"id":"c0451e14f6b7eff0","type":"inject","z":"80987f27785245a7","name":"Inject a string","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello FlowFuse!","payloadType":"str","x":190,"y":280,"wires":[["9fbd8a0a9d21562a"]]},{"id":"9fbd8a0a9d21562a","type":"debug","z":"80987f27785245a7","name":"Output \"Hello FlowFuse\"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":410,"y":280,"wires":[]}] ``` :: ::node-red-help --- category: common file: 20-inject name: Inject node: inject --- :: # Link Creates virtual connections between nodes without visible wires, helping organize complex flows. ## Where and why do we use the Link nodes? The Link In and Link Out nodes help organize flows by creating virtual connections that aren't visible until selected. This is essential for managing complex flows where physical wires would create visual clutter or when you want to reuse logic across multiple locations. By connecting distant parts of your flow without drawing wires across the canvas, you can keep related nodes grouped together, reduce wire crossings, and make flows more maintainable and easier to understand. ## Modes of operation Link nodes work in pairs to create virtual connections: ### Link Out Node Sends messages to one or more Link In nodes. You can configure which Link In nodes receive the message by selecting them from a list. The Link Out node can send to Link In nodes on the same tab or on different tabs, enabling cross-flow communication. ### Link In Node Receives messages from Link Out nodes. When a Link Out node sends a message, all connected Link In nodes receive a copy. The Link In node acts as a starting point for a new branch of your flow. ### Link Call Node Sends messages to a Link In node and waits for a response, similar to calling a function. This enables request-response patterns where you need to process data and return results. The response comes from a Link Out node configured to return to the calling Link Call node. ## How the nodes handle messages Link Out nodes pass messages to their connected Link In nodes exactly as received - no modifications are made. When multiple Link In nodes are connected to a single Link Out, each receives an independent copy of the message. Link In nodes emit the received message and start a new flow branch. Any nodes connected to a Link In node process the message as if it came from any other node. Link Call nodes send messages and pause, waiting for a response. The called Link In node processes the message through its connected nodes until reaching a Link Out node configured to return responses. This creates synchronous, function-like behavior within otherwise asynchronous flows. The virtual connections between Link nodes only become visible in the editor when you select either the Link In or Link Out node, showing which nodes are connected with dashed lines. ## Examples ### Organizing flow sections Link nodes separate different functional areas of a flow without cluttering the canvas with long wires. This example shows input processing separated from output handling, making each section easier to understand and maintain. ::render-flow ```json [{"id":"22ca6ef9668b5ddd","type":"group","z":"f77f61b90395f588","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["input-inject","process-input","link-out-process","link-in-output","format-output","output-debug"],"x":334,"y":5299,"w":792,"h":202},{"id":"input-inject","type":"inject","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"Sensor Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"temperature\":22,\"humidity\":65}","payloadType":"json","x":450,"y":5340,"wires":[["process-input"]]},{"id":"process-input","type":"function","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"Validate Input","func":"if (\n msg.payload &&\n msg.payload.temperature !== undefined &&\n msg.payload.humidity !== undefined\n) {\n return msg;\n}\nreturn null;","outputs":1,"x":760,"y":5340,"wires":[["link-out-process"]]},{"id":"link-out-process","type":"link out","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"To Output Handler","mode":"link","links":["link-in-output"],"x":935,"y":5340,"wires":[]},{"id":"link-in-output","type":"link in","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"From Input Process","links":["link-out-process"],"x":515,"y":5460,"wires":[["format-output"]]},{"id":"format-output","type":"function","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"Format for Display","func":"msg.payload = `Temp: ${msg.payload.temperature}°C, Humidity: ${msg.payload.humidity}%`;\nreturn msg;","outputs":1,"x":770,"y":5460,"wires":[["output-debug"]]},{"id":"output-debug","type":"debug","z":"f77f61b90395f588","g":"22ca6ef9668b5ddd","name":"Display Output","active":true,"tosidebar":true,"complete":"payload","x":1000,"y":5460,"wires":[]}] ``` :: ### Reusable processing logic Link In nodes enable reusing the same processing logic from multiple sources without duplicating nodes. This example shows multiple inputs feeding into a single processing pipeline through Link nodes, useful for common transformations or validations. ::render-flow ```json [{"id":"source1-inject","type":"inject","z":"a1b2c3d4e5f6g7h8","name":"Source 1","props":[{"p":"payload"},{"p":"source","v":"sensor-1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"25","payloadType":"num","x":180,"y":140,"wires":[["link-out-1"]]},{"id":"source2-inject","type":"inject","z":"a1b2c3d4e5f6g7h8","name":"Source 2","props":[{"p":"payload"},{"p":"source","v":"sensor-2","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"30","payloadType":"num","x":180,"y":200,"wires":[["link-out-2"]]},{"id":"source3-inject","type":"inject","z":"a1b2c3d4e5f6g7h8","name":"Source 3","props":[{"p":"payload"},{"p":"source","v":"sensor-3","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"28","payloadType":"num","x":180,"y":260,"wires":[["link-out-3"]]},{"id":"link-out-1","type":"link out","z":"a1b2c3d4e5f6g7h8","name":"","mode":"link","links":["link-in-process"],"x":335,"y":140,"wires":[]},{"id":"link-out-2","type":"link out","z":"a1b2c3d4e5f6g7h8","name":"","mode":"link","links":["link-in-process"],"x":335,"y":200,"wires":[]},{"id":"link-out-3","type":"link out","z":"a1b2c3d4e5f6g7h8","name":"","mode":"link","links":["link-in-process"],"x":335,"y":260,"wires":[]},{"id":"link-in-process","type":"link in","z":"a1b2c3d4e5f6g7h8","name":"Common Processor","links":["link-out-1","link-out-2","link-out-3"],"x":195,"y":380,"wires":[["process-data"]]},{"id":"process-data","type":"function","z":"a1b2c3d4e5f6g7h8","name":"Convert to Fahrenheit","func":"msg.payload = (msg.payload * 9/5) + 32;\nmsg.payload = `${msg.source}: ${msg.payload.toFixed(1)}°F`;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":380,"wires":[["result-debug"]]},{"id":"result-debug","type":"debug","z":"a1b2c3d4e5f6g7h8","name":"Processed Results","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":380,"wires":[]}] ``` :: ::node-red-help --- category: common file: 60-link name: Link node: link --- :: # Status Monitors and captures status updates from other nodes in your flows. ## Where and why do we use the Status node? The Status node lets you programmatically react to state changes in other nodes by capturing their status updates. While many nodes display status information visually below themselves in the editor, the Status node converts these updates into message flows you can process. This is essential for building automated error handling, creating custom monitoring dashboards, tracking node performance, or implementing logic that responds to operational conditions like queue sizes or connection states. ## Modes of operation The Status node can monitor status updates from different scopes: ### All Nodes Captures status updates from all nodes in the same tab or flow. This provides flow-wide visibility into node states, useful for centralized monitoring or logging. ### Same Group Limits status capture to nodes within the same group as the Status node. Use this when you want isolated status monitoring for specific sections of your flow that are grouped together. ### Selected Nodes Captures status from specific nodes you choose. This gives you fine-grained control over which nodes' status updates are monitored, useful when you only care about particular nodes or want different handling for different node types. ## How the node handles messages When a monitored node updates its status, the Status node emits a message object containing status information. The node creates a new message flow that you can use to react to status changes programmatically. The message object emitted by the Status node contains: - **status.text** - the status text displayed below the node - **status.fill** - the color of the status indicator (red, green, yellow, blue, grey) - **status.shape** - the shape of the status indicator (ring or dot) - **status.source**- object containing information about the node that generated the status: - **id** - the source node id - **type** - the type of the source node - **name** - the name, if set, of the source node This information enables you to implement sophisticated monitoring logic, including conditional responses based on which node changed status, what the status indicates, or patterns of status changes over time. ## Examples ### Monitoring delay node queue size The Status node tracks the number of messages queued in a delay node. This example captures status updates from the delay node and processes them to monitor queue depth, useful for detecting backpressure or triggering alerts when the queue grows too large. ::render-flow ```json [{"id":"delay-inject","type":"inject","z":"f1e2d3c4b5a69788","name":"Send Messages","props":[{"p":"payload"}],"repeat":"0.5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":200,"wires":[["delay-node"]]},{"id":"delay-node","type":"delay","z":"f1e2d3c4b5a69788","name":"Rate Limit Queue","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":400,"y":200,"wires":[["delay-output"]]},{"id":"status-monitor","type":"status","z":"f1e2d3c4b5a69788","name":"Monitor Queue","scope":["delay-node"],"x":190,"y":280,"wires":[["status-debug"]]},{"id":"delay-output","type":"debug","z":"f1e2d3c4b5a69788","name":"Rate Limited Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":200,"wires":[]},{"id":"status-debug","type":"debug","z":"f1e2d3c4b5a69788","name":"Queue Status","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"status","targetType":"msg","statusVal":"","statusType":"auto","x":390,"y":280,"wires":[]}] ``` :: ### Connection state monitoring Monitor the connection status of MQTT or other connection-based nodes. This example shows how to capture connection state changes and use them to trigger reconnection logic, send alerts, or update application state based on connectivity. ::render-flow ```json [{"id":"mqtt-in","type":"mqtt in","z":"a9b8c7d6e5f4g3h2","name":"MQTT Subscriber","topic":"sensor/#","qos":"0","datatype":"auto","broker":"mqtt-broker","nl":false,"rap":false,"inputs":0,"x":200,"y":180,"wires":[["mqtt-debug"]]},{"id":"mqtt-status","type":"status","z":"a9b8c7d6e5f4g3h2","name":"Connection Monitor","scope":["mqtt-in"],"x":210,"y":260,"wires":[["connection-check"]]},{"id":"mqtt-debug","type":"debug","z":"a9b8c7d6e5f4g3h2","name":"MQTT Messages","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":180,"wires":[]},{"id":"connection-check","type":"switch","z":"a9b8c7d6e5f4g3h2","name":"Check Connection","property":"status.text","propertyType":"msg","rules":[{"t":"eq","v":"connected","vt":"str"},{"t":"eq","v":"disconnected","vt":"str"}],"checkall":"true","outputs":2,"x":430,"y":260,"wires":[["connected-debug"],["disconnected-debug"]]},{"id":"connected-debug","type":"debug","z":"a9b8c7d6e5f4g3h2","name":"Connected","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"status","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":240,"wires":[]},{"id":"disconnected-debug","type":"debug","z":"a9b8c7d6e5f4g3h2","name":"Disconnected","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"status","targetType":"msg","statusVal":"","statusType":"auto","x":660,"y":280,"wires":[]},{"id":"mqtt-broker","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}] ``` :: ::node-red-help --- category: common file: 25-status name: Status node: status --- :: # Unknown Represents nodes that are not installed in your Node-RED instance. ## Where and why do we see the Unknown node? The Unknown node appears automatically when you import flows containing nodes that aren't installed in your current Node-RED instance. You cannot add Unknown nodes manually - they only appear as placeholders for missing node types. This helps you identify which node packages need to be installed before your imported flows can function properly. ## How the node appears When Node-RED encounters an unrecognized node type during import, it creates an Unknown node placeholder that preserves the original configuration and connections. Once you install the missing package and reload Node-RED, Unknown nodes automatically convert to their proper types with all settings intact. > Latest versions of Node-RED include package dependency information when exporting flows. When you import these flows, Node-RED automatically detects missing packages and prompts you to install them with a single click, eliminating the need to manually identify and install each required package. ## Identifying required packages Before importing flows, identify which node packages are installed in your source instance to prepare the target instance with necessary dependencies. ### Using the Palette Manager Access the Palette Manager through the Node-RED menu to see all installed packages and their versions. ![List of nodes installed, including unused nodes](https://flowfuse.com/docs/node-red/core-nodes/images/list-nodes-unused.png) ### Using System Info The System Info dialog provides a comprehensive list of installed node packages that you can view and copy for documentation. :video{ariaLabel="List of nodes installed through the System Info dialog" autoPlay="true" height="868" loop="true" muted="true" playsInline="true" preload="none" width="1484"} ## Migrating flows to FlowFuse When migrating flows to FlowFuse, use the [nr-tools plugin](https://flowfuse.com/docs/migration/introduction) for automatic package installation, credential migration, and complete flow transfer. This eliminates manual package identification and installation, making migration faster and less error-prone. ::node-red-help --- category: common file: 98-unknown name: Unknown node: unknown --- :: # Change ## What's the Change node in Node-RED used for? The Change node in Node-RED is used for modifying the content of messages within a flow. It allows you to add, remove, modify, or set message properties and payload values, making it a fundamental node for data transformation and manipulation. The Change node is essential for preparing data for further processing, formatting messages for specific outputs, and adapting data to suit the requirements of downstream nodes in a flow. ## Examples for the Change node Use cases for the Change node include: 1. **Data Transformation**: You can use the Change node to transform data from one format to another. For example, you can convert temperature values from Celsius to Fahrenheit, translate textual information, or convert timestamps to a different format. ![Data Transform](https://flowfuse.com/docs/node-red/core-nodes/images/change-data-transform.png) ::render-flow ```json [{"id":"1cd48684.04dbab","type":"tab","label":"Temperature Conversion","disabled":false,"info":""},{"id":"d803c3c9.0761d8","type":"inject","z":"1cd48684.04dbab","name":"Celsius","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"25","payloadType":"num","x":170,"y":100,"wires":[["21b83b07.36564"]]},{"id":"21b83b07.36564","type":"change","z":"1cd48684.04dbab","name":"Convert to Fahrenheit","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(($number(payload) * 9/5) + 32, 2)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":100,"wires":[["dc92db44.a50c08"]]},{"id":"dc92db44.a50c08","type":"debug","z":"1cd48684.04dbab","name":"Fahrenheit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":100,"wires":[]}] ``` :: 2. **Message Filtering**: The Change node can filter out messages based on specific conditions. You can use the Change node to route messages to different outputs, discard irrelevant messages, or take specific actions based on message properties. ![Message Filter](https://flowfuse.com/docs/node-red/core-nodes/images/change-message-filter.png) ::render-flow ```json [{"id":"a4569070.48f9d8","type":"inject","z":"d238e48a.85c08","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Bonjour","payloadType":"str","x":170,"y":120,"wires":[["53c33235.e5c248"]]}] ``` :: 3. **Message Enrichment**: The Change node allows you to add or modify properties in a message to enrich its content. For instance, you can add timestamps, add contextual information, or set specific identifiers for tracking purposes. ![Message Enrichment](https://flowfuse.com/docs/node-red/core-nodes/images/change-message-enrich.png) ::render-flow ```json [{"id":"34d49c7f.505d58","type":"tab","label":"Message Enrichment","disabled":false,"info":""},{"id":"f7c09e8f.01af2","type":"inject","z":"34d49c7f.505d58","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello, world!","payloadType":"str","x":170,"y":120,"wires":[["2ef767d1.3b5f32"]]},{"id":"2ef767d1.3b5f32","type":"change","z":"34d49c7f.505d58","name":"Add Timestamp","rules":[{"t":"set","p":"timestamp","pt":"msg","to":"$now()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":120,"wires":[["e3b0c949.c20ba"]]},{"id":"e3b0c949.c20ba","type":"debug","z":"34d49c7f.505d58","name":"Enriched Message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":120,"wires":[]}] ``` :: 4. **Renaming Properties**: The Change node allows you to rename message properties, making it easier to understand and work with data at various points in your flow. ![Example](https://flowfuse.com/docs/node-red/core-nodes/images/change-rename-property.png) ::render-flow ```json [{"id":"5e6054b9.8b6a64","type":"tab","label":"Renaming Properties","disabled":false,"info":""},{"id":"ca8a03f3.119d18","type":"inject","z":"5e6054b9.8b6a64","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"data\":123}","payloadType":"json","x":170,"y":120,"wires":[["d44de052.a77d"]]},{"id":"d44de052.a77d","type":"change","z":"5e6054b9.8b6a64","name":"Rename Property","rules":[{"t":"set","p":"sensorData","pt":"msg","to":"payload.data","tot":"msg"},{"t":"delete","p":"payload.data","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":120,"wires":[["690e6de1.3ad218"]]},{"id":"690e6de1.3ad218","type":"debug","z":"5e6054b9.8b6a64","name":"Renamed Property","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"sensorData","targetType":"msg","statusVal":"","statusType":"auto","x":570,"y":120,"wires":[]}] ``` :: 5. **Default Values**: If a message lacks certain properties, the Change node can set default values for those properties, ensuring consistency in the data flow. ![Example](https://flowfuse.com/docs/node-red/core-nodes/images/change-default.png) ::render-flow ```json [{"id":"3abbe88b.537ac4","type":"tab","label":"Default Values","disabled":false,"info":""},{"id":"947dcab3.47e8b","type":"inject","z":"3abbe88b.537ac4","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"name\":\"Alice\"}","payloadType":"json","x":170,"y":120,"wires":[["a8d3ce5c.7982c"]]},{"id":"a8d3ce5c.7982c","type":"change","z":"3abbe88b.537ac4","name":"Set Default Age","rules":[{"t":"missing","p":"payload.age","pt":"msg"},{"t":"set","p":"payload.age","pt":"msg","to":"25","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":120,"wires":[["f5892567.3bbd8"]]},{"id":"f5892567.3bbd8","type":"debug","z":"3abbe88b.537ac4","name":"Enriched Data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":550,"y":120,"wires":[]}] ``` :: 6. **Message Formatting**: When sending data to external systems or services, the Change node can format the message payload in the required format (e.g., JSON, XML) or adjust data to match specific API requirements. ![Example](https://flowfuse.com/docs/node-red/core-nodes/images/change-message-format.png) ::render-flow ```json [{"id":"49e37717.8c3d98","type":"tab","label":"Message Formatting","disabled":false,"info":""},{"id":"4b97a1f2.bf7a0c","type":"inject","z":"49e37717.8c3d98","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"temperature\":28,\"humidity\":50}","payloadType":"json","x":170,"y":120,"wires":[["f303ce36.e3c1f"]]},{"id":"f303ce36.e3c1f","type":"change","z":"49e37717.8c3d98","name":"Format as JSON","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.temperature & \" °C, Humidity: \" & payload.humidity & \"%\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":120,"wires":[["d7c1af9f.f1099"]]},{"id":"d7c1af9f.f1099","type":"debug","z":"49e37717.8c3d98","name":"Formatted Message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":120,"wires":[]}] ``` :: Overall, the Change node is a crucial tool for data manipulation and orchestration in Node-RED flows. Its flexibility and range of operations make it an essential node for customizing messages according to your specific use cases and the requirements of the nodes within your flow. ::node-red-help --- category: function file: 15-change name: Change node: change --- :: # Delay ## What's the Delay node in Node-RED used for? The Delay node allows you to introduce a delay in the flow of messages between nodes. It can be useful in various scenarios where you need to control the timing of message processing. For example, the delay node can limit the rate at which messages are processed downstream or throttle the flow of messaging. Both can be useful for interacting with external systems that may have limitations in place. Here are some other use cases for using the Delay node: **Batch Processing**: If you're dealing with a stream of incoming data that you want to process in batches, you can use the Delay node to introduce a delay between groups of messages. This can be helpful when you need to aggregate or analyze data in chunks. **Sequential Processing**: Sometimes you need to ensure that messages are processed in a specific order. The Delay node can be used to enforce a sequence of message processing, especially when dealing with asynchronous systems that might not guarantee order. **Simulation and Testing**: In testing and simulation scenarios, you might want to mimic real-world timing conditions. The Delay node can help you introduce delays that simulate actual conditions, allowing you to test how your system behaves over time. **Time-based Triggers**: You can use the Delay node to trigger actions at specific time intervals. For instance, you might want to send a status update every hour or perform a cleanup task at the end of the day. **Circuit Breaker**: The Delay node can be employed as a simple form of circuit breaker. If a downstream system is failing or experiencing issues, you can introduce a delay before retrying, giving the system some time to recover. ## Examples for the Delay node An example of using the Delay node to rate limit http request to an external API. ![Delay node properties](https://flowfuse.com/docs/node-red/core-nodes/images/delay-node-2.png) ::render-flow ```json [{"id":"1f825afc.866efc","type":"delay","z":"e92fb6c3b304fd7c","name":"Rate Limit","pauseType":"rate","timeout":"20","timeoutUnits":"seconds","rate":"10","nbRateUnits":"","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":620,"y":120,"wires":[["26f1f0e3.65e3c8"]]}] ``` :: ### Reset the queue for the delay node The Delay node might create a queue that will continue their journey to the connected output nodes. There's situations however where the queue needs to be cleared. This is done by resetting the queue. When a `reset` property is set, the queue will be empty. This is useful for when the lack of an event might need to send a notification. Schedule the notification to be sent but allow a positive event to cancel the notification. In the example below; say you want to turn off the lights if no movement was detected for a period of time, you set the delay node to that time. When there's motion detected, you than send a `msg.reset` message to the delay node to cancel them turning of the light. This depends on an inject node set to [send a message on an interval](https://flowfuse.com/docs/node-red/core-nodes/common/inject/#run-a-flow-daily-at-midnight) to turn the lights off. ::render-flow ```json [{"id":"06b93157ff07c9b1","type":"delay","z":"e512003df3c971c7","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":460,"y":100,"wires":[["f345c902d09aaf76"]]},{"id":"1965e58562943d69","type":"inject","z":"e512003df3c971c7","name":"Motion detected","props":[{"p":"reset","v":"true","vt":"bool"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":250,"y":160,"wires":[["06b93157ff07c9b1"]]},{"id":"f345c902d09aaf76","type":"debug","z":"e512003df3c971c7","name":"Never receive a message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":100,"wires":[]},{"id":"834f0812a01be7a4","type":"inject","z":"e512003df3c971c7","name":"Turn off lights","props":[{"p":"payload"}],"repeat":"10","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":100,"wires":[["06b93157ff07c9b1"]]}] ``` :: ::node-red-help --- category: function file: 89-delay name: Delay node: delay --- :: # Exec Node-RED is written in Javascript, as are the custom nodes in the [Flows Library](https://flows.nodered.org/search?type=node){rel=""nofollow""}. At times it would be great to use programs written in other languages. In this guide we'll explain the `exec` node. ### Exec Node Node-RED by default comes with the `exec` node. This node allows you to run a command as if you're on the command line. The exec node has one input, and three outputs. Let's create a basic example and work from there. Let's connect the inject to the `exec` input, and connect the three dots to debug nodes. I've set the command in the exec node to `date`. !["Wired up exec node"](https://flowfuse.com/docs/node-red/core-nodes/images/wired-up-exec-node.png) When triggered, you'll see two outputs in the debug pane. One from `Standard out`; the date and timestamp of the execution and the `Exit code` will output too. `Standard error` didn't output anything. Let's explain what happened: the `date` command was executed by the NodeJS without any input. The command returned the date as a string, and execution of the commmand completed. There weren't any errors, so the error code is set to zero. Non-zero exit codes imply didn't go as intended. Output is written to the standard output, and not standard error. This allows us to handle errors differently from succesful exections and the output in standard output. ### Exec vs Spawn mode Let's change the `exec` node to execute `ping google.com`. The ping command provides the round-trip time to google.com in milliseconds, each second. When we deploy, the program is stuck. That's because the output is generated each second and won't complete until there's a failure in the command. Not what you'd want. When there's continous output you'll need to select `spawn` mode in the settings windown of the `exec` node: ![spawn mode for exec in Node-RED](https://flowfuse.com/docs/node-red/core-nodes/images/spawn-mode-exec.png) This will produce a message per line to the standard out output for further processing. ### Shell Expansions As the arguments for the exec nodes are executed as is, shell expansions will not work. For example when the exec command is `rm -r /path/to/dir/*`, the `*` will not be expanded and Node-RED will try to remove the file or directory called `*` in the `/path/to/dir` directory. ### FlowFuse On FlowFuse Cloud, the Node-RED exec nodes are disabled. The Stacks, that is the containers that Node-RED runs in, don't include any accessible executatables so there would be no benefit to running exec commands. All the data is exposed through Javascript and Node.JS already. ::node-red-help --- category: function file: 90-exec name: Exec node: exec --- :: # Filter ## What's the Filter node in Node-RED used for? The Filter node, previously called "Report by Exception" (RBE), has two modes of operation, called deadband and narrowband. These modes allow users to limit network traffic, write operations to historians, or limit reporting of values outside a range that is worth reporting on. It's very versatile, once you fully understand the node. The Filter node is part of the core nodes in Node-RED, meaning it is installed by default. ### Deadband Mode In deadband mode, the data transmission is triggered only when the measured value changes beyond a specified threshold value. This threshold value is known as the deadband, and it is used to prevent frequent data transmissions when the value fluctuates around a certain point. The deadband is typically set to a percentage of the measurement range or an absolute value. For example, if the temperature sensor measures a range of 0-100 degrees Celsius, and the deadband is set to 2 degrees, then the system will only report temperature changes greater than 2 degrees. This helps reduce unnecessary network traffic and save processing power. ### Narrowband Mode In narrowband mode, the data transmission is triggered only when the measured value falls outside a specified range of values. This range is known as the narrowband or hysteresis, and it is used to prevent unnecessary data transmissions when the value fluctuates within a certain range. For example, if the temperature sensor measures a range of 0-100 degrees Celsius, and the narrowband is set to 5 degrees, then the system will only report temperature changes greater than 5 degrees above or below the last reported value. Both deadband and narrowband modes are used to optimize data transmission and reduce the number of unnecessary data transmissions. ## Examples ### Report all changes With 3 messages send to the filter node; 1, 2, 2, the following flow will send through the first messages, `1` and `2` respectively. Then filter out the last `2` message as no changes were observed since it sent on the previous message. ::render-flow ```json [{"id":"5adf6b757e2a7bb2","type":"rbe","z":"97a2668540e2f3ba","name":"Only report changes","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":420,"y":100,"wires":[["6682a9e8826ad09b"]]},{"id":"f60dc26f8d634312","type":"inject","z":"97a2668540e2f3ba","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,2]","payloadType":"json","x":130,"y":100,"wires":[["5b2b61bb112b23e7"]]},{"id":"6682a9e8826ad09b","type":"debug","z":"97a2668540e2f3ba","name":"Print changes","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":100,"wires":[]},{"id":"5b2b61bb112b23e7","type":"split","z":"97a2668540e2f3ba","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":250,"y":100,"wires":[["5adf6b757e2a7bb2"]]}] ``` :: ### Report changes, ignore the initial value With 3 messages send to the filter node; 1, 2, 2, the initial value is used as sentinel value. Each change afterwards is send onwards. In this case printing just 2 once. The first message; `1` is remembered, and the next message afterwards is the only change in the stream of values. ::render-flow ```json [{"id":"d5c20441fc01294b","type":"rbe","z":"97a2668540e2f3ba","name":"Ignore first message, only report changes","func":"rbei","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":480,"y":180,"wires":[["c5845cc63c81f81d"]]},{"id":"eef24d9b8c3f98c7","type":"inject","z":"97a2668540e2f3ba","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,2]","payloadType":"json","x":130,"y":180,"wires":[["9e8e79801228c3bb"]]},{"id":"c5845cc63c81f81d","type":"debug","z":"97a2668540e2f3ba","name":"Print changes","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":740,"y":180,"wires":[]},{"id":"9e8e79801228c3bb","type":"split","z":"97a2668540e2f3ba","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":250,"y":180,"wires":[["d5c20441fc01294b"]]}] ``` :: ### Report changes larger than a certain percentage "Deadband" If the filter node is configured to "block unless value change is great or equal than" mode with a 50% threshold configured as "compared to last valid output value" and it's send `1, 2, 2, 1` it will send on the messages `2, 1`. The initial message `1` is set as sentinel value. The second message `2` is an increase of 100% against the sentinel which updates the sentinel value to two, and sends it on. The third message is equal to the sentinel value and thus filtered. The last value, `1`, is a 50% change compared to the sentinel value of `2` and send forward. ::render-flow ```json [{"id":"af9390a840a0b28a","type":"rbe","z":"97a2668540e2f3ba","name":"Report changes over ","func":"deadbandEq","gap":"50%","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":420,"y":260,"wires":[["f76f04bfb73ad8db"]]},{"id":"2a28d48e44fd6450","type":"inject","z":"97a2668540e2f3ba","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,2,1]","payloadType":"json","x":130,"y":260,"wires":[["9155ade283e76ab6"]]},{"id":"f76f04bfb73ad8db","type":"debug","z":"97a2668540e2f3ba","name":"Print changes","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":620,"y":260,"wires":[]},{"id":"9155ade283e76ab6","type":"split","z":"97a2668540e2f3ba","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":250,"y":260,"wires":[["af9390a840a0b28a"]]}] ``` :: ### Report only on one specific value As the filter node can block all messages where the change is too large, it can used to only report a single value when it occurs. For example, to only report integers that are equal to 2, the start value is set to 2 and the change cannot be greater of equal to 1. ::render-flow ```json [{"id":"6b70bed3e7360f58","type":"rbe","z":"97a2668540e2f3ba","name":"Only send 2's","func":"narrowbandEq","gap":"1","start":"2","inout":"out","septopics":false,"property":"payload","topi":"topic","x":400,"y":340,"wires":[["5f02aa2b43499ca8"]]},{"id":"a5b05fcbe6fdf53e","type":"inject","z":"97a2668540e2f3ba","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,2]","payloadType":"json","x":130,"y":340,"wires":[["e58f98ecb220d7f3"]]},{"id":"5f02aa2b43499ca8","type":"debug","z":"97a2668540e2f3ba","name":"Print the same values","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":600,"y":340,"wires":[]},{"id":"e58f98ecb220d7f3","type":"split","z":"97a2668540e2f3ba","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":250,"y":340,"wires":[["6b70bed3e7360f58"]]}] ``` :: ::node-red-help --- category: function file: rbe name: Filter node: rbe --- :: # Function ## What Is a Function node in Node-RED? In Node-RED, a function node allows you to write custom JavaScript code to process message objects in your flow. It's used for specific tasks that can't be accomplished with the standard built-in nodes alone. When you write custom JavaScript in a function node, this code gets executed every time a message passes through the node. In Node-RED, a function should either return an object, which is a message object, or nothing at all. Returning other data types instead of an object will cause an error. By default, the function node returns the message object unchanged, passing the data as it is to further nodes. Ideally, the function node should have the message object returned at the end of the code written within it. Placing the return statement in the middle of the code may result in incomplete execution of the remaining code. If the function node needs to perform an asynchronous action before sending a message, it cannot use the return statement to send the message at the end of the function. Instead, in such cases, you must use `node.send()`, as shown below: ```javascript // Simulate an asynchronous operation with setTimeout setTimeout(() => { // After 2 seconds, create a message object with some data const message = { payload: "Async operation complete" }; // Send the message to subsequent nodes node.send(message); }, 2000); ``` Additionally, if you need to pass a message object mid-script within a function node to subsequent nodes, you can utilize `node.send()` for this purpose while continuing the execution of the remaining code, as shown below: ```javascript // Extract data from the incoming message const inputData = msg.payload; // Perform some processing const processedData = inputData * 2; // Send a message object with the processed data node.send({ payload: `Processed data: ${processedData}` }); // Continue executing the rest of the code... // Example of further processing... if (processedData > 100) { node.warn("High processed data value detected!"); } else { node.log("Processed data value within normal range."); } // Return the modified message object return msg; ``` If you don't want the function to pass anything to subsequent node, you can do this by returning `null` in the function node. By default, a function node has a single output, but you can configure it to have multiple outputs in the **setup tab** with the **output** property. You can then send the message to each output using an array, placing them in order of which output they should go to. ```javascript var msg1 = { payload: 1 }; var msg2 = { payload: [3,45,2,2,4] }; var msg3 = { payload: {"name":"bob"} }; var msg4 = { payload: "This is string" }; return [msg1, msg2, msg3, msg4]; ``` ## Function Node Different Tabs In the function node, we have four different types of tabs, each with its unique use case: ### Setup - Output: This property allows you to configure how many outputs the function node will have. - Timeout: This property Allows you to define how long the function node can run before an error is raised. By default, if set to 0, no timeout is applied. - Modules: This property Allows you to add or import additional modules into Function nodes, and they will be automatically installed when the flow is deployed. However, in the - settings, you'll need to set 'functionExternalModules' to 'true'. ### On Start In this tab, you can provide code that will run whenever the node is started. This can be used to set up any state the Function node requires. ### On Message This is the tab where you can provide the JavaScript code that will execute when it receives a message passed by another nodes. ### On Stop This tab allows you to add code to clean up any ongoing tasks or close connections before the flow is redeployed. ## Logging events When a function node needs to log something, it can utilize the following methods: - `node.log()`: This is used for general logging purposes. - `node.warn()`: This method is used to log warnings. - `node.error()`: This is used to log errors. Messages logged using `node.warn()` and `node.error()` will be sent to the **debug tab**. To view messages logged using `node.log()`, you can check the command from where you started Node-RED. If you're running it under an app like PM2, it will have its own method for displaying logs. On a Raspberry Pi, the install script adds a `node-red-log` command that shows the log. If you're using FlowFuse Cloud, you can find the logged messages in the Instance's **Node-RED logs** tab. ## Node-RED Objects Accessible in Function Node The following Node-RED objects can be accessed in a function node: - node: This object encapsulates properties and methods used for customizing and interacting with nodes in a flow. - context: The node’s local context. - flow: The flow scope context. - global: The global scope context. - RED: This object provides module access to the Node-RED runtime API. - env: This object contains the get method to access environment variables. ## Use-cases and examples 1. **Custom logic**: Sometimes your flow might require very specific logic that can’t be achieved using existing nodes. Function node allow you to implement this custom logic. In the example flow below, we have a function that converts temperature data, simulated using an inject node with a random number, from Celsius to Fahrenheit. Additionally, it performs other formatting. ::render-flow ```json [{"id":"b138b603c8f94cf8","type":"inject","z":"a2240ea952051e81","name":"Temperature sensor","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"$random() * 100","payloadType":"jsonata","x":240,"y":240,"wires":[["ab41b0f6e6fa58aa"]]},{"id":"ab41b0f6e6fa58aa","type":"function","z":"a2240ea952051e81","name":"Convert Celsius to Fahrenheit","func":"// Extract temperature reading from the incoming message\nconst temperatureCelsius = msg.payload;\n\n// Convert Celsius to Fahrenheit\nconst temperatureFahrenheit = (temperatureCelsius * 9 / 5) + 32;\n\n// Round the temperature Fahrenheit to two decimal places and convert it back to a number\nconst roundedTemperatureFahrenheit = parseFloat(temperatureFahrenheit.toFixed(2));\n\n// Update the message payload with the temperature in Fahrenheit\nmsg.payload = roundedTemperatureFahrenheit;\n\n// Return the modified message object\nreturn msg;\n\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":240,"wires":[["b7271bd3259bc56d"]]},{"id":"b7271bd3259bc56d","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":240,"wires":[]},{"id":"8f553a15e4f5a8dd","type":"comment","z":"a2240ea952051e81","name":"Convert received temperature from Celsius to Fahrenheit and format it","info":"","x":510,"y":180,"wires":[]}] ``` :: 2. **Conditional Routing:** When dealing with a broad range of conditions that require intricate logic for each case, the switch node may fall short. In such scenarios, using a function node with multiple outputs can be benificial. In the example flow below, we have an inject node generating a random number and sending it to the function node. We've set up the function node to evaluate the received numeric value and perform conditional routing based on predefined ranges, sending the message to different outputs accordingly. ::render-flow ```json [{"id":"6a1f8dd5e5037d24","type":"inject","z":"a2240ea952051e81","name":"send random number","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100","payloadType":"jsonata","x":180,"y":180,"wires":[["c4ef3a83b54cd6eb"]]},{"id":"c4ef3a83b54cd6eb","type":"function","z":"a2240ea952051e81","name":"Check against dynamic ranges","func":"// Extract numeric value from the incoming message\nconst numericValue = msg.payload;\n\n// Define ranges for conditional routing\nconst range1 = { min: 0, max: 10 };\nconst range2 = { min: 11, max: 20 };\nconst range3 = { min: 21, max: 30 };\n\n// Check numeric value against ranges\nif (numericValue >= range1.min && numericValue <= range1.max) {\n // Value falls within range 1\n return [msg, null, null];\n} else if (numericValue >= range2.min && numericValue <= range2.max) {\n // Value falls within range 2\n return [null, msg, null];\n} else if (numericValue >= range3.min && numericValue <= range3.max) {\n // Value falls within range 3\n return [null, null, msg];\n} else {\n // Value falls outside all ranges\n node.warn(\"Value falls outside all defined ranges.\");\n return null;\n}\n","outputs":3,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":180,"wires":[["a3af86e82cd2267a"],["35233d68a07ad23a"],["5a99ebc4ee439757"]]},{"id":"a3af86e82cd2267a","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":140,"wires":[]},{"id":"35233d68a07ad23a","type":"debug","z":"a2240ea952051e81","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":180,"wires":[]},{"id":"5a99ebc4ee439757","type":"debug","z":"a2240ea952051e81","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"b1e8ca2faa3faafc","type":"comment","z":"a2240ea952051e81","name":"The function checks the numeric value against dynamic ranges and sends them to outputs accordingly.","info":"","x":450,"y":80,"wires":[]}] ``` :: ::node-red-help --- category: function file: 10-function name: Function node: function --- :: # Function The **Function** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [Function](https://flowfuse.com/docs/node-red/core-nodes/function/function/) - [Switch](https://flowfuse.com/docs/node-red/core-nodes/function/switch/) - [Change](https://flowfuse.com/docs/node-red/core-nodes/function/change/) - [Range](https://flowfuse.com/docs/node-red/core-nodes/function/range/) - [Template](https://flowfuse.com/docs/node-red/core-nodes/function/template/) - [Delay](https://flowfuse.com/docs/node-red/core-nodes/function/delay/) - [Trigger](https://flowfuse.com/docs/node-red/core-nodes/function/trigger/) - [Exec](https://flowfuse.com/docs/node-red/core-nodes/function/exec/) - [Filter](https://flowfuse.com/docs/node-red/core-nodes/function/filter/) # Range ## What's the Range node in Node-RED used for? The "Range" node in Node-RED allows you to map a numeric value from one range to another. For example, if you wanted to map miles to kilometers, you can specific the input range at 1 to 100 and the target range as 1 to 160. Besides unit conversion, the range node can be used for: **Data Scaling**: Use the "Range" node to scale or normalize data. For example, if you have sensor readings that range from 0 to 1023 but you want to convert them to a 0-100 percentage scale, you can use the "Range" node for this transformation. **Data Compression**: Reduce the range of data values while preserving the relationships between values. This can be useful for displaying data on a smaller scale without losing important variations. **Analog-to-Digital Conversion**: When interfacing with analog sensors, you can map the analog voltage range to a digital value range for processing. **Data Smoothing**: Smooth out data fluctuations by mapping values within a range to a single value. ## Examples for the Range node An example of a change node that converts from miles to kilometers. ![Range properties](https://flowfuse.com/docs/node-red/core-nodes/images/range-node2.png) ::render-flow ```json [{"id":"183739aecda7dc43","type":"range","z":"e92fb6c3b304fd7c","minin":"1","maxin":"100","minout":"0","maxout":"160","action":"scale","round":true,"property":"payload","name":"Miles >Km","x":390,"y":220,"wires":[["9c4edf7250c34cdb"]]}] ``` :: ::node-red-help --- category: function file: 16-range name: Range node: range --- :: # Switch ## What is the Switch node used for in Node-RED The Switch node allows you to route messages based on certain conditions. It acts as a decision-making tool within your flow, allowing you to define rules for directing messages to different output branches. Here are some common use cases for using the Switch node in Node-RED: **Message Filtering**: You can use the Switch node to filter messages based on specific criteria. For example, you might want to filter out messages that don't meet a certain threshold or that don't contain certain keywords. **Conditional Routing**: The Switch node enables you to route messages down different paths in your flow based on conditions. You can set up rules that determine which output branch a message should be sent to, depending on its content or properties. **Event Processing**: If you're working with events or data streams, the Switch node can help you process different types of events differently. For instance, you might have events related to temperature and humidity readings, and you want to process them separately. **Value Conversion**: In cases where you need to convert values from one format to another, the Switch node can route messages to different converters based on the incoming value's properties. **Error Handling**: When working with data or APIs, you might receive error messages that need to be handled differently from regular data. The Switch node can direct error messages to a separate branch for appropriate handling. **Language or Region-Based Processing**: In applications involving localization or multilingual support, the Switch node can route messages based on language or region information in the message. ## Examples ![Switch Node Example](https://flowfuse.com/docs/node-red/core-nodes/images/switch-example-2.png) ::render-flow ```json [{"id":"1401d664616fc956","type":"tab","label":"Flow 8","disabled":false,"info":"","env":[]},{"id":"inject-node","type":"inject","z":"1401d664616fc956","name":"Simulate Temperature Data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"sensorType\": \"temperature\", \"value\": 28}","payloadType":"json","x":190,"y":200,"wires":[["temperature-route-node"]]},{"id":"temperature-route-node","type":"switch","z":"1401d664616fc956","name":"Temperature Routing","property":"payload.value","propertyType":"msg","rules":[{"t":"lt","v":"25","vt":"num"},{"t":"gte","v":"25","vt":"num"}],"checkall":"true","outputs":2,"x":470,"y":200,"wires":[["below-25-node"],["above-25-node"]]},{"id":"below-25-node","type":"debug","z":"1401d664616fc956","name":"Below 25°C","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":680,"y":160,"wires":[]},{"id":"above-25-node","type":"debug","z":"1401d664616fc956","name":"Above 25°C","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":680,"y":240,"wires":[]}] ``` :: ::node-red-help --- category: function file: 10-switch name: Switch node: switch --- :: # Template The "Template" node is used to create and manipulate text templates. For example for generating HTML, configuration files, our other text based strings. The Template node allows you to generate dynamic content by injecting data into predefined templates using the Mustache templating language. When there's no need for dynamic templates, the `Format` can be set to `Plain Text`, which has a slight performance benefit. ## How the Template node works ### Input Data You can connect the Template node to a source of data, such as an MQTT input, HTTP input, or any other node that provides data. This input data will be used to populate the template. ### Template Definition In the Template node configuration, you define the template using text based formatting languages like HTML with the Mustache syntax. Mustache is a simple and "logic-less" templating language that allows you to insert variables and expressions into your template. For example, you can define a template like this: `

Hello {{payload.name}}!

`. ::render-flow ```json [{"id":"97d742eec1cb7dbf","type":"inject","z":"a6b7ede2e13fcbdf","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"name\": \"FlowFuse\"}","payloadType":"json","x":180,"y":60,"wires":[["c85d70b41f374f02"]]},{"id":"c85d70b41f374f02","type":"template","z":"a6b7ede2e13fcbdf","name":"Template using payload.name","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"

Hello {{payload.name}}!

","output":"str","x":450,"y":60,"wires":[["6df7215459dfb240"]]},{"id":"6df7215459dfb240","type":"debug","z":"a6b7ede2e13fcbdf","name":"Print \"

Hello, FlowFuse!

\"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":750,"y":60,"wires":[]}] ``` :: This example will output `

Hello FlowFuse!

`. ### Output When the Template node receives input data, it processes the data and replaces Mustache placeholders with the corresponding values from the input data. The resulting HTML or text is then sent as output to the next node in the flow. ## Building JSON The template node can be configured to parse the result of the input and template as JSON, to further use the message as an object, not as string. #### Input data ```js { payload: ['ACME', 1] } ``` #### Template node configuration - Template: ```text { "product": "{{payload.0}}", "version": {{payload.1}} } ``` - Format: `Mustache template` - Output as: `Parsed JSON` #### Output ```json { "product": "ACME", "version": 1 } ``` ### Generating JavaScript object from YAML Node-RED can also parse YAML #### Input data ```js { topic: 'FlowFuse', payload: 3000 } ``` #### template node configuration - Template: ```text options: title: {{topic}} port: {{payload}} ``` - Format: `Mustache template` - Output as: `Parsed JSON` #### Output ```json { "title": "FlowFuse", "port": 3000 } ``` ## Comments When a template gets larger, it might be useful to add comments to the template which will not appear in the output. Comments work about the same as the normal syntax, with a `!` after the opening curly brackets: ```mustache {{! this won't show }} This text will be in the output ``` Will result in `This text will be in the output`. Note there's no empty line character `\n`. ### Mustache partials Mustache by default supports partials to include. This is an unsupported feature in Node-RED. ::node-red-help --- category: function file: 80-template name: Template node: template --- :: # Trigger ## What is the Trigger Node in Node-RED? The Trigger node in Node-RED facilitates the initiation and repetition of messages at customizable intervals, offering precise control over when messages are sent, their recurrence frequency, and optional delays. This functionality is valuable for automating tasks and efficiently managing communication flow within Node-RED flows. ## Inject Node Vs Trigger Node The Inject node lets you send messages at specific intervals but it starts immediately and continues indefinitely unless manually configured to stop. With the Trigger node, you have control over when the node starts and stops sending messages. Nonetheless, both nodes possess distinct use cases and limitations. ## Configuring the Trigger Node - **Send:** Message to be passed to subsequent nodes. - **Then:** - **Wait For:**Allows sending a message when triggered and then optionally a second message. You can also set it to send nothing when triggered or for the second message. - **Extend Delay if New Message Arrives:** Enabling this option will extend the delay time if a new message is received. - **Then Send:** Allows setting the second message to be sent after a specific delay, or you can set it to send nothing. - **Send Second Message to Separate Output:** Enabling this option will add a second output to receive the second message from the trigger node. - **Resend it Every:** Allows resending a message at specific intervals of time. - **Wait to be Reset:** Selecting this option will send a message once when triggered and will wait until it is reset. If not reset, it will not send any message with the same property specified in **handling** config property. If **all messages** are selected, it will not send any message if not reset. - **Reset the Trigger if:** Allows setting msg.payload that, when received, will reset the trigger node. Alternatively, sending a message containing a reset property will reset the node (which is the default behavior). - **Override Delay with `msg.delay`:** Enabling this option will allow sending the delay time dynamically with the `msg.delay`. The value must be provided in milliseconds. - **Handling:** Allows configuring the node to treat messages as separate streams, using a `msg` property to identify each stream. Selecting "All Messages" will handle all types of messages separately. ## Trigger node Use cases: - Repetitive Tasks: If you have tasks that need to be repeated at regular intervals, such as data polling or device status checks when triggered, the Trigger node can handle this by configuring it to resend messages at specified time intervals. - Timeout Handling: You can utilize the Trigger node to manage timeouts within your flow. For example, you could trigger an action if a response is not received within a certain time frame, or set up a timeout mechanism for user interactions. - Resource Conservation: The Trigger node can conserve energy or system resources by automatically initiating actions, such as turning off lights or closing valves, after a predefined period of inactivity or completion of a task ## Examples 1. In the example flow below, we've simulated a door lock system. We employ an inject node to input a password, which is then verified against a specified password in a switch node. If the input password is correct, a trigger node sends a payload to open the door. After 4 seconds, a second message is sent to close the door. This can also be utilized for scenarios involving turning an LED on and off. ::render-flow ```json [{"id":"39333c055828b138","type":"inject","z":"39d029f8bb26b820","name":"swap card","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"pass123","payloadType":"str","x":200,"y":240,"wires":[["7fec2333b929d424"]]},{"id":"6e6bc184119065fc","type":"trigger","z":"39d029f8bb26b820","name":"","op1":"opening door","op2":"closing door","op1type":"str","op2type":"str","duration":"4","extend":false,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":540,"y":240,"wires":[["d0ccd1e7dd9e313a"]]},{"id":"7fec2333b929d424","type":"switch","z":"39d029f8bb26b820","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"pass123","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":350,"y":240,"wires":[["6e6bc184119065fc"]]},{"id":"d0ccd1e7dd9e313a","type":"debug","z":"39d029f8bb26b820","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":240,"wires":[]},{"id":"5662274e316d9317","type":"comment","z":"39d029f8bb26b820","name":"Simulating a door lock system where entering the correct password will open the door for 5 seconds and then will close.","info":"","x":480,"y":140,"wires":[]}] ``` :: 2. In the example flow below, we have a trigger node polling data continuously from an API. It polls data at specific interval when a message is received and stops if a message is received with the property of 'reset'. This can also be used in scenarios where you want to read sensor data with custom control. ::render-flow ```json [{"id":"e682b48eea38c319","type":"inject","z":"a2240ea952051e81","name":"Start polling","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":200,"wires":[["b9ae5465226eae3a"]]},{"id":"5eb6498b447e55cd","type":"inject","z":"a2240ea952051e81","name":"Stop polling","props":[{"p":"reset","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":340,"wires":[["b9ae5465226eae3a"]]},{"id":"80ad06952f6dcae7","type":"http request","z":"a2240ea952051e81","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://jsonplaceholder.typicode.com/todos/","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":690,"y":280,"wires":[["6182fd9db1a9270a"]]},{"id":"b9ae5465226eae3a","type":"trigger","z":"a2240ea952051e81","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-500","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":440,"y":280,"wires":[["80ad06952f6dcae7"]]},{"id":"6182fd9db1a9270a","type":"debug","z":"a2240ea952051e81","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":900,"y":280,"wires":[]},{"id":"04893f86fbf06dee","type":"comment","z":"a2240ea952051e81","name":"Polling data from API with control to start and stop","info":"","x":560,"y":200,"wires":[]}] ``` :: ::node-red-help --- category: function file: 89-trigger name: Trigger node: trigger --- :: # Core nodes Every node in Node-RED's default palette, grouped into the same sections the editor's palette uses. Each page opens with why you would reach for that node, then mirrors the node's built-in help. ## [Common](https://flowfuse.com/docs/node-red/core-nodes/common/) - [Inject](https://flowfuse.com/docs/node-red/core-nodes/common/inject/): Guide on the Node-RED core node that injects a message into a flow - [Debug](https://flowfuse.com/docs/node-red/core-nodes/common/debug/) - [Complete](https://flowfuse.com/docs/node-red/core-nodes/common/complete/) - [Catch](https://flowfuse.com/docs/node-red/core-nodes/common/catch/) - [Status](https://flowfuse.com/docs/node-red/core-nodes/common/status/) - [Link](https://flowfuse.com/docs/node-red/core-nodes/common/link/) - [Comment](https://flowfuse.com/docs/node-red/core-nodes/common/comment/) - [Unknown](https://flowfuse.com/docs/node-red/core-nodes/common/unknown/) ## [Function](https://flowfuse.com/docs/node-red/core-nodes/function/) - [Function](https://flowfuse.com/docs/node-red/core-nodes/function/function/) - [Switch](https://flowfuse.com/docs/node-red/core-nodes/function/switch/) - [Change](https://flowfuse.com/docs/node-red/core-nodes/function/change/) - [Range](https://flowfuse.com/docs/node-red/core-nodes/function/range/) - [Template](https://flowfuse.com/docs/node-red/core-nodes/function/template/) - [Delay](https://flowfuse.com/docs/node-red/core-nodes/function/delay/) - [Trigger](https://flowfuse.com/docs/node-red/core-nodes/function/trigger/) - [Exec](https://flowfuse.com/docs/node-red/core-nodes/function/exec/) - [Filter](https://flowfuse.com/docs/node-red/core-nodes/function/filter/) ## [Network](https://flowfuse.com/docs/node-red/core-nodes/network/) - [TLS](https://flowfuse.com/docs/node-red/core-nodes/network/tls/) - [HTTP Proxy](https://flowfuse.com/docs/node-red/core-nodes/network/http-proxy/) - [MQTT In](https://flowfuse.com/docs/node-red/core-nodes/network/mqtt-in/) - [MQTT Out](https://flowfuse.com/docs/node-red/core-nodes/network/mqtt-out/) - [HTTP in](https://flowfuse.com/docs/node-red/core-nodes/network/http-in/) - [HTTP request](https://flowfuse.com/docs/node-red/core-nodes/network/http-request/) - [WebSocket](https://flowfuse.com/docs/node-red/core-nodes/network/websocket/) - [TCP in](https://flowfuse.com/docs/node-red/core-nodes/network/tcp-in/) - [UDP In](https://flowfuse.com/docs/node-red/core-nodes/network/udp-in/) - [UDP Out](https://flowfuse.com/docs/node-red/core-nodes/network/udp-out/) ## [Sequence](https://flowfuse.com/docs/node-red/core-nodes/sequence/) - [Split](https://flowfuse.com/docs/node-red/core-nodes/sequence/split/) - [Sort](https://flowfuse.com/docs/node-red/core-nodes/sequence/sort/) - [Batch](https://flowfuse.com/docs/node-red/core-nodes/sequence/batch/) - [Join](https://flowfuse.com/docs/node-red/core-nodes/sequence/join/) ## [Parsers](https://flowfuse.com/docs/node-red/core-nodes/parsers/) - [CSV](https://flowfuse.com/docs/node-red/core-nodes/parsers/csv/) - [HTML](https://flowfuse.com/docs/node-red/core-nodes/parsers/html/) - [JSON](https://flowfuse.com/docs/node-red/core-nodes/parsers/json/) - [XML](https://flowfuse.com/docs/node-red/core-nodes/parsers/xml/) - [YAML](https://flowfuse.com/docs/node-red/core-nodes/parsers/yaml/) ## [Storage](https://flowfuse.com/docs/node-red/core-nodes/storage/) - [Write File](https://flowfuse.com/docs/node-red/core-nodes/storage/write-file/) - [Read File](https://flowfuse.com/docs/node-red/core-nodes/storage/read-file/) # HTTP in ## What are Http-in nodes used for in Node-RED The "HTTP In" node in Node-RED is a core node that allows you to create an HTTP endpoint within your flow. It essentially sets up an HTTP server that listens for incoming HTTP requests on a specified URL path and HTTP method (e.g., GET, POST). When a request is received at this endpoint, it triggers the flow and allows you to process the request and generate a response using other nodes in the flow. you can send any type of data as a response whether it is html page, JSON, string, etc. The baseurl will be the URL of the Node-RED instance at which your flow is deployed or the URL of the Node-RED editor. ## Configuring http-in node - **Method:** Specify the HTTP method (e.g., GET, POST, PUT, DELETE) that the node should listen for, for more information on [Http request methods](https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/HTTP/Methods.html){rel=""nofollow""}. - **URL:** Define the endpoint at which it should listen. The path should look like "/test", and you can also set parameters like "/test/\:id" to access them using `msg.params.id`. ## Sending response *Note: This node does not send any response to the request. The flow must include an **HTTP Response node** to complete the request.* ## Examples 1. In the example flow below, we have an HTTP In node configured with the GET method and "/test" as the URL path. This node returns an HTML page as a response when a request is received. ::render-flow ```json [{"id":"d705b6ca20481a18","type":"http in","z":"a2240ea952051e81","name":"","url":"/test","method":"get","upload":false,"swaggerDoc":"","x":220,"y":220,"wires":[["500bcf5db325f188"]]},{"id":"f74c362610a1f4dd","type":"debug","z":"a2240ea952051e81","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":780,"y":160,"wires":[]},{"id":"102ecacbf029fa61","type":"http response","z":"a2240ea952051e81","name":"","statusCode":"200","headers":{},"x":800,"y":280,"wires":[]},{"id":"500bcf5db325f188","type":"template","z":"a2240ea952051e81","name":"","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"\n\n\n\n \n \n Devs page\n\n\n\n

Hello, Devs

\n\n\n","output":"str","x":480,"y":220,"wires":[["f74c362610a1f4dd","102ecacbf029fa61"]]},{"id":"d0bd0e43011a33b1","type":"comment","z":"a2240ea952051e81","name":"The HTTP In node returns an HTML page as response when a request is received at the specified path.","info":"","x":510,"y":100,"wires":[]}] ``` :: 2. In the example flow below, we have an HTTP In node configured to return the todo item as a JSON object stored in the global context, associated with the requested ID provided as a request parameter. ::render-flow ```json [{"id":"d705b6ca20481a18","type":"http in","z":"b152a914653d9fce","name":"","url":"/todo/:id","method":"get","upload":false,"swaggerDoc":"","x":270,"y":360,"wires":[["cc28a4ae08042cd2"]]},{"id":"f74c362610a1f4dd","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":880,"y":300,"wires":[]},{"id":"102ecacbf029fa61","type":"http response","z":"b152a914653d9fce","name":"","statusCode":"200","headers":{},"x":880,"y":420,"wires":[]},{"id":"d0bd0e43011a33b1","type":"comment","z":"b152a914653d9fce","name":"The HTTP In node returns the todo item associated with the requested ID provided as a request parameter.","info":"","x":540,"y":160,"wires":[]},{"id":"255028cacf698a4f","type":"inject","z":"b152a914653d9fce","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"[ { \"id\": 1, \"task\": \"Complete homework\", \"completed\": false }, { \"id\": 2, \"task\": \"Go for a run\", \"completed\": true }, { \"id\": 3, \"task\": \"Buy groceries\", \"completed\": false } ]","payloadType":"json","x":270,"y":240,"wires":[["560484dbf9da9f27"]]},{"id":"560484dbf9da9f27","type":"change","z":"b152a914653d9fce","name":"Store simulated todo JSON in global context","rules":[{"t":"set","p":"todos","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":240,"wires":[[]]},{"id":"cc28a4ae08042cd2","type":"function","z":"b152a914653d9fce","name":"Filter data based on recived param i","func":"let id = Number(msg.req.params.id);\nlet todoList = global.get('todos');\nlet todo = todoList.filter((task)=>task.id===id);\nmsg.payload = todo;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":360,"wires":[["f74c362610a1f4dd","102ecacbf029fa61"]]}] ``` :: 3. In the example flow below, we have an HTTP In node configured with the POST method and "/todo" as the URL path. When a POST request containing a todo JSON object is received, it stores it in the todo list within the global context. ::render-flow ```json [{"id":"203195252f71d9f4","type":"http in","z":"b152a914653d9fce","name":"","url":"/todo","method":"post","upload":true,"swaggerDoc":"","x":220,"y":280,"wires":[["93df3c07ae4ad228","995da14e2a688758"]]},{"id":"93df3c07ae4ad228","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":500,"y":240,"wires":[]},{"id":"eb447a5a61f6654d","type":"http response","z":"b152a914653d9fce","name":"","statusCode":"201","headers":{},"x":820,"y":320,"wires":[]},{"id":"995da14e2a688758","type":"function","z":"b152a914653d9fce","name":"store todo in todolist ","func":"let todoList = global.get('todos') || [];\nlet newTodo = msg.payload;\n\ntodoList.push(newTodo);\nglobal.set('todos',todoList)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":320,"wires":[["eb447a5a61f6654d"]]},{"id":"503937c4fc8b7902","type":"comment","z":"b152a914653d9fce","name":"The HTTP In node stores the todo object in the todolist when a POST request with a todo object is received.","info":"","x":520,"y":180,"wires":[]}] ``` :: ## Output - **payload:** For a GET request, contains an object of any query string parameters. Otherwise, contains the body of the HTTP request. - **req object:**An HTTP request object. This object contains multiple properties that provide information about the request. - **body:** The body of the incoming request. The format will depend on the request. - **headers:** An object containing the HTTP request headers. - **query:** An object containing any query string parameters. - **params:** An object containing any route parameters. - **cookies:** An object containing the cookies for the request. - **files:** If enabled within the node, an object containing any files uploaded as part of a POST request. - **res object:** An HTTP response object. This property should not be used directly; ::node-red-help --- category: network file: 21-httpin name: HTTP in node: http in --- :: # HTTP Proxy ## What are HTTP Proxy Nodes in Node-RED? The HTTP Proxy Config node in Node-RED allows you to configure settings for an HTTP proxy server. It enables routing outgoing HTTP requests through a specified proxy. This is useful for scenarios where internet access requires passing through a proxy server. However, this node is not directly available in the Node-RED palette; it is accessible within the configuration settings of some Node-RED core nodes and certain custom nodes used for facilitating network communication, such as HTTP request node, etc. ### What is an HTTP Proxy? An HTTP proxy is a server that sits between a client and one or more other servers. The HTTP proxy intercepts all HTTP(S) requests from that client and decides to which server the request needs to be forwarded. Once the server has responded, the HTTP proxy returns the response to the client. ### Why Use a Proxy When Requesting to Other Services? - **Privacy:** Proxy servers can hide the requester's IP address, enhancing privacy online. - **Security and Encryption:** Proxies can provide an additional layer of security by encrypting communication between the client and the destination server. This helps safeguard sensitive data from potential threats or eavesdropping. ## Configuring the HTTP Proxy Config Node - **URL:** The URL of the proxy server through which outgoing HTTP requests will be routed. This could be an IP address or a domain name. - **Use Proxy Authentication:** Whether the proxy server requires authentication credentials for access. If this option is enabled, you'll need to provide a username and password. - **Ignore Hosts:** An optional setting that allows you to specify hosts that should bypass the proxy and connect directly to the destination server. This can be useful for accessing local resources or services that don't need to pass through the proxy. *Note: When accessing to the host in the ignored host list, no proxy will be used.* ::node-red-help --- category: network file: 06-httpproxy name: HTTP Proxy node: http proxy --- :: # HTTP request ## What are HTTP request nodes used for in Node-RED In Node-RED, an HTTP Request node allows you to make HTTP requests to external servers or services. This allows you to interact with web services, APIs, or any other HTTP-based endpoints. When you configure an HTTP Request node, you typically specify the method (GET, POST, PUT, DELETE, among others), the URL of the endpoint you want to communicate with, any headers you need to include, and the payload if applicable. Once configured, this node will send the HTTP request when triggered by an incoming message or event. ## Configuring HTTP Request node Below, you'll find a range of settings to tailor HTTP requests to fit the needs of different APIs or web services. Depending on the service you're working with, some options might be crucial, while others could be optional. - **Method:** Select the HTTP method for the request (e.g., GET, POST, PUT, DELETE). You can dynamically set it using `msg.method`. - **URL:** Specify the endpoint URL to communicate with. Dynamic URL setting is allowed using `msg.url`. additionally, if you want to construct a URL with the message's property you can utilize Mustache-style tags with double braces `{{ }}`. For example, `example.com/{{topic}}`, where the value of `msg.topic` will be automatically inserted. Using double braces `{{ }}` performs HTML escaping by default, so special characters in the substituted value will be escaped. However, if you want to preserve special characters like `/` and `&` in the constructed URL, you can use triple braces `{{{ }}}`. - **Payload:**Allows to choose how received payload from the previous node will be sent with the request: - **Ignore:** If enabled Payload will be ignored. - **Append to query-string parameter:** Enabling this option will Allow sending URL query string parameters using `msg.payload`. - **Send as request:** Send payload data as part of the request body. - **Enable Secure Connection:** Allows to activate SSL/TLS for secure communication. TLS configuration options are available, For more information refer to [TLS config node](https://flowfuse.com/docs/node-red/core-nodes/network/tls/). - **Use Authentication:**If required, allow to provide credentials for authentication. - **Type:**Select the authentication type. - **basic:**Uses Basic authentication where the username and password are sent in the request headers in Base64-encoded form. - **Username:** Provide the username for authentication. - **Password:** Provide the password for authentication. - **digest:** Uses Digest authentication, which is more secure than Basic authentication as it sends hashed passwords rather than plaintext. - **bearer:**Uses Bearer token authentication where a bearer token, typically a JSON Web Token (JWT), is sent in the Authorization header. - **Token:** Provide the bearer token if bearer authentication is selected. - **Enable Connection Keep-Alive:** Enabling this option will allow Maintain persistent connections for efficiency. - **Use Proxy:** Allows to Route requests through a proxy server if necessary, for more information on the configuration of [HTTP Proxy](https://flowfuse.com/docs/node-red/core-nodes/network/http-proxy/) config node - **Only send non-2xx responses to Catch node:** Enabling this option will send only non-success responses to the Catch node. - **Disable Strict HTTP Parsing:** Enabling this option relaxes how Node-RED interprets HTTP responses. It's handy when dealing with responses that don't perfectly match the standard HTTP format. - **Return:**Allows to Choose the format for response data conversion - **A UTF-8 string:** Return response data as a UTF-8 string. - **A binary buffer:** Return response data as a binary buffer. - **A parsed JSON object:** Parse response data as JSON and return the object. - **Headers:** Allows to Add headers to the HTTP request such as content-type, accept, user agent, etc. You can dynamically set headers using `msg.headers`. However Reset `msg.headers` to avoid unintended header inheritance when using multiple HTTP request nodes in the same flow. Moreover, If `msg.payload` is an Object, the node automatically sets the `Content-Type` to `application/json`. ## Usecase 1. **API Integration:** The HTTP request node allows seamless integration with external APIs. Developers can utilize it to fetch data from APIs using GET requests or send data to APIs using POST/PUT requests. For instance, fetching weather data from a weather API or posting data to a messaging service like Slack are common scenarios. 2. **Webhooks:** With the HTTP request node, users can set up webhooks to trigger actions in response to specific events. This enables real-time communication between different applications. For example, triggering a webhook to notify a third-party service when certain conditions are met or when data is received from a sensor. For more information, refer to [Using webhook with Node-RED](https://flowfuse.com/docs/node-red/integration-technologies/webhook/) 3. **Remote Control and Device Management:** In smart home systems, the HTTP request node can be used to facilitates remote device management. It allows users to control various devices such as lights, thermostats, and security cameras via web or mobile interfaces by interacting with device APIs. Actions like toggling devices, adjusting settings, and receiving real-time updates can be achieved through the HTTP request node. These are a few use cases of the HTTP Request node, but its ability to communicate with other services is a significant and core capability. This capability alone opens the door to a diverse array of different use cases. ## Examples 1. Below is an example showing how you can construct a URL with message properties in the HTTP request node, an example includes sending a GET request. ::render-flow ```json [{"id":"b152a914653d9fce","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"dce407bcce963567","type":"inject","z":"b152a914653d9fce","name":"Get post ","props":[{"p":"topic","v":"1","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":200,"y":220,"wires":[["ea91769386ca7b48"]]},{"id":"ea91769386ca7b48","type":"http request","z":"b152a914653d9fce","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://jsonplaceholder.typicode.com/posts/{{topic}}","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":490,"y":220,"wires":[["2f597047663e2964"]]},{"id":"2f597047663e2964","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"9f40509b6642d082","type":"comment","z":"b152a914653d9fce","name":"The HTTP request node retrieves posts from a mock API using the ID passed as a query parameter with `msg.topic` by the inject node.","info":"","x":510,"y":140,"wires":[]}] ``` :: 2. Below is an example showing how you can send an HTTP POST request to a mock API. This example includes registering a user. ::render-flow ```json [{"id":"dce407bcce963567","type":"inject","z":"b152a914653d9fce","name":"Register With Mock API","props":[{"p":"payload.email","v":"eve.holt@reqres.in","vt":"str"},{"p":"payload.password","v":"password","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":220,"y":220,"wires":[["ea91769386ca7b48"]]},{"id":"ea91769386ca7b48","type":"http request","z":"b152a914653d9fce","name":"","method":"POST","ret":"txt","paytoqs":"ignore","url":"https://reqres.in/api/login","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":490,"y":220,"wires":[["2f597047663e2964"]]},{"id":"2f597047663e2964","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"9f40509b6642d082","type":"comment","z":"b152a914653d9fce","name":"The HTTP request node sends a POST request to register a user.","info":"","x":430,"y":140,"wires":[]}] ``` :: 3. Below is an example demonstrating how you can dynamically set the URL, method, and headers for the HTTP request node. Additionally, this example illustrates sending a GET request to a mock API with an authorization token. ::render-flow ```json [{"id":"dce407bcce963567","type":"inject","z":"b152a914653d9fce","name":"make request ","props":[{"p":"url","v":"https://postman-echo.com/basic-auth","vt":"str"},{"p":"method","v":"GET","vt":"str"},{"p":"headers.Authorization","v":"Basic cG9zdG1hbjpwYXNzd29yZA","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":210,"y":220,"wires":[["ea91769386ca7b48"]]},{"id":"ea91769386ca7b48","type":"http request","z":"b152a914653d9fce","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":490,"y":220,"wires":[["2f597047663e2964"]]},{"id":"2f597047663e2964","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"9f40509b6642d082","type":"comment","z":"b152a914653d9fce","name":"The HTTP request node retrieves posts from a mock API using the ID passed as a query parameter with `msg.topic` by the inject node.","info":"","x":510,"y":140,"wires":[]}] ``` :: 4. Below is an example showing how you can send a PUT request using the HTTP request node. ::render-flow ```json [{"id":"dce407bcce963567","type":"inject","z":"b152a914653d9fce","name":"Update post","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"id\":1,\"title\":\"foo\",\"body\":\"bar\",\"userId\":1}","payloadType":"json","x":210,"y":220,"wires":[["ea91769386ca7b48"]]},{"id":"ea91769386ca7b48","type":"http request","z":"b152a914653d9fce","name":"","method":"PUT","ret":"obj","paytoqs":"ignore","url":"https://jsonplaceholder.typicode.com/posts/1","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":490,"y":220,"wires":[["2f597047663e2964"]]},{"id":"2f597047663e2964","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"9f40509b6642d082","type":"comment","z":"b152a914653d9fce","name":"The HTTP request node sends a PUT request to update the post.","info":"","x":470,"y":140,"wires":[]}] ``` :: 5. Below is an example showing how you can send a DELETE request using the HTTP request node. ::render-flow ```json [{"id":"dce407bcce963567","type":"inject","z":"b152a914653d9fce","name":"delete post","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":200,"y":220,"wires":[["ea91769386ca7b48"]]},{"id":"ea91769386ca7b48","type":"http request","z":"b152a914653d9fce","name":"","method":"DELETE","ret":"obj","paytoqs":"ignore","url":"https://jsonplaceholder.typicode.com/posts/1","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":490,"y":220,"wires":[["2f597047663e2964"]]},{"id":"2f597047663e2964","type":"debug","z":"b152a914653d9fce","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]},{"id":"9f40509b6642d082","type":"comment","z":"b152a914653d9fce","name":"The HTTP request node sends a DELETE request to delete a post.","info":"","x":440,"y":140,"wires":[]}] ``` :: ## Output - **payload:** The body of the response can be returned as a string, parsed JSON object, or a binary buffer. - **statusCode:** Indicates the status code of the response or the error code if the request couldn't be completed. - **headers:** An object containing the response headers. - **responseURL:** Provides the final redirected URL if any redirects occurred during processing; otherwise, it shows the URL of the original request. - **responseCookies:** If the response includes cookies, this property is an object containing name/value pairs for each cookie. - **redirectList:** Accumulated information about redirects, including the next redirect destination (`location`) and cookies returned from the redirect source. ::node-red-help --- category: network file: 21-httprequest name: HTTP request node: http request --- :: # Network The **Network** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [TLS](https://flowfuse.com/docs/node-red/core-nodes/network/tls/) - [HTTP Proxy](https://flowfuse.com/docs/node-red/core-nodes/network/http-proxy/) - [MQTT In](https://flowfuse.com/docs/node-red/core-nodes/network/mqtt-in/) - [MQTT Out](https://flowfuse.com/docs/node-red/core-nodes/network/mqtt-out/) - [HTTP in](https://flowfuse.com/docs/node-red/core-nodes/network/http-in/) - [HTTP request](https://flowfuse.com/docs/node-red/core-nodes/network/http-request/) - [WebSocket](https://flowfuse.com/docs/node-red/core-nodes/network/websocket/) - [TCP in](https://flowfuse.com/docs/node-red/core-nodes/network/tcp-in/) - [UDP In](https://flowfuse.com/docs/node-red/core-nodes/network/udp-in/) - [UDP Out](https://flowfuse.com/docs/node-red/core-nodes/network/udp-out/) # MQTT In This node connects to an MQTT broker and subscribes to messages from a specified topic. ## Configuration Options ### Server Configuration The MQTT server is automatically configured and managed by FlowFuse. When this node is added to the canvas, a corresponding MQTT broker client is created automatically. The connection settings are handled internally, requiring no manual configuration. ### Topic Defines the topic to subscribe to. The topic can include MQTT wildcards: - `+` for a single-level wildcard - `#` for a multi-level wildcard ### QoS (Quality of Service) Specifies the message delivery guarantee level: - 0: Fire and forget - 1: At least once - 2: Once and once only (default) If not defined, the default value is 0. ## Output Properties When a message is received, the node outputs the following properties: - `msg.payload`: The message content. Strings are passed as-is, and binary data is output as a Buffer. - `msg.topic`: The topic on which the message was received. - `msg.qos`: The QoS level of the received message. - `msg.retain`: True if the message was retained on the broker. - `msg.responseTopic`: MQTTv5 response topic. - `msg.correlationData`: MQTTv5 correlation data. - `msg.contentType`: MQTTv5 content type of the payload. - `msg.userProperties`: MQTTv5 user properties. - `msg.messageExpiryInterval`: MQTTv5 message expiry time in seconds. ## Dynamic Subscription Control The MQTT In node can be configured to dynamically manage connections and topic subscriptions. When this feature is enabled, the node accepts control messages through its input. ### Input Properties These inputs only apply when dynamic subscriptions are enabled: `msg.action`: Defines the action to perform. Supported actions include: `"connect"`, `"disconnect"`, `"getSubscriptions"`, `"subscribe"`, and `"unsubscribe"`. - For `"connect"`, `msg.broker` this can override broker configuration properties such as: - `broker` - `port` - `url` (overrides broker and port) - `username` - `password` - `clientid` - `cleansession` :brIf a broker is already connected, an error is logged unless `force` is specified. In that case, the node disconnects, applies the new settings, and reconnects with the updated configuration. - For `"subscribe"` or `"unsubscribe"`, `msg.topic` specifies the target topic(s). This can be: - A string containing a single topic filter. - An object containing `topic` and `qos` properties. - An array of strings or objects for multiple topics. #### Subscribe :brSubscribes to one or more topics. ```javascript // Single topic msg.action = 'subscribe'; msg.topic = 'sensors/temperature'; // Single topic with QoS msg.action = 'subscribe'; msg.topic = { topic: 'sensors/temperature', qos: 1 }; // Multiple topics msg.action = 'subscribe'; msg.topic = ['sensors/temperature', 'sensors/humidity']; // Multiple topics with QoS msg.action = 'subscribe'; msg.topic = [ { topic: 'sensors/temperature', qos: 1 }, { topic: 'sensors/humidity', qos: 2 } ]; ``` #### Unsubscribe :brRemoves subscriptions from one or more topics. ```javascript // Single topic msg.action = 'unsubscribe'; msg.topic = 'sensors/temperature'; // Multiple topics msg.action = 'unsubscribe'; msg.topic = ['sensors/temperature', 'sensors/humidity']; ``` - For `getSubscriptions` the `msg.payload` output is an array of subscription objects holding the `topic` and `qos` level ```javascript [ { topic: 'sensors/temperature', qos: 1 }, { topic: 'sensors/humidity', qos: 2 } ] ``` ## Example: Cheerlights This example subscribes to the public topic `cheerlights/coloured/hex` on the Mosquitto test broker. :br Each time a new color is published, the color code is displayed in the Debug panel. ::render-flow ```json [{"id":"0e7402215650517c","type":"mqtt in","z":"a149bb66646389a3","name":"Cheerlights","topic":"cheerlights/coloured/hex","qos":"2","datatype":"auto-detect","broker":"037ca6b6ca0d7699","nl":false,"rap":true,"rh":0,"inputs":0,"x":220,"y":260,"wires":[["6173b5c60df9bfee"]]},{"id":"6173b5c60df9bfee","type":"debug","z":"a149bb66646389a3","name":"Cheerlights Debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":530,"y":260,"wires":[]},{"id":"037ca6b6ca0d7699","type":"mqtt-broker","name":"Public Mosquitto Broker","broker":"test.mosquitto.org","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true}] ``` :: ## Notes - The MQTT In node automatically reconnects if the connection to the broker is lost. - Retained messages are received immediately when subscribing to a topic that has one. - The node can work alongside the MQTT Out node for bi-directional communication. ::node-red-help --- category: network file: 10-mqtt name: MQTT In node: mqtt in --- :: # MQTT Out The MQTT Out node connects to an MQTT broker and publishes messages to one or more topics. :br It is typically used to send sensor data, commands, or event notifications from Node-RED to external systems or devices that subscribe to MQTT topics. ## How it works When a message arrives at the node input, it publishes the content of `msg.payload` to the specified topic on the broker. :br If no topic is set in the node, it must be provided in `msg.topic`. If `msg.payload` is not set, no message will be sent. :br To send an empty message, set `msg.payload` to an empty string (`""`). ## Configuration The MQTT Out node requires a connection to an MQTT broker. :br This can be configured by clicking the pencil icon next to the **Server** field. :br Multiple MQTT nodes (in or out) can share the same broker configuration. ### Server Defines the broker connection details: - Broker address (for example, `test.mosquitto.org`) - Port (default 1883 for non-TLS, 8883 for TLS) - Optional username and password - MQTT version (v3.1, v3.1.1, or v5) - Enable TLS for encrypted connections - Client ID and session options ### Topic Specifies the MQTT topic to publish to. :br You can enter a fixed topic or leave it blank to use `msg.topic` dynamically. ### QoS Defines the message delivery quality level: - 0 – fire and forget - 1 – at least once - 2 – once and once only (default) The QoS can be overridden at runtime using `msg.qos`. ### Retain If set to true, the broker will retain the last message sent to the topic and deliver it to new subscribers immediately. :br You can override this in the flow using `msg.retain`. To clear a retained topic, send a blank message (`msg.payload = ""`) with `msg.retain = true`. ## Input properties The MQTT Out node accepts the following properties in incoming messages: - `msg.payload` (string | buffer) – the message payload to publish. :br If it contains an object, it is automatically converted to a JSON string. :br If it contains a buffer, it is sent as-is. - `msg.topic` (string) – the topic to publish to. Required if not defined in the node. - `msg.qos` (number) – overrides the configured QoS (0, 1, or 2). - `msg.retain` (boolean) – overrides the retain flag. ### MQTT v5 properties If the broker and node are using MQTT version 5, the following properties can also be set: - `msg.responseTopic` (string) – the MQTT response topic for the message - `msg.correlationData` (buffer) – correlation data for the message - `msg.contentType` (string) – the content type of the payload - `msg.userProperties` (object) – any user-defined properties - `msg.messageExpiryInterval` (number) – expiry time, in seconds - `msg.topicAlias` (number) – topic alias to use ## Dynamic control The MQTT Out node can also respond to special control messages to manage the connection dynamically. :br If one of these control messages is received, the node will perform the action but will not publish the payload. ### `msg.action` Defines the action to perform. Supported actions: - `connect` – establish a connection to the broker - `disconnect` – close the current connection ### `msg.broker` For the **connect** action, this property can override broker configuration options dynamically, including: - `broker` - `port` - `url` (overrides both broker and port) - `username` - `password` If the node is already connected and new settings are provided, it will log an error unless the property `force` is set to true. :br In that case, it will disconnect, apply the new configuration, and reconnect. ## Example: Simple Publish This example shows how to publish a timestamp to an MQTT topic on the public Mosquitto test broker. ::render-flow ```json [{"id":"c1b20f45c3c3e77e","type":"tab","label":"MQTT Publish Example","disabled":false,"info":"","env":[]},{"id":"e5b31919a35d7f51","type":"inject","z":"c1b20f45c3c3e77e","name":"Inject Timestamp","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"example/time","payload":"","payloadType":"date","x":180,"y":120,"wires":[["6cd12cecb82889e2"]]},{"id":"6cd12cecb82889e2","type":"mqtt out","z":"c1b20f45c3c3e77e","name":"MQTT Out","topic":"","qos":"1","retain":"false","broker":"da4d8b90.3a89d8","x":430,"y":120,"wires":[]},{"id":"da4d8b90.3a89d8","type":"mqtt-broker","name":"Public Mosquitto Broker","broker":"test.mosquitto.org","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true}] ``` :: ## Notes - The node automatically converts objects to JSON strings when publishing. - For large payloads or binary data, use buffers to avoid unnecessary conversion. - Multiple MQTT Out nodes can share the same broker connection. - MQTT v5 users can take advantage of additional properties for richer message metadata. ::node-red-help --- category: network file: 10-mqtt name: MQTT Out node: mqtt out --- :: # TCP in ## What are TCP-In nodes used for in Node-RED The TCP node in Node-RED allows you to establish connections to remote TCP ports, serving as a TCP client for communication with external services or devices. Additionally, it facilitates the creation of a TCP server that can accept incoming connections. This functionality supports various applications, such as interacting with web servers or receiving data streams from IoT devices. Whether you're working on IoT projects, industrial automation, or networked systems, this node seamlessly integrates TCP/IP communication into your Node-RED workflows. ### What is TCP TCP (Transmission Control Protocol) is one of the core protocols of the Internet Protocol Suite (commonly known as TCP/IP). It is a connection-oriented protocol that provides reliable, ordered, and error-checked delivery of data packets over a network. TCP guarantees the delivery of data packets in the same order they were sent. For more information on TCP, refer to [RFC 9293](https://www.ietf.org/rfc/rfc9293.html){rel=""nofollow""}. ## Configuring TCP-In Node - **Type:** - **Listen on:** Allows the TCP node to act as a server, listening for incoming connections. - **Connect to:** Enables the TCP node to act as a client, establishing connections to remote servers. - **Hostname:** Specifies the hostname or IP address of the remote server when using the "Connect to" type. - **Port:** Specifies the TCP port number to listen on (when using "Listen on" type) or to connect to (when using "Connect to" type). - **Enable secure (SSL/TLS) connection:** Enabling this option activates SSL/TLS for secure communication. In Node-RED we have TLS config node which allows to activate TLS secure communication, refer to [TLS config node](https://flowfuse.com/docs/node-red/core-nodes/network/tls/) for details on configurations. - **Output:** - **Streams of:** Selecting this option will outputs the data stream received from the TCP connection as a continuous stream of messages. - **Single:** Selecting this option will outputs a single message containing the data received from the TCP connection. - **Payloads:**Stream or single message of the data you're sending/receiving: - **buffer:** Data is sent/received as a buffer object. - **String:** Data is sent/received as a string. - **Base64 String:**Data is sent/received as a Base64 String. - **delimited by:** Specify the delimiter for splitting incoming data streams. Specify the delimiter for splitting incoming data streams. Commonly, `,`, `\r`, `\n`. - **re-attach delimiter:** Enabling this option will reattach the delimiter to its original place. **Note: The default TCP nodes have been removed from the Node-RED palette in the FlowFuse Cloud due to limitations in routing connections to the container running Node-RED inside the FlowFuse platform** ## Usecases **Communicating with Servers:** The TCP-In node allows Node-RED to interact with various servers through TCP/IP communication. This enables applications such as fetching data from web servers or exchanging information with other networked services. **Integration with TCP-based Devices:** Node-RED can integrate seamlessly with TCP-based devices like industrial sensors, PLCs (Programmable Logic Controllers), or custom hardware controllers. The TCP-In node enables bidirectional communication, facilitating tasks such as sending commands to devices or receiving real-time data streams. ## Examples 1. In the example flow below, we create a basic TCP server using the tcp-in node. ::render-flow ```json [{"id":"a8c5eab2876f058e","type":"group","z":"5b972161c4e0464e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e286c8cf0f18b990","279de14c4fcb638c","15e102f80cf28535","c4d2ad15a31bd520","aaea81c136a207e0","64a84ea2a47d4ccf"],"x":194,"y":179,"w":672,"h":222},{"id":"e286c8cf0f18b990","type":"tcp in","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"TCP Server","server":"server","host":"","port":"2000","datamode":"stream","datatype":"utf8","newline":"","topic":"","trim":false,"base64":false,"tls":"","x":330,"y":280,"wires":[["aaea81c136a207e0"]]},{"id":"279de14c4fcb638c","type":"inject","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Send data to tcp server","props":[{"p":"payload"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":350,"y":360,"wires":[["15e102f80cf28535"]]},{"id":"15e102f80cf28535","type":"tcp request","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"TCP request","server":"127.0.0.1","port":"2000","out":"sit","ret":"string","splitc":" ","newline":"","trim":false,"tls":"","x":570,"y":360,"wires":[["c4d2ad15a31bd520"]]},{"id":"c4d2ad15a31bd520","type":"debug","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":760,"y":360,"wires":[]},{"id":"aaea81c136a207e0","type":"debug","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":720,"y":280,"wires":[]},{"id":"64a84ea2a47d4ccf","type":"comment","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Creating TCP server uisng TCP In node","info":"","x":510,"y":220,"wires":[]}] ``` :: 2. In the example flow below, we demonstrate how to utilize the TCP-In node alongside other TCP nodes to enable bidirectional communication. ::render-flow ```json [{"id":"a8c5eab2876f058e","type":"group","z":"5b972161c4e0464e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e286c8cf0f18b990","279de14c4fcb638c","15e102f80cf28535","c4d2ad15a31bd520","aaea81c136a207e0","64a84ea2a47d4ccf","bb1fcbe5804228d7","f0962665b353693f","e9d80655226685f3","0d400b0bcd290e30","dc8d47a4e642899f","4ed5c064fef77a4b"],"x":194,"y":199,"w":672,"h":382},{"id":"e286c8cf0f18b990","type":"tcp in","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"TCP Server","server":"server","host":"","port":"2000","datamode":"stream","datatype":"utf8","newline":"","topic":"","trim":false,"base64":false,"tls":"","x":290,"y":300,"wires":[["aaea81c136a207e0"]]},{"id":"279de14c4fcb638c","type":"inject","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Send data to tcp server","props":[{"p":"payload"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":350,"y":420,"wires":[["15e102f80cf28535"]]},{"id":"15e102f80cf28535","type":"tcp request","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"TCP request","server":"127.0.0.1","port":"2000","out":"sit","ret":"string","splitc":" ","newline":"","trim":false,"tls":"","x":570,"y":420,"wires":[["c4d2ad15a31bd520"]]},{"id":"c4d2ad15a31bd520","type":"debug","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":760,"y":420,"wires":[]},{"id":"aaea81c136a207e0","type":"debug","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":740,"y":300,"wires":[]},{"id":"64a84ea2a47d4ccf","type":"comment","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Creating TCP server uisng TCP In node","info":"","x":530,"y":240,"wires":[]},{"id":"bb1fcbe5804228d7","type":"tcp in","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"TCP Server","server":"server","host":"","port":"2000","datamode":"stream","datatype":"utf8","newline":"","topic":"","trim":false,"base64":false,"tls":"","x":290,"y":440,"wires":[[]]},{"id":"f0962665b353693f","type":"debug","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":680,"y":460,"wires":[]},{"id":"e9d80655226685f3","type":"inject","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":310,"y":540,"wires":[["0d400b0bcd290e30"]]},{"id":"0d400b0bcd290e30","type":"tcp out","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"","host":"","port":"","beserver":"reply","base64":false,"end":false,"tls":"","x":750,"y":540,"wires":[]},{"id":"dc8d47a4e642899f","type":"comment","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Sending data to tcp server","info":"","x":510,"y":340,"wires":[]},{"id":"4ed5c064fef77a4b","type":"comment","z":"5b972161c4e0464e","g":"a8c5eab2876f058e","name":"Sending data to client ","info":"","x":500,"y":500,"wires":[]}] ``` :: ::node-red-help --- category: network file: 31-tcpin name: TCP in node: tcp in --- :: # TLS ## What is the TLS config node in Node-RED? In Node-RED, the TLS Config node is used to configure Transport Layer Security (TLS) settings for secure communication over networks. TLS ensures that data transmitted between applications, servers, and devices is encrypted and secure. However, this node is not directly available in the Node-RED palette; it is accessible within the configuration settings of some Node-RED core nodes and certain custom nodes used for facilitating network communication, such as HTTP Request, TCP-In, and custom nodes like Kafka, MongoDB, etc. ## Configuring TLS config node - **Use key and certificates from local files:** Enabling this option will allow you to enter the path of the certificate files if not, allows to upload directly from the device - **Certificate:** the server's certificate( PEM FORMAT) . - **Private Key:** the private key associated with the certificate ( PEM FORMAT). - **Passphrase (optional):** If the private key is encrypted, provide the passphrase. - **CA Certificate:** Optionally provide a CA certificate for certificate verification ( PEM FORMAT). - **Verify server certificate:** Enabling this option will verify the server certificate. - **Server Name:** Specify the server name for SNI (Server Name Indication). - **ALPN Protocol:** Specify the ALPN (Application-Layer Protocol Negotiation) protocol. Additionally, it's worth noting that TLS configuration details can also be provided by cloud services such as Kafka broker, MQTT, or databases when using cloud-based solutions. These services often offer TLS configuration options as part of their service settings, which you can integrate with Node-RED as needed. ## Usecases The TLS Config node primarily facilitates secure communication over the network. Below are some scenarios: **Web Server Security:** When building web servers using Node-RED, it's essential to secure communication between clients and the server. The TLS Config node can be used to configure TLS settings for nodes such as HTTP, TCP, enabling HTTPS communication and encrypting data exchanged between clients and the server. **Database Connection Security:** Node-RED is frequently used to interact with databases for storing and retrieving data. When connecting to databases, such as MongoDB or MySQL, it's crucial to ensure that data is transmitted securely. The TLS Config node can be used to configure TLS settings for database nodes, securing communication with the database server. ::node-red-help --- category: network file: 05-tls name: TLS node: tls --- :: # UDP In ## What is udp-in node in Node-RED? The UDP-In node in Node-RED enables the reception of UDP messages from remote devices or services. It acts as a listener, waiting for incoming UDP packets on a specified port. This functionality is crucial for real-time applications, such as IoT data ingestion and network communication. Whether you're receiving sensor data from IoT devices or communicating with other networked systems, the UDP-In node seamlessly integrates UDP communication into your Node-RED workflows, providing a lightweight and efficient solution for data reception. ### What is UDP? UDP (User Datagram Protocol) is a connectionless protocol in the Internet Protocol suite. It transmits data packets, or datagrams, without establishing a connection, prioritizing speed over reliability. Commonly used for real-time applications like video streaming, online gaming, and VoIP. UDP's simplicity reduces latency but doesn't guarantee delivery of data packets. For more information refer to [UDP MDN Docs](https://developer.mozilla.org/en-US/docs/Glossary/UDP){rel=""nofollow""}. ## Configuring UDP-In Node The UDP-In node in Node-RED provides versatile configuration options to tailor UDP message reception according to specific requirements: - **Listen for:** - **UDP messages:** Receive standard UDP messages from remote devices or services. - **Multicast messages:**Listen for multicast messages, allowing communication with multiple recipients simultaneously. - **Group:** Specify the multicast group address. - **Local IF:** Choose the network interface to use for receiving multicast messages. - **On port:** Define the port number on which the UDP-In node will listen for incoming messages. - **Using:** - **IPv4:** Utilize IPv4 addressing for communication. - **IPv6:** Utilize IPv6 addressing for communication, supporting the latest IP version. - **Output:** Choose the format for the received data: - **As a buffer:** Receive messages as buffer objects. - **As a string:** Receive messages as strings. - **As a base64 encoded string:** Receive messages encoded in base64 format. *Note: On some systems, you may need root or administrator access to use ports below 1024 and/or broadcast, and have to ensure your firewall allows the data in.* **Note: The default UDP nodes have been removed from the Node-RED palette in the FlowFuse Cloud due to limitations in routing connections to the container running Node-RED inside the FlowFuse platform** ## Usecases - **Sensor data acquisition:** Receive real-time data from IoT sensors deployed in the field, such as temperature, humidity, or motion sensor readings. - **Device status monitoring:** Monitor the operational status of IoT devices, such as connected appliances or industrial machinery, by receiving status updates over UDP. - **Environmental monitoring:** Collect environmental data from IoT devices installed in remote locations, such as air quality sensors or weather stations, for analysis and decision-making. - **Asset tracking:** Receive location data from IoT devices equipped with GPS or RFID technology to track the movement of assets, vehicles, or livestock in real-time. - **Media stream reception:** Receive media streams, such as video or audio content, for applications like CCTV surveillance, live broadcasting, or multimedia communication. ## Example 1. In the example below, we have a UDP-In node configured to receive data sent over localhost and port 90, using a UDP-Out node. ::render-flow ```json [{"id":"8b65196d8e0682a7","type":"group","z":"5b972161c4e0464e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["c69190416293c2c5","1448bee95281f5bb","4d31d05731f0fa6c","cd0155779b527eb2","9b0fd28ce351cbee","53660b468f150faa"],"x":374,"y":59,"w":472,"h":262},{"id":"c69190416293c2c5","type":"inject","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":490,"y":160,"wires":[["1448bee95281f5bb"]]},{"id":"1448bee95281f5bb","type":"udp out","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","addr":"127.0.0.1","iface":"","port":"90","ipv":"udp4","outport":"","base64":false,"multicast":"false","x":730,"y":160,"wires":[]},{"id":"4d31d05731f0fa6c","type":"udp in","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","iface":"","port":"90","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":470,"y":280,"wires":[["cd0155779b527eb2"]]},{"id":"cd0155779b527eb2","type":"debug","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":720,"y":280,"wires":[]},{"id":"9b0fd28ce351cbee","type":"comment","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"Sending data to client","info":"","x":600,"y":100,"wires":[]},{"id":"53660b468f150faa","type":"comment","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"Receving data from server ","info":"","x":610,"y":220,"wires":[]}] ``` :: ## Output - **msg.payload:** Output received messages as a Buffer, string, or base64 encoded string. - **msg.fromip:** The IP address and port from which the message was received, formatted as "IP\:Port". - **msg.ip and msg.port:** The IP address and port from which the message was received. ::node-red-help --- category: network file: 32-udp name: UDP In node: udp in --- :: # UDP Out ## What is udp-out node in Node-RED? The udp-out node in Node-RED is a node used for sending UDP messages to a specified network destination. When you add a udp-out node to your Node-RED flow, you configure it with the IP address and port of the destination device or service. Then, any messages received by the udp-out node are sent as UDP packets to that destination. This node is particularly useful for applications where real-time communication or lightweight message transmission is required. For more information on UDP refer to [What is UDP](https://flowfuse.com/docs/node-red/core-nodes/network/udp-in/#what-is-udp) ## Configuring UDP-Out Node - **Send to:** - Choose the destination type: - Single IP: Specify the IP address of the target device or service. you can also specify the ip dynamically with msg.ip when this field left blank - Broadcast: Send the message to all devices on the network. you can either specify the address as the local broadcast IP address or use 255.255.255.255, which represents the global broadcast address. - Multicast: Send the message to a group of devices. - Group: Specify the multicast group address. - Local Interface: Select the network interface for sending multicast messages. - **Port:** - Specify the port number that the UDP packets will be sent to. you can also specify the ip dynamically with msg.port when this field left blank - **Address:** - Choose the type of address: - IPv4 Address: Specify an IPv4 address for the destination. - IPv6 Address: Specify an IPv6 address for the destination. - bind to random local port: When selected, the system automatically assigns an available local port for sending UDP packets. - bind to local port: When selected, you need to specify the local port number to which the UDP socket will be bound. - **Decode Base64 encoded payload?:** - Choose whether to decode the payload as Base64 before sending it. This option is useful if the payload is encoded in Base64 format and needs to be decoded before being sent as a UDP message. *Note: On some systems, you may need root or administrator access to use ports below 1024 and/or broadcast, and have to ensure your firewall allows the data in.* **Note: The default UDP nodes have been removed from the Node-RED palette in the FlowFuse Cloud due to limitations in routing connections to the container running Node-RED inside the FlowFuse platform** ## Usecases - **Sensor data transmission:** Utilize the udp-out node to transmit real-time data acquired from IoT sensors deployed in the field, such as temperature, humidity, or motion sensor readings, to a centralized processing system or server for analysis and storage. - **Environmental monitoring:** Utilize the udp-out node to transmit environmental data collected from IoT devices, such as air quality sensors or weather stations, to a central server for analysis and decision-making. - **Asset tracking:** Utilize the udp-out node to send location data from IoT devices equipped with GPS or RFID technology to a tracking system for real-time monitoring of assets, vehicles, or livestock. - **Media stream transmission:** Employ the udp-out node to transmit media streams, such as video or audio content, for applications like live broadcasting, surveillance, or multimedia communication. ## Example 1. In the example below, we have a udp-out node configured to send data over localhost on port 90 and receive it with a udp-in node. ::render-flow ```json [{"id":"8b65196d8e0682a7","type":"group","z":"5b972161c4e0464e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["c69190416293c2c5","1448bee95281f5bb","4d31d05731f0fa6c","cd0155779b527eb2","9b0fd28ce351cbee","53660b468f150faa"],"x":374,"y":59,"w":472,"h":262},{"id":"c69190416293c2c5","type":"inject","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":490,"y":160,"wires":[["1448bee95281f5bb"]]},{"id":"1448bee95281f5bb","type":"udp out","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","addr":"127.0.0.1","iface":"","port":"90","ipv":"udp4","outport":"","base64":false,"multicast":"false","x":730,"y":160,"wires":[]},{"id":"4d31d05731f0fa6c","type":"udp in","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"","iface":"","port":"90","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":470,"y":280,"wires":[["cd0155779b527eb2"]]},{"id":"cd0155779b527eb2","type":"debug","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":720,"y":280,"wires":[]},{"id":"9b0fd28ce351cbee","type":"comment","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"Sending data to client","info":"","x":600,"y":100,"wires":[]},{"id":"53660b468f150faa","type":"comment","z":"5b972161c4e0464e","g":"8b65196d8e0682a7","name":"Receving data from server ","info":"","x":610,"y":220,"wires":[]}] ``` :: ::node-red-help --- category: network file: 32-udp name: UDP Out node: udp out --- :: # WebSocket ## What are WebSocket nodes used for in Node-RED Node-RED provides two WebSocket nodes that serve distinct purposes and can operate in two modes. ### Listen On In this mode, Node-RED functions as a WebSocket server, enabling remote clients to establish connections. - The `WebSocket-in` node is responsible for receiving messages sent from remote clients. - The `WebSocket-out` node facilitates the flow to send messages either to a specific connected client or to broadcast messages to all connected clients. ### Connect To In this mode, Node-RED acts as a client, establishing connections with remote WebSocket servers. - The `WebSocket-in` node receives messages sent from the remote WebSocket server to Node-RED. - The `WebSocket-out` node allows the flow to send messages to the remote server. ## WebSocket node configuration ### Path When you use the WebSocket node in "Listen on" mode, you'll have to specify the path or endpoint to which remote clients will establish a connection. ### Send/Receive - Payload: This option sends or receives only the `msg.payload` as data over the WebSocket connection. It excludes any additional `msg` properties. - Entire Message: When enabled, this option allows the entire message object, including payload, and other properties to be sent or received as a JSON formatted string. ### URL When you use the WebSocket node in "Connect to" mode, you'll have to specify the connection URL that should use `ws://` or `wss://` scheme and point to an existing WebSocket listener. ### Subprotocol This option allows you to specify a particular WebSocket subprotocol to use during the connection handshake. For example, if a WebSocket server requires the use of the "mqtt" subprotocol, you would configure the WebSocket node's "Subprotocol" option to "mqtt" to ensure that the WebSocket handshake includes the MQTT protocol, enabling proper communication between Node-RED and the WebSocket server. ### Send heartbeat Enabling this option allows specifying the time interval in seconds for sending periodic ping messages from the client to the server to maintain the connection. The server responds with a pong message to confirm the connection status. This helps prevent the connection from being closed due to inactivity or network issues. ## Examples Simple Echo test This shows both modes, with one set acting as a WebSocket Echo Server and the other connecting to that server and sending and receiving messages. ::render-flow ```json [{"id":"43294be738b7699a","type":"tab","label":"WebSockets","disabled":false,"info":"","env":[]},{"id":"e788dbc44428f6b7","type":"websocket in","z":"43294be738b7699a","name":"","server":"","client":"4db78eb653d6d50d","x":290,"y":240,"wires":[["d86baf93795dbb2d"]]},{"id":"8c702971b0292f9b","type":"websocket out","z":"43294be738b7699a","name":"","server":"","client":"4db78eb653d6d50d","x":510,"y":180,"wires":[]},{"id":"dda81a976ccf2a2b","type":"inject","z":"43294be738b7699a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":220,"y":180,"wires":[["8c702971b0292f9b"]]},{"id":"d86baf93795dbb2d","type":"debug","z":"43294be738b7699a","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":580,"y":240,"wires":[]},{"id":"d36a13e17814e375","type":"websocket in","z":"43294be738b7699a","name":"","server":"542f751e15337c46","client":"","x":230,"y":320,"wires":[["1660b4da46db22ad"]]},{"id":"1660b4da46db22ad","type":"websocket out","z":"43294be738b7699a","name":"","server":"542f751e15337c46","client":"","x":560,"y":320,"wires":[]},{"id":"4db78eb653d6d50d","type":"websocket-client","path":"ws://localhost:1880/ws/echo","tls":"","wholemsg":"false","hb":"0","subprotocol":""},{"id":"542f751e15337c46","type":"websocket-listener","path":"/ws/echo","wholemsg":"false"}] ``` :: ::node-red-help --- category: network file: 22-websocket name: WebSocket node: websocket --- :: # CSV Converts between CSV formatted text and JavaScript objects. ## Where and why do we use the CSV node? The CSV node processes comma-separated values (CSV) data. It converts CSV strings into JavaScript objects for processing, or transforms objects back into CSV format for export. This is essential when working with spreadsheet data, database exports, or any tabular information that needs to be read, modified, or generated. ## Modes of operation The CSV node operates in two directions depending on the input it receives: ### CSV to Object When the input is a CSV string, the node parses it into JavaScript objects. Each row becomes an object with properties named after the column headers. This mode lets you process spreadsheet data programmatically, apply calculations, filter rows, or transform the data structure. The node can handle CSV strings with or without headers. When headers are present, they become the property names in the output objects. Without headers, you can specify column names manually. ### Object to CSV When the input is a JavaScript object or array of objects, the node converts it into CSV format. This mode is useful for generating reports, exporting processed data, or creating files for import into spreadsheet applications. You can control whether to include headers in the output and specify which columns to export. ## How the node handles messages The CSV node processes the `msg.payload` property. For CSV input, it outputs one message per row (or a single message with an array of all rows, depending on configuration). For object input, it generates a CSV string in the output `msg.payload`. The node supports various CSV formats and can handle quoted fields, different delimiters, and special characters. It preserves data types when configured to do so, converting strings to numbers or booleans as appropriate. When parsing CSV with headers, the node stores the column names in `msg.columns`. This property can be modified to control which columns appear in the output when converting back to CSV. ## Examples ### Processing energy consumption data Suppose you have a CSV file that details the energy consumption of various manufacturing stations in a factory. The file includes the station name, the month, and the total electricity consumed. You want to add a new column that displays the number of parts produced per station, based on the assumption that each part consumes a specific amount of electricity. CSV Input: ```text Station,Month,Consumption Rio,January,4000 New York,January,1000 Tokio,January,3000 ``` This flow reads the CSV data, converts it to objects, adds a calculated PartsProduced column, and converts it back to CSV format. ::render-flow ```json [{"id":"1","type":"inject","z":"3a74ad88ecc45bcb","name":"Start","repeat":"","crontab":"","once":false,"onceDelay":0.1,"payload":"","payloadType":"date","x":90,"y":60,"wires":[["2"]]},{"id":"2","type":"template","z":"3a74ad88ecc45bcb","name":"CSV Data","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Station,Month,Consumption\nRio,January,4000\nNew York,January,1000\nTokio,January,3000","x":240,"y":60,"wires":[["3"]]},{"id":"3","type":"csv","z":"3a74ad88ecc45bcb","name":"CSV In","sep":",","hdrin":true,"hdrout":"","multi":"one","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":false,"include_null_values":false,"x":410,"y":60,"wires":[["4","5"]]},{"id":"4","type":"debug","z":"3a74ad88ecc45bcb","name":"Debug JSON","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":590,"y":60,"wires":[]},{"id":"5","type":"change","z":"3a74ad88ecc45bcb","name":"Add PartsProduced","rules":[{"t":"set","p":"columns","pt":"msg","to":"msg.columns & \", PartsProduced\"","tot":"jsonata"},{"t":"set","p":"payload.PartsProduced","pt":"msg","to":"payload.Consumption / 10","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":230,"y":140,"wires":[["6"]]},{"id":"6","type":"csv","z":"3a74ad88ecc45bcb","name":"CSV out","sep":",","hdrin":"","hdrout":"all","multi":"one","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":false,"include_null_values":false,"x":420,"y":140,"wires":[["7"]]},{"id":"7","type":"debug","z":"3a74ad88ecc45bcb","name":"Debug Final CSV","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":610,"y":140,"wires":[]}] ``` :: ::node-red-help --- category: parsers file: 70-CSV name: CSV node: csv --- :: # HTML Extracts elements from an HTML document. ## Where and why do we use the HTML node? The HTML node parses HTML documents and extracts specific elements using CSS selectors. This is essential when you need to scrape data from web pages, extract specific content from HTML responses, or process HTML documents to retrieve structured information. Unlike the [template node](https://flowfuse.com/docs/node-red/core-nodes/function/template/) which generates HTML, this node is purely for parsing and extraction. ## How it works The HTML node uses CSS selectors to find and extract elements from HTML content in `msg.payload`. You specify which elements to extract using standard CSS selector syntax (like `h1`, `.classname`, `#id`, or more complex selectors). The node supports a combination of CSS and jQuery selectors - see the [css-select documentation](https://github.com/fb55/css-select){rel=""nofollow""} for the full syntax. The selector can be configured in the node's edit panel or provided dynamically via `msg.select`. ## Modes of operation The HTML node can output extracted content in different ways: ### Single Message with Array Returns one message where `msg.payload` contains an array of all matched elements. Use this when you want to process all results together or need to know the total count of matches. ### Multiple Messages Sends separate messages for each matched element. Each message contains one matched element in `msg.payload` and includes a `msg.parts` property for sequence tracking. Use this when you want to process each match individually through subsequent nodes. ### Return Format For each matched element, you can choose to return: - **HTML markup** - the complete HTML including tags and attributes - **Text content** - just the text with all HTML tags stripped ## How the node handles messages The HTML node processes the HTML string in `msg.payload`. After parsing and extracting the specified elements, it outputs the results according to the configured mode. When outputting multiple messages, the node automatically adds the `msg.parts` property to enable proper handling by downstream nodes like Join. This property includes the sequence identifier, message index, and total count. The node uses CSS selector syntax with jQuery extensions, so you can use: - Tag selectors: `h1`, `div`, `span` - Class selectors: `.classname` - ID selectors: `#elementid` - Attribute selectors: `[href]`, `[data-value="123"]` - Complex selectors: `div.content > p`, `ul li:first-child` - jQuery extensions: `:first`, `:last`, `:even`, `:odd` ## Examples ### Extracting page titles This example fetches the Node-RED homepage and extracts the text from the `h1` tag. The HTTP Request node retrieves the page, and the HTML node parses it to find the heading. ::render-flow ```json [{"id":"fe3ffa918ba45f27","type":"html","z":"99a0b45110d553ec","name":"Select H1 element","property":"payload","outproperty":"payload","tag":"h1","ret":"html","as":"single","x":610,"y":40,"wires":[["07dd1efff04d231a"]]},{"id":"339359b6a6793b3d","type":"http request","z":"99a0b45110d553ec","name":"Get Node-RED.org homepage","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://nodered.org/","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":true,"headers":[],"x":350,"y":40,"wires":[["fe3ffa918ba45f27"]]},{"id":"e7dcdcff49c14ab1","type":"inject","z":"99a0b45110d553ec","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":40,"wires":[["339359b6a6793b3d"]]},{"id":"07dd1efff04d231a","type":"debug","z":"99a0b45110d553ec","name":"Print H1 content","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":820,"y":40,"wires":[]}] ``` :: ::node-red-help --- category: parsers file: 70-HTML name: HTML node: html --- :: # Parsers The **Parsers** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [CSV](https://flowfuse.com/docs/node-red/core-nodes/parsers/csv/) - [HTML](https://flowfuse.com/docs/node-red/core-nodes/parsers/html/) - [JSON](https://flowfuse.com/docs/node-red/core-nodes/parsers/json/) - [XML](https://flowfuse.com/docs/node-red/core-nodes/parsers/xml/) - [YAML](https://flowfuse.com/docs/node-red/core-nodes/parsers/yaml/) # JSON Converts between JSON strings and JavaScript objects. ## Where and why do we use the JSON node? The JSON node processes JavaScript Object Notation (JSON) data. It converts between JSON-formatted strings and JavaScript objects, making it essential when working with APIs, storing data, or transmitting information between different services. This bidirectional conversion lets you parse incoming JSON data for processing and format JavaScript objects into JSON strings for output. ## Modes of operation The JSON node operates in two directions depending on what it detects in the input: ### JSON String to Object When the input is a JSON string, the node parses it into a JavaScript object. This mode is essential when receiving data from APIs, reading JSON files, or processing JSON payloads from HTTP requests. Once converted to an object, you can access and manipulate the data using standard JavaScript operations. ### Object to JSON String When the input is a JavaScript object, the node converts it into a JSON string. Use this mode when preparing data to send to APIs, writing to files, or transmitting structured data. You can optionally format the output with indentation for improved readability. ### Automatic Detection The node automatically detects whether the input is a JSON string or JavaScript object and performs the appropriate conversion. You can also configure it to always convert in a specific direction or validate JSON without conversion. ## How the node handles messages The JSON node processes the `msg.payload` property by default, but you can configure it to work with any message property. After conversion, it replaces the property with the converted value. When parsing JSON strings, the node validates the syntax and reports errors if the JSON is malformed. When converting objects to strings, it handles nested structures, arrays, and standard JavaScript data types (strings, numbers, booleans, null). The node can format JSON output with pretty printing, adding indentation and line breaks to make the structure more readable. This is useful for debugging or generating human-readable output files. ## Examples ### Monitoring equipment efficiency Suppose you have a JSON data stream from sensors installed on an assembly line in a manufacturing plant. The JSON objects include equipment name, timestamp, and efficiency percentage. This flow extracts the information and calculates a daily average efficiency for each equipment to help with predictive maintenance and production optimization. **JSON Input:** ```json { "equipment": "Drill Press", "timestamp": "2023-09-22T12:34:56Z", "efficiency": 89.5 } ``` The flow parses incoming JSON strings, groups messages together, calculates the average efficiency, and converts the result back to JSON format. ::render-flow ```json [{"id":"609e5eb634beaf5c","type":"tab","label":"Flow 4","disabled":false,"info":"","env":[]},{"id":"a0ce2ea0.b7597","type":"inject","z":"609e5eb634beaf5c","name":"Simulate Data","props":[{"p":"payload"}],"repeat":"0.5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"equipment\":\"Drill Press\",\"timestamp\":\"2023-09-22T12:34:56Z\",\"efficiency\":89.5}","payloadType":"json","x":140,"y":80,"wires":[["8d32bd8d.6d5cc"]]},{"id":"8d32bd8d.6d5cc","type":"json","z":"609e5eb634beaf5c","name":"Parse JSON","property":"payload","action":"obj","pretty":false,"x":330,"y":80,"wires":[["673dc89e.64ac18"]]},{"id":"673dc89e.64ac18","type":"join","z":"609e5eb634beaf5c","name":"Group Messages","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"10","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":530,"y":80,"wires":[["1780e12a.aa407f"]]},{"id":"1780e12a.aa407f","type":"function","z":"609e5eb634beaf5c","name":"Calculate Efficiency","func":"let arr = msg.payload;\nlet sum = 0;\nlet count = 0;\n\narr.forEach(function(item) {\n sum += item.efficiency;\n count++;\n});\n\nlet averageEfficiency = sum / count;\n\nmsg.payload = {\n equipment: arr[0].equipment,\n averageEfficiency: averageEfficiency\n};\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":80,"wires":[["6285ddd29f8b38c7"]]},{"id":"6a79ba9.44db444","type":"debug","z":"609e5eb634beaf5c","name":"Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1110,"y":80,"wires":[]},{"id":"6285ddd29f8b38c7","type":"json","z":"609e5eb634beaf5c","name":"Parse Object","property":"payload","action":"str","pretty":false,"x":950,"y":80,"wires":[["6a79ba9.44db444"]]}] ``` :: ::node-red-help --- category: parsers file: 70-JSON name: JSON node: json --- :: # XML Converts between XML strings and JavaScript objects. ## Where and why do we use the XML node? The XML node processes Extensible Markup Language (XML) data. It converts between XML-formatted strings and JavaScript objects, making it essential when working with legacy systems, SOAP APIs, configuration files, or any service that uses XML for data exchange. This bidirectional conversion lets you parse incoming XML data for processing and format JavaScript objects into XML strings for output. ## Modes of operation The XML node operates in two directions depending on what it detects in the input: ### XML String to Object When the input is an XML string, the node parses it into a JavaScript object. This mode is essential when receiving data from SOAP APIs, reading XML configuration files, or processing XML payloads from devices and sensors. Once converted to an object, you can access and manipulate the data using standard JavaScript operations. XML elements become object properties, nested elements create nested objects, and attributes are preserved in the conversion. ### Object to XML String When the input is a JavaScript object, the node converts it into an XML string. Use this mode when preparing data to send to SOAP services, writing XML configuration files, or transmitting structured data to systems that require XML format. The node generates valid, well-formed XML from your JavaScript object structure. ### Property naming conventions When converting between XML and an object, any XML attributes are added as a property named `$` by default. Any text content is added as a property named `_`. **Understanding the `$` property (attributes):** The `$` property stores all XML attributes as key-value pairs. ```xml ``` Becomes: ```javascript { product: { $: { id: "P123", category: "electronics", inStock: "true" } } } ``` **Understanding the `_` property (text content):** The `_` property stores the text content of an XML element. ```xml Hello World 29.99 ``` Becomes: ```javascript { message: { _: "Hello World" }, price: { _: "29.99" } } ``` **Complex example combining both:** ```xml jsmith john@example.com Active ``` Becomes: ```javascript { user: { $: { id: "456", role: "admin" }, username: { _: "jsmith" }, email: { $: { verified: "true" }, _: "john@example.com" }, status: { _: "Active" } } } ``` ## Demo Flow ### XML String to Object ::render-flow ```json [{"id":"227621b279d19ace","type":"xml","z":"b446dfa04d79d359","name":"XML to Object","property":"payload","attr":"","chr":"","x":380,"y":180,"wires":[["953fc1f64f4b892e"]]},{"id":"404972c7c517dedd","type":"inject","z":"b446dfa04d79d359","name":"Inject XML","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":" Laptop 999.99 ","payloadType":"str","x":220,"y":180,"wires":[["227621b279d19ace"]]},{"id":"953fc1f64f4b892e","type":"debug","z":"b446dfa04d79d359","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":530,"y":180,"wires":[]}] ``` :: ### Object to XML String ::render-flow ```json [{"id":"227621b279d19ace","type":"xml","z":"b446dfa04d79d359","name":"Object to XML","property":"payload","attr":"","chr":"","x":440,"y":180,"wires":[["953fc1f64f4b892e"]]},{"id":"404972c7c517dedd","type":"inject","z":"b446dfa04d79d359","name":"Inject Object","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{ \"id\": 10, \"name\": \"Demo Item\", \"category\": \"Sample\", \"price\": 499, \"inStock\": true, \"rating\": 4.5, \"manufacturer\": \"DemoCorp\", \"createdAt\": \"2025-01-01T10:00:00Z\", \"description\": \"A medium-sized JSON object for XML conversion testing.\" }","payloadType":"json","x":270,"y":180,"wires":[["227621b279d19ace"]]},{"id":"953fc1f64f4b892e","type":"debug","z":"b446dfa04d79d359","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":180,"wires":[]}] ``` :: ::node-red-help --- category: parsers file: 70-XML name: XML node: xml --- :: # YAML Converts between YAML format and JavaScript objects. ## Where and why do we use the YAML node? The YAML node processes YAML (Yet Another Markup Language) data, which is a human-readable data serialization format. It converts between YAML strings and JavaScript objects, making it essential when working with configuration files, Kubernetes manifests, CI/CD pipelines, or any system that uses YAML for data representation. The format's readability makes it popular for configuration management and data exchange. ## Modes of operation The YAML node operates bidirectionally, automatically detecting the input format: ### YAML to Object When the input is a YAML string, the node parses it into a JavaScript object. This mode is essential when reading YAML configuration files, processing YAML data from APIs, or converting YAML documents into a structure you can manipulate programmatically. ### Object to YAML When the input is a JavaScript object, the node converts it into YAML format. Use this mode when generating configuration files, creating YAML documents for deployment systems, or formatting data in a human-readable way for storage or transmission. ## How the node handles messages The YAML node processes a configurable message property (default is `msg.payload`). After successful conversion, it replaces that property with the converted value. If parsing fails due to invalid syntax, the node throws an error that can be caught using a Catch node. When no data is passed in the configured property, the node passes the full message unchanged to the next node. This allows it to be used in flows where the property might not always be present. The node validates the structure during parsing and will report errors for malformed YAML, such as incorrect indentation, missing colons, or unclosed quotes. ## Examples ### Parsing JSON to YAML The YAML node automatically detects the input format. When it receives JSON, it converts the data to YAML format. In this example, a JSON object `{"foo":"bar"}` is converted to YAML. ::render-flow ```json [{"id":"4481ea08a9fe27e1","type":"inject","z":"7d38803e3d40ee7e","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"foo\":\"bar\"}","payloadType":"json","x":190,"y":220,"wires":[["b31536833d27aee0"]]},{"id":"b31536833d27aee0","type":"yaml","z":"7d38803e3d40ee7e","property":"payload","name":"Parse JSON to YAML","x":400,"y":220,"wires":[["90fed168a9d1a4b5"]]},{"id":"90fed168a9d1a4b5","type":"debug","z":"7d38803e3d40ee7e","name":"Debug: Output YAML","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":220,"wires":[]}] ``` :: ### Parsing YAML to JSON When the input is YAML format, the node automatically converts it to a JavaScript object (JSON). This makes it easy to work with YAML configuration files in Node-RED flows. ::render-flow ```json [{"id":"a0dc30d8f5225962","type":"inject","z":"7d38803e3d40ee7e","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"foo: bar","payloadType":"str","x":180,"y":300,"wires":[["0fc216b8cebccc25"]]},{"id":"0fc216b8cebccc25","type":"yaml","z":"7d38803e3d40ee7e","property":"payload","name":"Parse YAML to JSON","x":400,"y":300,"wires":[["c9a3f66e67b41ad4"]]},{"id":"c9a3f66e67b41ad4","type":"debug","z":"7d38803e3d40ee7e","name":"Debug: Output JSON","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":300,"wires":[]}] ``` :: ### Error handling for invalid input When the input is malformed, the YAML node throws an error. This error can be caught using a Catch node, allowing you to handle parsing failures gracefully. In this example, the YAML string is missing a closing quote, which triggers an error. ::render-flow ```json [{"id":"0fc216b8cebccc25","type":"yaml","z":"7d38803e3d40ee7e","property":"payload","name":"Input invalid","x":370,"y":420,"wires":[[]]},{"id":"a0dc30d8f5225962","type":"inject","z":"7d38803e3d40ee7e","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"foo: \"bar","payloadType":"str","x":180,"y":420,"wires":[["0fc216b8cebccc25"]]},{"id":"c9a3f66e67b41ad4","type":"debug","z":"7d38803e3d40ee7e","name":"Caught error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":500,"wires":[]},{"id":"6e3ba1ebc7beaf81","type":"catch","z":"7d38803e3d40ee7e","name":"","scope":["0fc216b8cebccc25"],"uncaught":false,"x":190,"y":500,"wires":[["c9a3f66e67b41ad4"]]}] ``` :: ::node-red-help --- category: parsers file: 70-YAML name: YAML node: yaml --- :: # Batch Creates sequences of messages based on various rules. ## Where and why do we use the Batch node? The Batch node groups sequences of messages into batches. It's useful when you need to collect multiple messages before processing them together, create time-windowed data collections, or reorganize message flows by topic. ## Modes of operation The Batch node operates in three different modes, each suited for different use cases. ### Group by Number of Messages Groups messages into sequences of a given length. Set the batch size to 5 and the first 5 messages form one batch, the next 5 form another batch, and so on. ![Batching messages into 5 groups](https://flowfuse.com/docs/node-red/core-nodes/images/batch-example1.png) The overlap option lets you repeat messages between batches. When enabled, messages at the end of one sequence appear at the start of the next. With a batch size of 5 and overlap of 1, you get sequences like 1-5, then 5-9, then 9-13. This creates a sliding window effect that's useful for analysis requiring context from previous data. ![Batching messages into 5 groups with overlap](https://flowfuse.com/docs/node-red/core-nodes/images/batch-example2.png) ### Group by Time Interval Groups all messages that arrive within a specified time period. Set it to 2 seconds and every message received in that window gets batched together. When the interval expires, the batch releases and a new window starts. You can optionally configure the node to send an empty message if nothing arrives within the interval. :video{ariaLabel="Batching messages into 2 second groups" autoPlay="true" height="502" loop="true" muted="true" playsInline="true" preload="none" width="845"} ### Concatenate Sequences Creates a new message sequence by combining incoming sequences in a specified order. Each incoming message must have both a `msg.topic` property and a `msg.parts` property that identifies its sequence. You configure the node with a list of topic values to control the order sequences get concatenated. This mode lets you duplicate sequences for parallel processing or reorder them by topic. For example, you could filter an array of numbers into positive and negative values, assign each group a different topic, then concatenate them in whichever order you need. ![Duplicating a sequence of data](https://flowfuse.com/docs/node-red/core-nodes/images/batch-example4.png) ![Batch filter and concatenate](https://flowfuse.com/docs/node-red/core-nodes/images/batch-example5.png) ## How the node handles messages The Batch node buffers messages internally to work across sequences. The Node-RED runtime setting `nodeMessageBufferMaxLength` limits how many messages can be buffered to prevent memory issues. If you send a message with the `msg.reset` property set to true, the node immediately deletes all buffered messages without sending them. This is useful when you need to start fresh or handle error conditions. ### Demo flows ::render-flow{:height='700'} ```json [{"id":"52c7d8c93d68afb5","type":"tab","label":"Batch Node","disabled":false,"info":"","env":[]},{"id":"7aa502dddb9274d2","type":"group","z":"52c7d8c93d68afb5","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["e3a5c066.16333","9223c119.c5268","e4d07fa1.78c16","848f59e5.7528d8","cf1bbb5e.ba3e68","a1e311d5.4dca1","2776c823.77eba8","3c47b863c67393a2","93c3e8c551c02f95","572ede73fc15f038","5043a65dd26e130d"],"x":34,"y":439,"w":592,"h":382},{"id":"2701ed93a0fbc58d","type":"group","z":"52c7d8c93d68afb5","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["cf1fd796.197678","d1c8ddf0.99b4e","f727a5d3.ea1a28","f4d6dba4.7e8ab8","31b81865.611788","356c8654.2ad1aa"],"x":34,"y":899,"w":492,"h":242},{"id":"0b151c343c56c94c","type":"group","z":"52c7d8c93d68afb5","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["74853568.22b87c","8f4f683.99d1998","6c47ccb3.bb0184","49c2ac1.59a9354","311dd6b4.5aeb7a","e27c55b0.18e9c8","9e65f29a.69ca2","817acbfb.452af8"],"x":34,"y":59,"w":552,"h":302},{"id":"b857a13782d7c5d9","type":"group","z":"52c7d8c93d68afb5","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["53645699.a35c48","4cb873e6.f9996c","51089be3.4ecbf4","84c34533.6284a8","c7241026.18245","67d24449.028eec","5d909bfb.6faf44","7b289e4ad723a92a"],"x":614,"y":59,"w":592,"h":302},{"id":"3eda846bc0d54e59","type":"group","z":"52c7d8c93d68afb5","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["7f1ce95c.7ddbc8","3412e439.eda55c","e6f01877.16d558","c11e5c5f.876d6","e99c703b.f404","dbd6e8b8.cbf2b8","408f3032.eafc1","5137b2d0.f4838c","c571b56c.ae63b8","c548f2c.641141","4aa7d5ab1091553e","65261ee2e95176c2"],"x":614,"y":899,"w":632,"h":402},{"id":"5043a65dd26e130d","type":"junction","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","x":100,"y":580,"wires":[["93c3e8c551c02f95","3c47b863c67393a2"]]},{"id":"65261ee2e95176c2","type":"junction","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","x":980,"y":1040,"wires":[["3412e439.eda55c"]]},{"id":"e3a5c066.16333","type":"batch","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"","mode":"interval","count":10,"overlap":0,"interval":"2","allowEmptySequence":false,"topics":[],"x":280,"y":660,"wires":[["9223c119.c5268"]]},{"id":"9223c119.c5268","type":"join","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":150,"y":740,"wires":[["e4d07fa1.78c16"]]},{"id":"e4d07fa1.78c16","type":"debug","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":510,"y":740,"wires":[]},{"id":"848f59e5.7528d8","type":"comment","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"↑ create message sequence received within 2s","info":"","x":270,"y":780,"wires":[]},{"id":"cf1bbb5e.ba3e68","type":"comment","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"← join sequence to array","info":"","x":490,"y":660,"wires":[]},{"id":"a1e311d5.4dca1","type":"inject","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":480,"wires":[["5043a65dd26e130d"]]},{"id":"2776c823.77eba8","type":"delay","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"Rate limit 1msg/0.5s","pauseType":"rate","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"0.5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":460,"y":580,"wires":[["e3a5c066.16333"]]},{"id":"5842d64f.9fc608","type":"comment","z":"52c7d8c93d68afb5","name":"Example: Time-based Group Mode - Group messages received within 2s","info":"*Time-based Group mode* of batch node can be used to create new message sequences from incoming messages received within specified time range. \n","x":300,"y":420,"wires":[]},{"id":"cf1fd796.197678","type":"inject","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"Array of 3 characters [\"a\", \"b\", \"c\"]","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"SEQ","payload":"[\"a\", \"b\", \"c\"]","payloadType":"json","x":210,"y":940,"wires":[["d1c8ddf0.99b4e"]]},{"id":"d1c8ddf0.99b4e","type":"split","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":430,"y":940,"wires":[["f727a5d3.ea1a28"]]},{"id":"f727a5d3.ea1a28","type":"batch","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"","mode":"concat","count":10,"overlap":0,"interval":10,"allowEmptySequence":false,"topics":[{"topic":"SEQ"},{"topic":"SEQ"}],"x":190,"y":1020,"wires":[["f4d6dba4.7e8ab8"]]},{"id":"f4d6dba4.7e8ab8","type":"join","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":190,"y":1100,"wires":[["31b81865.611788"]]},{"id":"31b81865.611788","type":"debug","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":410,"y":1100,"wires":[]},{"id":"356c8654.2ad1aa","type":"comment","z":"52c7d8c93d68afb5","g":"2701ed93a0fbc58d","name":"← Duplicate SEQ","info":"","x":400,"y":1020,"wires":[]},{"id":"c851c021.a9688","type":"comment","z":"52c7d8c93d68afb5","name":"Example: Concatenate Mode - Duplicate a sequence of data","info":"*Concatenate mode* of batch node can be used to combine input message sequences to create a new message sequence. Order of the sequences can be specified using message topic assigned to each message in a sequence. Message sequence can be specified multiple times.\n","x":260,"y":880,"wires":[]},{"id":"3c47b863c67393a2","type":"change","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"Reset","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"},{"t":"delete","p":"payload","pt":"msg"},{"t":"delete","p":"topic","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":130,"y":660,"wires":[["e3a5c066.16333"]]},{"id":"3d208473.f31e1c","type":"comment","z":"52c7d8c93d68afb5","name":"Example: Number-based Group Mode - Group 5 consecutive messages","info":"","x":290,"y":40,"wires":[]},{"id":"74853568.22b87c","type":"batch","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"","mode":"count","count":"5","overlap":0,"interval":"5","allowEmptySequence":false,"topics":[],"x":180,"y":220,"wires":[["8f4f683.99d1998"]]},{"id":"8f4f683.99d1998","type":"join","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":170,"y":280,"wires":[["6c47ccb3.bb0184"]]},{"id":"6c47ccb3.bb0184","type":"debug","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":350,"y":280,"wires":[]},{"id":"49c2ac1.59a9354","type":"comment","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"↑ create message sequence with 5 messages","info":"","x":290,"y":320,"wires":[]},{"id":"311dd6b4.5aeb7a","type":"comment","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"← join sequence to array","info":"","x":390,"y":220,"wires":[]},{"id":"e27c55b0.18e9c8","type":"inject","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":100,"wires":[["9e65f29a.69ca2"]]},{"id":"9e65f29a.69ca2","type":"function","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"send: 1..20","func":"for(var x = 1; x <= 20; x++) {\n node.send({payload: x});\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":160,"wires":[["74853568.22b87c"]]},{"id":"817acbfb.452af8","type":"comment","z":"52c7d8c93d68afb5","g":"0b151c343c56c94c","name":"← send 20 msgs with numbers 1..20","info":"","x":420,"y":160,"wires":[]},{"id":"ecff527d.d64cb","type":"comment","z":"52c7d8c93d68afb5","name":"Example: Number-based Group Mode - 5 consecutive messages, overlap 1 msg","info":"","x":900,"y":40,"wires":[]},{"id":"53645699.a35c48","type":"batch","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"","mode":"count","count":"5","overlap":"1","interval":"5","allowEmptySequence":false,"topics":[],"x":760,"y":220,"wires":[["4cb873e6.f9996c"]]},{"id":"4cb873e6.f9996c","type":"join","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":750,"y":280,"wires":[["51089be3.4ecbf4"]]},{"id":"51089be3.4ecbf4","type":"debug","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":930,"y":280,"wires":[]},{"id":"84c34533.6284a8","type":"comment","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"↑ create message sequence with 5 messages with overlap of 1 msg","info":"","x":940,"y":320,"wires":[]},{"id":"c7241026.18245","type":"comment","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"← join sequence to array","info":"","x":970,"y":220,"wires":[]},{"id":"67d24449.028eec","type":"inject","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":720,"y":100,"wires":[["5d909bfb.6faf44"]]},{"id":"5d909bfb.6faf44","type":"function","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"send: 1..20","func":"for (var x = 1; x <= 20; x++) {\n node.send({ payload: x });\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":160,"wires":[["53645699.a35c48"]]},{"id":"7b289e4ad723a92a","type":"comment","z":"52c7d8c93d68afb5","g":"b857a13782d7c5d9","name":"← send 20 msgs with numbers 1..20","info":"","x":1000,"y":160,"wires":[]},{"id":"93c3e8c551c02f95","type":"function","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"send: 1..20","func":"for (var x = 1; x <= 20; x++) {\n node.send({ payload: x });\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":580,"wires":[["2776c823.77eba8"]]},{"id":"572ede73fc15f038","type":"comment","z":"52c7d8c93d68afb5","g":"7aa502dddb9274d2","name":"↓ send 20 msgs with numbers 1..20","info":"","x":320,"y":540,"wires":[]},{"id":"7f1ce95c.7ddbc8","type":"join","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":770,"y":1220,"wires":[["e6f01877.16d558"]]},{"id":"3412e439.eda55c","type":"batch","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"","mode":"concat","count":10,"overlap":0,"interval":10,"allowEmptySequence":false,"topics":[{"topic":"NEG"},{"topic":"POS"}],"x":770,"y":1140,"wires":[["7f1ce95c.7ddbc8"]]},{"id":"e6f01877.16d558","type":"debug","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1050,"y":1220,"wires":[]},{"id":"c11e5c5f.876d6","type":"change","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"POS","rules":[{"t":"set","p":"topic","pt":"msg","to":"POS","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":887,"y":1020,"wires":[["65261ee2e95176c2"]]},{"id":"e99c703b.f404","type":"change","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"NEG","rules":[{"t":"set","p":"topic","pt":"msg","to":"NEG","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":887,"y":1060,"wires":[["65261ee2e95176c2"]]},{"id":"dbd6e8b8.cbf2b8","type":"switch","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":">= 0? \\n < 0?","property":"payload","propertyType":"msg","rules":[{"t":"gt","v":"0","vt":"num"},{"t":"else"}],"checkall":"true","repair":true,"outputs":2,"x":750,"y":1040,"wires":[["c11e5c5f.876d6"],["e99c703b.f404"]]},{"id":"408f3032.eafc1","type":"split","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":930,"y":940,"wires":[["dbd6e8b8.cbf2b8"]]},{"id":"5137b2d0.f4838c","type":"inject","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"SEQ","payload":"[1,-6,-8,7,2,-3]","payloadType":"json","x":750,"y":940,"wires":[["408f3032.eafc1"]]},{"id":"c571b56c.ae63b8","type":"comment","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"↑ Join the sequence of messages","info":"","x":850,"y":1260,"wires":[]},{"id":"c548f2c.641141","type":"comment","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"← Order sequence of messages: NEG, POS","info":"","x":1050,"y":1140,"wires":[]},{"id":"16341de8ac839049","type":"comment","z":"52c7d8c93d68afb5","name":"Example: Concatenate Mode - Batch Filter & Concat","info":"*Concatenate mode* of batch node can be used to combine input message sequences to create a new message sequence. Order of the sequences can be specified using message topic assigned to each message in a sequence. Message sequence can be specified multiple times.\n","x":810,"y":880,"wires":[]},{"id":"4aa7d5ab1091553e","type":"comment","z":"52c7d8c93d68afb5","g":"3eda846bc0d54e59","name":"Set topic \"POS\" \\n or \"NEG\"","info":"","x":1140,"y":1040,"wires":[]}] ``` :: ::node-red-help --- category: sequence file: 19-batch name: Batch node: batch --- :: # Sequence The **Sequence** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [Split](https://flowfuse.com/docs/node-red/core-nodes/sequence/split/) - [Sort](https://flowfuse.com/docs/node-red/core-nodes/sequence/sort/) - [Batch](https://flowfuse.com/docs/node-red/core-nodes/sequence/batch/) - [Join](https://flowfuse.com/docs/node-red/core-nodes/sequence/join/) # Join Joins sequences of messages into a single message. ## Where and why do we use the Join node? The Join node combines multiple messages into one. It's the counterpart to the Split node and can automatically reverse a split operation, or you can configure it to merge messages from different sources based on specific rules. This is essential when you need to aggregate data from multiple sources, reassemble split sequences, or reduce message streams into summary values. ## Modes of operation The Join node operates in three different modes, each suited for different use cases. ### Automatic Mode When paired with the Split node, automatically joins messages to reverse the split that was performed. Uses the `msg.parts` property of incoming messages to determine how the sequence should be joined. The `msg.parts` property should contain: - **id** - identifier for the message group - **index** - position within the group - **count** - total number of messages in the group - **type** - the message type (string, array, object, or buffer) - **ch** - for strings or buffers, the delimiter used to split - **key** - for objects, the property key this message came from - **len** - the length when split using fixed length ### Manual Mode Configure how to join sequences by selecting which message property to join and choosing the output format: - **String or buffer** - joins the selected property with specified join characters or buffer - **Array** - adds each selected property or entire message to an output array - **Key/value object** - uses a property of each message as the key for storing the required value - **Merged object** - merges the property of each message under a single object You can define when to send the combined message: - After a specific number of message parts - After a timeout following the first message - After receiving a message with `msg.complete` property set ### Reduce Sequence Mode Applies a JSONata expression to each message in a sequence and accumulates the result to produce a single message. This is useful for calculations like sums, averages, or any custom aggregation logic. The reduce expression runs for each message with special variables available: - `$A` - the accumulated value - `$I` - index of the message in the sequence - `$N` - number of messages in the sequence An optional fix-up expression can be applied after all messages have been processed to perform final calculations. ## How the node handles messages The Join node buffers messages internally to work across sequences. The Node-RED runtime setting `nodeMessageBufferMaxLength` limits how many messages can be buffered to prevent memory issues. If you send a message with the `msg.reset` property set, the node clears the partly complete message without sending it and resets any part counts. When using manual mode with timeout, send a message with `msg.restartTimeout` set to restart the timeout. For manual mode, the other properties of the output message come from the last message received before sending. ## Examples ### Automatic mode This example shows automatic mode. The Split node breaks an array into individual messages, then Join automatically reassembles them back into the original array. ::render-flow ```json [{"id":"b5ea6d2a.6e7bb","type":"tab","label":"openValve","disabled":false,"info":""},{"id":"84ed227552b4e6eb","type":"join","z":"b5ea6d2a.6e7bb","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":590,"y":300,"wires":[["f2dba285d7a067cd"]]},{"id":"522b4e247e84ac0e","type":"inject","z":"b5ea6d2a.6e7bb","name":"Send array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[ { \"id\": 1, \"task\": \"Complete project proposal\", \"completed\": false }, { \"id\": 2, \"task\": \"Review presentation slides\", \"completed\": true }, { \"id\": 3, \"task\": \"Prepare for client meeting\", \"completed\": false } ]","payloadType":"json","x":220,"y":300,"wires":[["351e98a55e5a50c6"]]},{"id":"f2dba285d7a067cd","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":300,"wires":[]},{"id":"d47a5edb2d5d5b70","type":"comment","z":"b5ea6d2a.6e7bb","name":"Joining the messages to reverse the split that was performed.","info":"","x":500,"y":220,"wires":[]},{"id":"351e98a55e5a50c6","type":"split","z":"b5ea6d2a.6e7bb","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":410,"y":300,"wires":[["84ed227552b4e6eb"]]}] ``` :: ### Manual mode Here manual mode combines three separate sensor readings into one object. Each message has a different `msg.topic` (temperature, humidity, pressure) and those topics become the keys in the output object. ::render-flow ```json [{"id":"b5ea6d2a.6e7bb","type":"tab","label":"openValve","disabled":false,"info":""},{"id":"84ed227552b4e6eb","type":"join","z":"b5ea6d2a.6e7bb","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":490,"y":300,"wires":[["f2dba285d7a067cd"]]},{"id":"522b4e247e84ac0e","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"temperature","payload":"40","payloadType":"num","x":260,"y":240,"wires":[["84ed227552b4e6eb"]]},{"id":"12e54e4066bac7a3","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"humidity","payload":"33","payloadType":"num","x":270,"y":300,"wires":[["84ed227552b4e6eb"]]},{"id":"b04d4f51f0602607","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"pressure","payload":"1000","payloadType":"num","x":270,"y":360,"wires":[["84ed227552b4e6eb"]]},{"id":"f2dba285d7a067cd","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":720,"y":300,"wires":[]},{"id":"d47a5edb2d5d5b70","type":"comment","z":"b5ea6d2a.6e7bb","name":"Combining three payload into one object ","info":"","x":520,"y":180,"wires":[]}] ``` :: ### Reduce sequence mode This example uses reduce mode to calculate total inventory. The expression `$A+payload.quantity` adds each item's quantity to the running total, starting from 0. ::render-flow ```json [{"id":"b5ea6d2a.6e7bb","type":"tab","label":"openValve","disabled":false,"info":""},{"id":"84ed227552b4e6eb","type":"join","z":"b5ea6d2a.6e7bb","name":"","mode":"reduce","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"3","reduceRight":false,"reduceExp":"$A+payload.quantity","reduceInit":"0","reduceInitType":"num","reduceFixup":"$A","x":590,"y":300,"wires":[["f2dba285d7a067cd"]]},{"id":"522b4e247e84ac0e","type":"inject","z":"b5ea6d2a.6e7bb","name":"Send array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[ { \"id\": 1, \"name\": \"Laptop\", \"quantity\": 15 }, { \"id\": 2, \"name\": \"Printer\", \"quantity\": 5 }, { \"id\": 3, \"name\": \"Monitor\", \"quantity\": 10 } ]","payloadType":"json","x":220,"y":300,"wires":[["351e98a55e5a50c6"]]},{"id":"f2dba285d7a067cd","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":300,"wires":[]},{"id":"d47a5edb2d5d5b70","type":"comment","z":"b5ea6d2a.6e7bb","name":"Calculating total stocks using Join node reduced expression mode","info":"","x":510,"y":220,"wires":[]},{"id":"351e98a55e5a50c6","type":"split","z":"b5ea6d2a.6e7bb","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":410,"y":300,"wires":[["84ed227552b4e6eb"]]}] ``` :: ::node-red-help --- category: sequence file: 17-split name: Join node: join --- :: # Sort Sorts an array or a sequence of messages. ## Where and why do we use the Sort node? The Sort node arranges data in ascending or descending order. You can sort either an array within a message payload or a sequence of messages based on their properties. This is essential when you need to organize data before displaying it, process items by priority, or find top/bottom values in datasets. ## Modes of operation The Sort node operates in two different modes: ### Array Sorting Sorts an array stored in a message property. The entire array gets arranged based on element values or a JSONata expression. Use this when you have a complete dataset in one message that needs ordering. ### Message Sequence Sorting Sorts a sequence of messages that have a `msg.parts` property. Messages need these fields in `msg.parts`: - **id** - identifier for the message group - **index** - position within the group - **count** - total messages in the group The Split node automatically creates `msg.parts`, but you can set it manually if needed. Use this mode when processing streams of individual messages that need to be reordered based on their properties. ## How the node handles messages The Sort node buffers messages internally when working with message sequences. For array sorting, it processes the array immediately and outputs the sorted result. For message sequences, it collects all messages in the sequence before sorting and releasing them in the new order. When sorting, you can specify: - **Element value** - Sorts based on the element's value directly - **Expression** - Uses a JSONata expression to extract the sort value from complex objects The sort direction can be: - **Ascending** - Smallest to largest (A to Z) - **Descending** - Largest to smallest (Z to A) Enable **As numbers** to sort numerically instead of alphabetically. Without this, "10" comes before "2" because it's treated as text. ## Examples ### Sorting arrays This example sorts numbers and letters in ascending order. The arrays get arranged from smallest to largest, or A to Z. ::render-flow ```json [{"id":"b5ea6d2a.6e7bb","type":"tab","label":"openValve","disabled":false,"info":""},{"id":"416d6d32df411abe","type":"sort","z":"b5ea6d2a.6e7bb","name":"","order":"ascending","as_num":false,"target":"payload","targetType":"msg","msgKey":"payload","msgKeyType":"elem","seqKey":"payload.quantity","seqKeyType":"msg","x":570,"y":320,"wires":[["eb923bde78247dc5"]]},{"id":"c8bd64176725f43f","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[7,8,41,90,2,4,2]","payloadType":"json","x":360,"y":320,"wires":[["416d6d32df411abe"]]},{"id":"eb923bde78247dc5","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":320,"wires":[]},{"id":"67f2e62eec9509c5","type":"comment","z":"b5ea6d2a.6e7bb","name":"Ordering numbers in ascending order","info":"","x":530,"y":240,"wires":[]},{"id":"481e382abac7a730","type":"sort","z":"b5ea6d2a.6e7bb","name":"","order":"ascending","as_num":false,"target":"payload","targetType":"msg","msgKey":"payload","msgKeyType":"elem","seqKey":"payload.quantity","seqKeyType":"msg","x":570,"y":440,"wires":[["f43093160e436025"]]},{"id":"21502b212a9c0f80","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[\"G\", \"F\", \"T\", \"A\", \"R\", \"P\", \"H\", \"W\", \"C\", \"Y\", \"N\", \"B\", \"L\", \"O\", \"X\", \"I\", \"V\", \"E\", \"J\", \"U\", \"K\", \"M\", \"S\", \"Z\", \"D\", \"Q\"]","payloadType":"json","x":330,"y":440,"wires":[["481e382abac7a730"]]},{"id":"f43093160e436025","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":440,"wires":[]},{"id":"dba624ac20a75580","type":"comment","z":"b5ea6d2a.6e7bb","name":"Ordering alphabets in ascending order","info":"","x":510,"y":380,"wires":[]}] ``` :: ### Sorting message sequences Here the Sort node arranges a sequence of messages in descending order by the quantity property. The Split node breaks the array into individual messages, each gets sorted by its quantity value, and higher quantities come first. ::render-flow ```json [{"id":"416d6d32df411abe","type":"sort","z":"b5ea6d2a.6e7bb","name":"","order":"descending","as_num":false,"target":"","targetType":"seq","msgKey":"payload","msgKeyType":"elem","seqKey":"payload.quantity","seqKeyType":"msg","x":550,"y":320,"wires":[["eb923bde78247dc5"]]},{"id":"c8bd64176725f43f","type":"inject","z":"b5ea6d2a.6e7bb","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[ { \"id\": 1, \"name\": \"Laptop\", \"quantity\": 15 }, { \"id\": 2, \"name\": \"Printer\", \"quantity\": 5 }, { \"id\": 3, \"name\": \"Monitor\", \"quantity\": 10 } ]","payloadType":"json","x":270,"y":320,"wires":[["05e33079464a9243"]]},{"id":"eb923bde78247dc5","type":"debug","z":"b5ea6d2a.6e7bb","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":720,"y":320,"wires":[]},{"id":"05e33079464a9243","type":"split","z":"b5ea6d2a.6e7bb","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":410,"y":320,"wires":[["416d6d32df411abe"]]},{"id":"67f2e62eec9509c5","type":"comment","z":"b5ea6d2a.6e7bb","name":"Ordering sequence of messages in descending order based on the quantity property of each message.","info":"","x":530,"y":240,"wires":[]}] ``` :: ::node-red-help --- category: sequence file: 18-sort name: Sort node: sort --- :: # Split Splits a message into a sequence of messages. ## Where and why do we use the Split node? The Split node breaks one message into multiple messages. This is essential when you receive bulk data that needs individual processing. For example, a SQL query might return hundreds of rows, or an API might send multiple sensor readings in one payload. The Split node turns that single message into a stream of messages you can process one at a time. ## Modes of operation The Split node's behavior depends on what type of data is in `msg.payload`: ### String/Buffer Splits on a specified character (default is `\n` for newlines), a buffer sequence, or into fixed lengths. You can use spaces for words, commas for CSV data, or any character. Also supports multi-character strings and buffer sequences. ### Array Splits into individual array elements, or into arrays of a fixed length. Useful when APIs have batch size limits and you need to chunk data into smaller groups. ### Object Sends one message for each key-value pair. By default, the key name goes into `msg.topic` and the value goes into `msg.payload`. ## How the node handles messages Each output message gets a `msg.parts` property with information about how it was split from the original. This lets the Join node reassemble the sequence back into a single message. The property contains: - **id** - identifier for the message group - **index** - position within the group - **count** - total messages in the group (not set in streaming mode since the total is unknown) - **type** - the original data type (string, array, object, or buffer) - **ch** - for strings or buffers, the delimiter used to split the message - **key** - for objects, the key name this message came from (also copied to `msg.topic` by default) - **len** - when using fixed length splitting, the length of each segment ### Streaming mode In streaming mode, the node processes incomplete data across multiple messages. Say a serial device sends newline-terminated commands but a message ends mid-command. The node splits and sends the complete parts, then holds the incomplete part and prepends it to the next message that arrives. Because streaming mode doesn't know how many messages to expect, it doesn't set `msg.parts.count`. This means you can't use it with the Join node in automatic mode, since Join needs to know when the sequence is complete. ## Examples ### Splitting arrays Arrays are the simplest case. Feed in an array and get one message per element. Here an array `[1, 2, 3, 4]` becomes four messages. ::render-flow ```json [{"id":"6354daaccf2b2504","type":"inject","z":"2862bf5c278ff5bd","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1, 2, 3, 4]","payloadType":"json","x":140,"y":100,"wires":[["82ab52c7f894f725"]]},{"id":"82ab52c7f894f725","type":"split","z":"2862bf5c278ff5bd","name":"Split Array","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":310,"y":100,"wires":[["80ee79b75e373ba9"]]},{"id":"80ee79b75e373ba9","type":"debug","z":"2862bf5c278ff5bd","name":"Print individual values","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":520,"y":100,"wires":[]}] ``` :: ### Regrouping elements Sometimes you need to chunk data into smaller groups. Say an API only accepts 20 records at a time but you have 100. Set `Fixed length of` to split the array into chunks of that size. With input `[1, 2, 3, 4, 5]` and `Fixed length of` set to 2, you get three messages: `[1, 2]`, `[3, 4]`, and `[5]`. ::render-flow ```json [{"id":"57087c8029d44fa2","type":"inject","z":"2862bf5c278ff5bd","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1, 2, 3, 4, 5]","payloadType":"json","x":150,"y":160,"wires":[["b8d0aec7f0cba6c5"]]},{"id":"b8d0aec7f0cba6c5","type":"split","z":"2862bf5c278ff5bd","name":"Regroup array","splt":"\\n","spltType":"str","arraySplt":"2","arraySpltType":"len","stream":false,"addname":"","x":340,"y":160,"wires":[["d45d698bae8b575d"]]},{"id":"d45d698bae8b575d","type":"debug","z":"2862bf5c278ff5bd","name":"Print individual values","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":560,"y":160,"wires":[]}] ``` :: ### Splitting strings The default string split uses `\n` (newline) as the delimiter, which splits text by line. This works for processing logs, CSV data, or any line-based format. Here we split a list of European cities, one per line. ::render-flow ```json [{"id":"39a0a053a3696cd7","type":"inject","z":"2862bf5c278ff5bd","name":"Trigger","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":220,"wires":[["60bf012438abb4eb"]]},{"id":"4b56a3ed831df59e","type":"split","z":"2862bf5c278ff5bd","name":"Split by line","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":470,"y":220,"wires":[["31f8ca22882b297f"]]},{"id":"31f8ca22882b297f","type":"debug","z":"2862bf5c278ff5bd","name":"Print each line","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":660,"y":220,"wires":[]},{"id":"60bf012438abb4eb","type":"template","z":"2862bf5c278ff5bd","name":"Data in lines","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Amsterdam\nAndorra la Vella\nAthens","output":"str","x":290,"y":220,"wires":[["4b56a3ed831df59e"]]}] ``` :: ### Splitting by word Change the delimiter to a space and you can split sentences into words. Put a space character in the `Split using` field. It won't be visible in the form but it's there. ::render-flow ```json [{"id":"619209d6e3f02473","type":"inject","z":"2862bf5c278ff5bd","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"foo bar","payloadType":"str","x":130,"y":280,"wires":[["15b9b3d17a64e2c7"]]},{"id":"15b9b3d17a64e2c7","type":"split","z":"2862bf5c278ff5bd","name":"Split by space","splt":" ","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":300,"y":280,"wires":[["12607e8708ef58f2"]]},{"id":"12607e8708ef58f2","type":"debug","z":"2862bf5c278ff5bd","name":"Print each word","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":500,"y":280,"wires":[]}] ``` :: ### Splitting objects When you split an object, you get one message per key-value pair. The key goes into `msg.topic` and the value goes into `msg.payload`. This example splits a simple object mapping words to numbers. ::render-flow ```json [{"id":"3c4c5535ec3b2138","type":"inject","z":"2862bf5c278ff5bd","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"one\": 1, \"two\": 2}","payloadType":"json","x":170,"y":340,"wires":[["eb3227c954debb95"]]},{"id":"eb3227c954debb95","type":"split","z":"2862bf5c278ff5bd","name":"Split map","splt":"\\n","spltType":"str","arraySplt":"1","arraySpltType":"len","stream":false,"addname":"","x":360,"y":340,"wires":[["8c82877cdaff8f0d"]]},{"id":"8c82877cdaff8f0d","type":"debug","z":"2862bf5c278ff5bd","name":"Print property values","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":560,"y":340,"wires":[]}] ``` :: ::node-red-help --- category: sequence file: 17-split name: Split node: split --- :: # Storage The **Storage** section of Node-RED's default palette. Each page opens with why you would reach for that node, then mirrors the node's built-in help. - [Write File](https://flowfuse.com/docs/node-red/core-nodes/storage/write-file/) - [Read File](https://flowfuse.com/docs/node-red/core-nodes/storage/read-file/) # Read File ## What is the Read File node in Node-RED? The Read File node in Node-RED is used to read the contents of a file from the file system. In FlowFuse Cloud, the Read File node interacts with a cloud-based storage solution, leveraging AWS S3 for file storage. However, in the Node-RED instance running on edge devices using FlowFuse device agent, this node will read the file from the device's local file system. The Read File node can read both string and binary buffer data, making it versatile for various integration needs within Node-RED flows. ## Configuring the Read File node - **Filename:**Specify the filename. - **Path:** Define the path to the file that needs to be read. - **msg:** Use a message property to dynamically set the filename. By default, it will use `msg.filename`. If `msg.filename` is used, the file will be closed after every write. For optimal performance, consider using a fixed filename. - **Expression:** Utilize a JSON expression to dynamically set the filename based on data in the flow. - **Environment Variable (env var):** Utilize an environment variable to dynamically set the filename. - **Output Format:** - **Single UTF string:** Output the file contents as a single UTF-8 string. - **A message per line:** Output each line of the file as a separate message. - **A single buffer object:** Output the contents of the file as a single buffer object. - **A stream of buffers:** Output the contents of file as chunks of buffers. The chunk size being operating system dependant, but typically 64k (Linux/Mac) or 41k (Windows). - **Include all existing properties in each msg:** When enabled, all existing properties in the input message (`msg`) will be included in each output message generated by the node. - **Encoding:** Specify the encoding format to use when reading the file if the output format is set to string. *Tip: Always use an absolute path for the filename to ensure Node-RED can accurately locate and manipulate the specified file.* ## Output - **payload**: The contents of the file as either a string or binary buffer. - **filename**: If not configured in the node, this optional property sets the name of the file to be read. ## Usecases 1. **Configuration Loading:** Read configuration files containing parameters or settings for your Node-RED flows or applications. This allows you to dynamically adjust the behavior of your flows without modifying the flow structure. 2. **Data Aggregation:** Read multiple data files and aggregate their contents into a single message or dataset for further processing or analysis. This can be useful for tasks like combining multiple CSV files into a unified dataset. 3. **System Monitoring:** Read system log files or status reports to monitor the health and performance of various components within your system. You can then analyze this data within Node-RED to trigger alerts or perform diagnostics. 4. **Content Parsing:** Read files containing structured data formats, such as XML or JSON, and parse their contents within Node-RED to extract relevant information. This can be useful for tasks like extracting data from API responses or parsing configuration files. 5. **File Transformation:** Read files in one format and transform their contents into a different format using Node-RED's processing capabilities. For example, you can read a CSV file and convert its contents into JSON format for further processing or visualization. 6. **External Integration:** Read data from files generated by external systems or services and integrate this data into your Node-RED flows for further processing or action. This can facilitate interoperability between different systems or applications. 7. **File Monitoring:** Continuously monitor files for changes or updates using the Read File node, triggering flows or actions based on the detected changes. This enables real-time processing of file-based events within your Node-RED application. 8. **Data Backup and Recovery:** Read files containing backup data or snapshots and use Node-RED to manage and automate backup and recovery processes. ## Examples 1. In the example flow, we demonstrate how to read the file content using the Read File node and obtain the output as a string. ::render-flow ```json [{"id":"5098633f9aec714f","type":"file in","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"","filename":"/myfile.txt/","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":520,"y":360,"wires":[["60134a0cf5da9669"]]},{"id":"c4feedf988092418","type":"inject","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Read file","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":280,"y":360,"wires":[["5098633f9aec714f"]]},{"id":"60134a0cf5da9669","type":"debug","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":360,"wires":[]},{"id":"bfb2903869157e1f","type":"file","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"","filename":"/myfile.txt/","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":520,"y":240,"wires":[["2537ebc276403591"]]},{"id":"01503f5a493d03cc","type":"inject","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Write into file","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":240,"wires":[["bfb2903869157e1f"]]},{"id":"2537ebc276403591","type":"debug","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":240,"wires":[]},{"id":"42f7744a16a52b9d","type":"comment","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Reading file using read file node","info":"","x":530,"y":300,"wires":[]},{"id":"8155df85eb599235","type":"comment","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Creating a myfile.txt file and writing content to it.","info":"","x":520,"y":180,"wires":[]}] ``` :: 2. In the example flow, we demonstrate how to read the file content using the Read File node and obtain the output as a buffer object. ::render-flow ```json [{"id":"ead17e92ffc5c32a","type":"group","z":"b5ea6d2a.6e7bb","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["5098633f9aec714f","c4feedf988092418","60134a0cf5da9669","bfb2903869157e1f","01503f5a493d03cc","2537ebc276403591","42f7744a16a52b9d","8155df85eb599235"],"x":174,"y":139,"w":712,"h":262},{"id":"5098633f9aec714f","type":"file in","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"","filename":"/myfile.txt/","filenameType":"str","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":520,"y":360,"wires":[["60134a0cf5da9669"]]},{"id":"c4feedf988092418","type":"inject","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Read file","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":280,"y":360,"wires":[["5098633f9aec714f"]]},{"id":"60134a0cf5da9669","type":"debug","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":360,"wires":[]},{"id":"bfb2903869157e1f","type":"file","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"","filename":"/myfile.txt/","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":520,"y":240,"wires":[["2537ebc276403591"]]},{"id":"01503f5a493d03cc","type":"inject","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Write into file","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":240,"wires":[["bfb2903869157e1f"]]},{"id":"2537ebc276403591","type":"debug","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":240,"wires":[]},{"id":"42f7744a16a52b9d","type":"comment","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Reading file using read file node","info":"","x":530,"y":300,"wires":[]},{"id":"8155df85eb599235","type":"comment","z":"b5ea6d2a.6e7bb","g":"ead17e92ffc5c32a","name":"Creating a myfile.txt file and writing content to it.","info":"","x":520,"y":180,"wires":[]}] ``` :: ::node-red-help --- category: storage file: 10-file name: Read File node: file in --- :: # Write File ## What is the Write File Node in Node-RED? The "Write File" node in Node-RED is used to write data to a file on the filesystem. It's commonly employed in flows where you need to save data or logs for later analysis or storage. In FlowFuse Cloud, the Write File node interacts with a cloud-based storage solution, leveraging AWS S3 for file storage. However, in the Node-RED instance running on edge devices using the [FlowFuse device agent](https://flowfuse.com/platform/device-agent/), this node will interact with the local file system of that device. The content to be written is specified using `msg.payload`. ## Configuring the Write File Node in Node-RED: - **Filename:** Specify the filename - **Path:** Specify the path to the file where data will be saved. - **msg:** Use a message property to dynamically set the filename, By default, it will use `msg.filename`, and if `msg.filename` is used, the file will be closed after every write. For the best performance, use a fixed filename. - **Expression:** Use a JSON expression to set the filename dynamically based on data in the flow. - **Environment Variable (env var):** Use an environment variable to set the filename dynamically. - **Action:** Choose the action - **Append to File:** Adds new data to the existing file. - **Overwrite File:** Replaces the content of the file with new data. - **Delete File:** Removes the specified file from the filesystem. - **Add Newline (\n) to Each Payload:** Enabling this option will append a newline character to each payload before writing to the file. - **Create Directory if it Doesn't Exist:** Enabling this option will create the specified directory if it is not already present in the filesystem. - **Encoding:** Specifies the character encoding to be used when writing data to the file, when selecting "set by `msg.encoding`" you can set it dynamically. *Tip: Always use an absolute path for the filename to ensure Node-RED can accurately locate and manipulate the specified file.* ## Output After the completion of the write operation, the input message passed to the write file node is sent to the output port. ## Use Cases 1. **Data Logging:** Store sensor readings, IoT device data, or system metrics into a file for historical analysis or monitoring trends over time. 2. **Error Logging:** Capture and log error messages, exceptions, or debugging information to a file for troubleshooting and debugging purposes. 3. **User Inputs:** Save user inputs or form submissions to a file, such as user preferences, feedback, or user-generated content. 4. **Reporting:** Generate reports in CSV, JSON, or plain text formats and save them to a file for later retrieval or distribution. 5. **Backup and Recovery:** Create backup files of critical data or system states for disaster recovery or version control purposes. 6. **Integration with External Systems:** Save data retrieved from APIs, databases, or external services to a file for further processing or analysis. 7. **Archiving:** Archive log files, historical data, or outdated documents to maintain a record of past events or changes. 8. **Data Transformation:** Perform data transformation operations and write the transformed data to a file in a different format or structure. 9. **Event Logging:** Log events, notifications, or user interactions to a file for auditing, compliance, or historical tracking purposes. ## Examples 1. In the example flow below, we demonstrate how to create a file and write content to it. ::render-flow ```json [{"id":"aa247fb1ef163c92","type":"group","z":"b5ea6d2a.6e7bb","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["bfb2903869157e1f","01503f5a493d03cc","2537ebc276403591","8ad766e1d67466f4"],"x":194,"y":179,"w":712,"h":162},{"id":"bfb2903869157e1f","type":"file","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"","filename":"/myfile.txt/","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":540,"y":300,"wires":[["2537ebc276403591"]]},{"id":"01503f5a493d03cc","type":"inject","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"Write into file","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":310,"y":300,"wires":[["bfb2903869157e1f"]]},{"id":"2537ebc276403591","type":"debug","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":800,"y":300,"wires":[]},{"id":"8ad766e1d67466f4","type":"comment","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"Write timestamps to the myfile.txt file. If the file doesn't exist, it will be created.","info":"","x":550,"y":220,"wires":[]}] ``` :: 2. In the example flow below, we demonstrate how to delete a file using the Write File node. ::render-flow ```json [{"id":"b5ea6d2a.6e7bb","type":"tab","label":"openValve","disabled":false,"info":""},{"id":"aa247fb1ef163c92","type":"group","z":"b5ea6d2a.6e7bb","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["bfb2903869157e1f","01503f5a493d03cc","2537ebc276403591","8ad766e1d67466f4"],"x":194,"y":179,"w":712,"h":162},{"id":"bfb2903869157e1f","type":"file","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"","filename":"/myfile.txt/","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"delete","encoding":"none","x":550,"y":300,"wires":[["2537ebc276403591"]]},{"id":"01503f5a493d03cc","type":"inject","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"Delete file","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":300,"wires":[["bfb2903869157e1f"]]},{"id":"2537ebc276403591","type":"debug","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":800,"y":300,"wires":[]},{"id":"8ad766e1d67466f4","type":"comment","z":"b5ea6d2a.6e7bb","g":"aa247fb1ef163c92","name":"Deleting myfile.txt using write file node","info":"","x":550,"y":220,"wires":[]}] ``` :: ::node-red-help --- category: storage file: 10-file name: Write File node: file --- :: # Using DynamoDB with Node-RED (2026 Updated) Amazon's DynamoDB is a fully managed NoSQL database service known for its fast and predictable performance and scalable design. This makes it suitable for applications needing low-latency responses. In this documentation, we’ll look at how to set up and use DynamoDB, configure the necessary IAM roles, and apply Node-RED flows to store and retrieve data effectively. ## Setting Up DynamoDB ### Creating a DynamoDB Table ![AWS DynamoDB with Node-RED](https://flowfuse.com/docs/node-red/database/images/flowfuse-dynamodb-aws-setup-node-red.png){dataZoomable=""} There's no need to create a DynamoDB database, you do need to create a table. Here’s how you can do it: 1. Log into the AWS Management Console and navigate to the DynamoDB section. 2. Click on **Create table**. 3. For the table name, enter a table name. We will use **FlowFuse-Email** for this demonstration. For the partition key, use **email** with type String. 4. We will keep the **default settings**. 5. Finally, click on **Create** to establish your table. ### Configuring IAM for DynamoDB ![AWS IAM DynamoDB with Node-RED](https://flowfuse.com/docs/node-red/database/images/dynamodb-flowfuse-iam-node-red.png){dataZoomable=""} When working with Amazon DynamoDB, it’s crucial to ensure that the right entities have the appropriate permissions to perform actions on your database. This is where [AWS Identity and Access Management (IAM)](https://aws.amazon.com/iam/){rel=""nofollow""} plays an important role. By properly configuring IAM, you ensure that your data is not only secure from unauthorized access but also that the correct roles and users have the precise level of access needed, avoiding any unnecessary permissions that could lead to security risks. #### Create a New IAM Role with Necessary Policies: 1. Go to IAM in the AWS Console. 2. Create a new role and give a name. 3. For permission options select **Attach Policies Directly**. 4. Search for the policy **AmazonDynamoDBFullAccess** and select **Next**. 5. For finer control, customize the policy to restrict access as needed. 6. Lastly click **Create User**. #### Generate Access Keys: 1. Inside the IAM console select **Users**. 2. Select the newly created user. 3. Navigate to **Security credentials**. 4. Navigate to Access Keys and select **Create access key**. 5. Select **Application running outside AWS** and click **Next**. 6. Give description tag if desired and click **Create access Key**. 7. Save the **Access ID** and **Secret Key** for use later. ## Working with DynamoDB: Use Case Flows ![DynamoDB Flow Node-RED](https://flowfuse.com/docs/node-red/database/images/node-red-dynamodb-flow-flowfuse.png){dataZoomable=""} ### Prerequisites Please install the following nodes: 1. [node-red-contrib-aws](https://flows.nodered.org/node/node-red-contrib-aws){rel=""nofollow""} 2. [node-red-node-data-generator](https://flows.nodered.org/node/node-red-node-data-generator){rel=""nofollow""} Now, let's dive into some practical examples using Node-RED to manage a customer list: ### Inserting Data into DynamoDB This first flow will generate data and send it to DynamoDB via the PutItem operation. ::render-flow ```json [{"id":"10b0b970739d616f","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"","operation":"PutItem","Statements":"","RequestItems":"","TableName":"FlowFuse-Demo","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":820,"y":240,"wires":[["0157a98fa69fe514"],["dc92470ddaa225bf"]]},{"id":"functionNode","type":"function","z":"37b2bc0d1ebdc616","name":"Set Value","func":"msg.Item = {\n \"source\": { \"S\": msg.source },\n \"time\": { \"N\": String(msg.payload) },\n \"temp\": {\"S\": String(msg.datagen.temp) },\n \"email\": {\"S\": msg.datagen.email},\n \"name\": {\"S\": msg.datagen.name},\n \"work\": {\"S\": msg.datagen.work},\n \"address\": {\"S\": msg.datagen.address},\n \"country\": {\"S\": msg.datagen.country},\n};\n\nmsg.TableName = \"FlowFuse-Email\";\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":240,"wires":[["10b0b970739d616f"]]},{"id":"0157a98fa69fe514","type":"debug","z":"37b2bc0d1ebdc616","name":"Store Customer Info","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1060,"y":220,"wires":[]},{"id":"dc92470ddaa225bf","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 28","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1020,"y":260,"wires":[]},{"id":"45d46649bae57332","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"source","v":"FF_DEVICE_NAME","vt":"env"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":240,"wires":[["1160ce65b26429f3"]]},{"id":"1160ce65b26429f3","type":"data-generator","z":"37b2bc0d1ebdc616","name":"","field":"datagen","fieldType":"msg","syntax":"json","template":"{\n \"name\": \"{{firstName}} {{lastName}}\",\n \"work\": \"{{company}}\",\n \"email\": \"{{email}}\",\n \"address\": \"{{int 1 100}} {{street}}\",\n \"country\": \"{{country}}\",\n \"temp\": {{float 30 36}}\n}","x":440,"y":240,"wires":[["functionNode"]]}] ``` :: 1. Configure the AWS DyanamoDB node by **editing** the node. 2. Click the **pencil** to create a new config. 3. Give it a **name**. 4. Input the **region** where your DynamoDB is located. (e.g. us-east-1) 5. Input Access ID and Secrete key provide in the step above "**Configuring IAM for DynamoDB**." 6. Click **Confirm**. 7. **Trigger** the inject node and confirm you don't have any errors. 8. The Debug node can confirm the data is stored correctly. 9. The data structure will look similar to the image below. ![DynamoDB Flow Node-RED](https://flowfuse.com/docs/node-red/database/images/dynamodb-data-structure-node-red-flowfuse.png){dataZoomable=""} ### Retrieving All Data for a specific Partition Key The GetItem operation fetches data, which you can then display using debug nodes. To retrieve specific customer information based on their email import this flow: ::render-flow ```json [{"id":"f7e069b78d2ef5c5","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":340,"wires":[["07caf16318aea2f6"]]},{"id":"07caf16318aea2f6","type":"function","z":"37b2bc0d1ebdc616","name":"Get Specific Customer Info","func":"msg = {\n TableName: \"FlowFuse-Email\",\n Key: {\n \"email\": { \"S\": \"florance.shelly@cirpria.xyz\" } //put email you want to search here\n }\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":340,"wires":[["3485e6cddd28c3eb"]]},{"id":"3485e6cddd28c3eb","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"","operation":"GetItem","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":760,"y":340,"wires":[["0c0b39c735d71d77"],["ae673a52c1c7a123"]]},{"id":"0c0b39c735d71d77","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 30","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1000,"y":320,"wires":[]},{"id":"ae673a52c1c7a123","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1000,"y":360,"wires":[]}] ``` :: ![Dynamodb Get Item flowfuse](https://flowfuse.com/docs/node-red/database/images/node-red-dynamodb-get-item-flowfuse.png){dataZoomable=""} The function node specifies the **TableName** and **Partition key** to search against. ### Querying Data When you need to find items under specific criteria, set up your query with partition keys and conditions. The Query operation allows you to efficiently retrieve data without scanning the entire contents of a particular partition key. If a key has significant about of data, but only need one particular value. This would then be the ideal path. ::render-flow ```json [{"id":"99a876a56a78ae25","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":420,"wires":[["d59f213bc3f6712a"]]},{"id":"d59f213bc3f6712a","type":"function","z":"37b2bc0d1ebdc616","name":"Returns only the attriubutes desired","func":"msg.TableName = \"FlowFuse-Email\";\nmsg.KeyConditionExpression = \"email = :email\";\nmsg.ExpressionAttributeValues = {\n \":email\": { \"S\": \"florance.shelly@cirpria.xyz\" }\n};\nmsg.ProjectionExpression = \"#n, #t\";\nmsg.ExpressionAttributeNames = {\n \"#t\": \"temp\",\n \"#n\": \"name\"\n} // Only retrieve these attributes\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":420,"wires":[["e178ffb9a87226a5"]]},{"id":"e178ffb9a87226a5","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"","operation":"Query","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":730,"y":420,"wires":[["b2e6d9d9dcf9dcd3"],["b2595472f1dc6570"]]},{"id":"b2e6d9d9dcf9dcd3","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 32","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":400,"wires":[]},{"id":"b2595472f1dc6570","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 33","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":440,"wires":[]}] ``` :: In this example we are retrieving only **name** and **temp** from the matching partition key match. ![DynamoDB Query Node-RED](https://flowfuse.com/docs/node-red/database/images/node-red-dynamodb-query-flowfuse.png){dataZoomable=""} ### Scanning Data While scanning is available, it should be used sparingly due to its high demand on resources, especially in large databases. Use it when necessary prioritize using query for regular operations. ::render-flow ```json [{"id":"671cf37c1ae941d2","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"TableName","v":"FlowFuse-Email","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":540,"wires":[["8cb14f9530eeb524"]]},{"id":"8cb14f9530eeb524","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"","operation":"Scan","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":470,"y":540,"wires":[["86e3a35428218d91"],["5c6e85699cc783fc"]]},{"id":"86e3a35428218d91","type":"debug","z":"37b2bc0d1ebdc616","name":"Get Values From Partition Key","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":520,"wires":[]},{"id":"5c6e85699cc783fc","type":"debug","z":"37b2bc0d1ebdc616","name":"Should be used sparingly for Large databases","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":560,"wires":[]}] ``` :: ![DynamoDB Scan Node-RED](https://flowfuse.com/docs/node-red/database/images/node-red-dynamodb-scan-flowfuse.png){dataZoomable=""} ## Full Flow ::render-flow{:height='400'} ```json [{"id":"10b0b970739d616f","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"75ba91a6ba62cece","operation":"PutItem","Statements":"","RequestItems":"","TableName":"FlowFuse-Email","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":820,"y":220,"wires":[["0157a98fa69fe514"],["dc92470ddaa225bf"]]},{"id":"functionNode","type":"function","z":"37b2bc0d1ebdc616","name":"Set Value","func":"msg.Item = {\n \"source\": { \"S\": msg.source },\n \"time\": { \"N\": String(msg.payload) },\n \"temp\": {\"S\": String(msg.datagen.temp) },\n \"email\": {\"S\": msg.datagen.email},\n \"name\": {\"S\": msg.datagen.name},\n \"work\": {\"S\": msg.datagen.work},\n \"address\": {\"S\": msg.datagen.address},\n \"country\": {\"S\": msg.datagen.country},\n};\n\nmsg.TableName = \"FlowFuse-Email\";\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":220,"wires":[["10b0b970739d616f"]]},{"id":"0157a98fa69fe514","type":"debug","z":"37b2bc0d1ebdc616","name":"Store Customer Info","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1060,"y":200,"wires":[]},{"id":"dc92470ddaa225bf","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 28","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1020,"y":240,"wires":[]},{"id":"45d46649bae57332","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"source","v":"FF_DEVICE_NAME","vt":"env"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":220,"wires":[["1160ce65b26429f3"]]},{"id":"f7e069b78d2ef5c5","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":320,"wires":[["07caf16318aea2f6"]]},{"id":"07caf16318aea2f6","type":"function","z":"37b2bc0d1ebdc616","name":"Get Specific Customer Info","func":"msg = {\n TableName: \"FlowFuse-Email\",\n Key: {\n \"email\": { \"S\": \"florance.shelly@cirpria.xyz\" } //put email you want to search here\n }\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":320,"wires":[["3485e6cddd28c3eb"]]},{"id":"3485e6cddd28c3eb","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"75ba91a6ba62cece","operation":"GetItem","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":760,"y":320,"wires":[["0c0b39c735d71d77"],["ae673a52c1c7a123"]]},{"id":"0c0b39c735d71d77","type":"debug","z":"37b2bc0d1ebdc616","name":"Get Customer Info","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1030,"y":300,"wires":[]},{"id":"ae673a52c1c7a123","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1000,"y":340,"wires":[]},{"id":"99a876a56a78ae25","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":420,"wires":[["d59f213bc3f6712a"]]},{"id":"d59f213bc3f6712a","type":"function","z":"37b2bc0d1ebdc616","name":"Returns only the attriubutes desired","func":"msg.TableName = \"FlowFuse-Email\";\nmsg.KeyConditionExpression = \"email = :email\";\nmsg.ExpressionAttributeValues = {\n \":email\": { \"S\": \"florance.shelly@cirpria.xyz\" }\n};\nmsg.ProjectionExpression = \"#n, #t\";\nmsg.ExpressionAttributeNames = {\n \"#t\": \"temp\",\n \"#n\": \"name\"\n} // Only retrieve these attributes\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":420,"wires":[["e178ffb9a87226a5"]]},{"id":"e178ffb9a87226a5","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"75ba91a6ba62cece","operation":"Query","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":730,"y":420,"wires":[["b2e6d9d9dcf9dcd3"],["b2595472f1dc6570"]]},{"id":"b2e6d9d9dcf9dcd3","type":"debug","z":"37b2bc0d1ebdc616","name":"Get Specific Customer Info","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":400,"wires":[]},{"id":"b2595472f1dc6570","type":"debug","z":"37b2bc0d1ebdc616","name":"debug 33","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":440,"wires":[]},{"id":"1160ce65b26429f3","type":"data-generator","z":"37b2bc0d1ebdc616","name":"","field":"datagen","fieldType":"msg","syntax":"json","template":"{\n \"name\": \"{{firstName}} {{lastName}}\",\n \"work\": \"{{company}}\",\n \"email\": \"{{email}}\",\n \"address\": \"{{int 1 100}} {{street}}\",\n \"country\": \"{{country}}\",\n \"temp\": {{float 30 36}}\n}","x":440,"y":220,"wires":[["functionNode"]]},{"id":"671cf37c1ae941d2","type":"inject","z":"37b2bc0d1ebdc616","name":"","props":[{"p":"payload"},{"p":"TableName","v":"FlowFuse-Email","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":540,"wires":[["8cb14f9530eeb524"]]},{"id":"8cb14f9530eeb524","type":"AWS DynamoDB","z":"37b2bc0d1ebdc616","aws":"75ba91a6ba62cece","operation":"Scan","Statements":"","RequestItems":"","TableName":"","BackupName":"","GlobalTableName":"","ReplicationGroup":"","AttributeDefinitions":"","KeySchema":"","BackupArn":"","Key":"","ExportArn":"","Statement":"","TransactStatements":"","TableArn":"","S3Bucket":"","ResourceArn":"","Item":"","TargetTableName":"","Tags":"","TransactItems":"","TagKeys":"","PointInTimeRecoverySpecification":"","ContributorInsightsAction":"","ReplicaUpdates":"","TimeToLiveSpecification":"","name":"","x":470,"y":540,"wires":[["86e3a35428218d91"],["5c6e85699cc783fc"]]},{"id":"86e3a35428218d91","type":"debug","z":"37b2bc0d1ebdc616","name":"Get Values From Partition Key","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":520,"wires":[]},{"id":"5c6e85699cc783fc","type":"debug","z":"37b2bc0d1ebdc616","name":"Should be used sparingly for Large databases","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":560,"wires":[]},{"id":"75ba91a6ba62cece","type":"amazon config","name":"AWS-gdziuba","region":"us-east-1","proxyRequired":false,"proxy":""}] ``` :: # Using Firebase with Node-RED (2026 Updated) Firebase provides two database options: Realtime Database (RTDB) and Cloud Firestore. This guide focuses on Cloud Firestore, Firebase's newer, more flexible document database with better performance, richer queries, and multi-regional support. Cloud Firestore is a scalable NoSQL document database that offers real-time synchronization, offline support, and seamless integration with Node-RED. Using this combination, developers can build event-driven flows for IoT dashboards, notifications, and synchronized device management. ## Prerequisites Before you start, ensure you have the following: - **Node-RED instance**: Ensure you have a running Node-RED instance. The quickest and easiest way to set up Node-RED is via FlowFuse. [Sign up](https://app.flowforge.com/account/create/){rel=""nofollow""} to get started. Once you have a FlowFuse instance, you can easily manage, deploy, scale, and collaborate with your team on flows securely. - **Firebase account**: You will need a Firebase account with the necessary configuration details to create projects and access Cloud Firestore. ## Step 1: Install the Cloud Firestore Node-RED Package To connect Node-RED with Cloud Firestore, you need to install the required Node-RED node. 1. Open your Node-RED editor. 2. Go to **Menu → Manage palette → Install**. 3. In the search box, enter: `@gogovega/node-red-contrib-cloud-firestore` 4. Click **Install** next to the package. 5. After installation, restart your Node-RED instance to ensure the configuration node loads properly. 6. The Firestore nodes will appear in your palette, ready to use in your flows. ## Step 2: Configure the Firestore Node Once the Firestore nodes are installed, you need to configure them with your Firebase project credentials. 1. Drag a Firestore node (e.g., Firestore Out node) onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Click the **+** icon next to the **Database** field to add a new configuration. 4. In the **Authentication**tab: - Select **Email/Password** as the authentication type. - Enter your Firebase **API key** (from your project's web app settings). - Enter the **email** and **password** of a Firebase user with access to Firestore. 5. In the **Database**section: - Enter your **Firebase Project ID**. 6. Click **Done** to save the configuration. > **Security Note**: Keep your credentials secure. Avoid exposing your API key, email, or password publicly. When sharing flows, use [environment variables](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/) to keep sensitive information safe. ## Step 3: Create a Document Before sending data from Node-RED, you need a collection where the data will be stored. Firestore organizes data in documents within collections. 1. Drag a **Firestore Out** node onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Select the **Firestore configuration** you created in Step 2. 4. Set the **Operation** to `Set / Create Document`. 5. Enter the **Collection** name and **Document ID**: - You can enter them as static strings, e.g., `devices` and `raspberry_pi_5_01`. - Or you can set them dynamically using `msg.collection` and `msg.document`. 6. Drag an **Inject** node onto the canvas and connect it to the Firestore node. Configure the payload data you want to store. For example, set `msg.payload` to: ```json { "device_id": "device_001", "status": "online", "last_seen": "2025-09-22T13:10:00Z", "location": "Room 101" } ``` 7. Click **Done** to save the configuration. 8. Deploy the flow. To test, click the **Inject** button on the Inject node to send the data to Firestore. You should see the Firestore node update its status: - **Querying…** – Node-RED is sending the data to Firestore. - **Done** – Data has been successfully written to your collection. ## Step 4: Updating a Document Updating an existing document in Firestore lets you change one or more fields without replacing the entire document. 1. Drag a **Firestore Out** node onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Select the **Firestore configuration** you created earlier. 4. Set the **Operation** to `Update Document`. 5. Specify the **Collection** name and the **Document ID**you want to update. - Example: `devices` and `raspberry_pi_5_01`. 6. Connect an **Inject** node to provide the updated data. For example, set `msg.payload` to: ```json { "status": "offline", "last_seen": "2025-09-23T11:45:00Z" } ``` 7. Deploy the flow. 8. Click the **Inject** button. The Firestore node will update the specified fields in the document. > **Note**: Fields not included in `msg.payload` will remain unchanged. ## Step 5: Deleting a Document To remove a document from a Firestore collection: 1. Drag another **Firestore Out** node onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Select your Firestore configuration. 4. Set the **Operation** to `Delete Document`. 5. Enter the **Collection** and **Document ID**to delete. - Example: `devices` and `raspberry_pi_5_01`. 6. Connect an **Inject** node to trigger the deletion. 7. Deploy the flow and click the **Inject** button. Once executed, the specified document will be permanently removed from the collection. ## Step 6: Reading Data from Firestore The **Firestore Get** node allows Node-RED to read data from a Firestore collection or document. This is useful for dashboards, data processing, or one-time data retrieval. 1. Drag a **Firestore Get** node onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Select the **Firestore configuration** you created in Step 2. 4. Choose the **Type**: - **Collection** – Reads all documents within a single collection. - **Collection Group**– Reads documents across multiple collections with the same name. > If **Collection** or **Collection Group** is selected, specify the name in the **Collection / Group** field. - **Document**– Reads a single document. > If **Document** is selected, specify the **Collection** and **Document ID**. You can also enter them together in a single field using the format `collectionName/documentName`. 5. To sort or filter your data, check the option **"Do you want to sort and order your data?"**. Then configure the query constraints, such as: - `limitToFirst` or `limitToLast` – Limit the number of results returned. - `startAt` / `startAfter` – Start the query at a specific value. - `endAt` / `endBefore` – End the query at a specific value. - `orderBy` – Sort documents by a specific field. - `where` – Apply filters to select specific documents. 6. Connect a Debug node to the Firestore node to monitor the output and deploy the flow. ## Step 7: Listening for Real-time Changes Unlike the **Firestore Get** node, which retrieves data only once, the **Firestore In** node establishes a real-time listener. This means Node-RED will continuously receive updates whenever documents are **added**, **modified**, or **removed** in the specified collection, collection group, or document. This capability is particularly useful for building live dashboards, sending notifications, or keeping device states synchronized without repeatedly polling the database. 1. Drag a **Firestore In** node onto the Node-RED canvas. 2. Double-click the node to open its configuration panel. 3. Select the **Firestore configuration** you created in Step 2. 4. Choose the **Type**: - **Collection** – Listens to all documents within a single collection. - **Collection Group**– Listens to documents across multiple collections with the same name. > If **Collection** or **Collection Group** is selected, specify the name in the **Collection / Group** field. - **Document**– Listens to changes in a single document. > If **Document** is selected, specify the **Collection** and **Document ID**. You can also enter them together in a single field using the format `collectionName/documentName`. 5. When **Collection** or **Collection Group** is selected, choose the type of changes you want to listen for with the **filter**field: - **Added documents** - **Modified documents** - **Removed documents** 6. To refine your listener, enable **"Do you want to sort and order your data?"**and configure query constraints such as: - `limitToFirst` or `limitToLast` – Limit the number of results returned. - `startAt` / `startAfter` – Start the query at a specific value. - `endAt` / `endBefore` – End the query at a specific value. - `orderBy` – Sort documents by a specific field. - `where` – Apply filters to select specific documents. 7. Connect a Debug node to the Firestore node to monitor the output and deploy the flow. ## Example Flow The flow below demonstrates all the concepts covered in this guide. You can explore and modify it as needed. ::render-flow ```json [{"id":"57c1f30f8a825e5c","type":"group","z":"b5ce73e91740e4b2","name":"","style":{"label":true,"stroke":"#7fb7df"},"nodes":["5792767043952f56","15d852f4a29abec1","16c12e22e1f3b257","c4d08c57eec14060","f69b62a66076edf1"],"x":114,"y":279,"w":972,"h":122},{"id":"5792767043952f56","type":"inject","z":"b5ce73e91740e4b2","g":"57c1f30f8a825e5c","name":"Send Timestamp","props":[{"p":"payload.timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":240,"y":360,"wires":[["c4d08c57eec14060"]]},{"id":"15d852f4a29abec1","type":"debug","z":"b5ce73e91740e4b2","g":"57c1f30f8a825e5c","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":360,"wires":[]},{"id":"16c12e22e1f3b257","type":"comment","z":"b5ce73e91740e4b2","g":"57c1f30f8a825e5c","name":"Set Timestamp to \"timestampOverwritten\"","info":"","x":320,"y":320,"wires":[]},{"id":"c4d08c57eec14060","type":"firestore-out","z":"b5ce73e91740e4b2","g":"57c1f30f8a825e5c","name":"Overwrite Timestamp","database":"e8796a1869e179bc","collection":"demo","collectionType":"str","document":"timestampOverwritten","documentType":"str","queryMethod":"set","queryOptions":{"merge":false},"x":540,"y":360,"wires":[]},{"id":"f69b62a66076edf1","type":"firestore-in","z":"b5ce73e91740e4b2","g":"57c1f30f8a825e5c","name":"Timestamp Changes","database":"e8796a1869e179bc","collection":"","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"demo/timestampOverwritten","documentType":"str","filter":"none","inputs":0,"passThrough":false,"x":770,"y":360,"wires":[["15d852f4a29abec1"]]},{"id":"e8796a1869e179bc","type":"firebase-config","name":"My Database","authType":"email","claims":{},"createUser":false,"status":{"firestore":false,"storage":false},"useClaims":false},{"id":"8f193a9c1fc939fb","type":"group","z":"b5ce73e91740e4b2","name":"","style":{"stroke":"#c8e7a7","label":true},"nodes":["7376db537268899b","bd18e498f7c61507","19355c55dc280ad7","0ef7c0721cf81927","29aaf3383098e09e","735b562a594841f3","6a90881898ed3551","ee3a2b0bc367a47e","ca1a112e5c6cbdb2","9acbf29beeba99c3","ce937eb6b8c8ca65","16d258ae4b97ca34","78d3b0d5f0f884f4","cf5f66733f714098","fe80dd5b71c8eebe","a8a4da4c647877d1","4d5563941ff8ff6e","108e033753b35f5a","6e2869b197f278f5","d8baeef2707a77b5","1b5c69ac26a6eed7","c87c1434f562e22c","770a532dd82c2c5d","1addc1cfbb75e991"],"x":114,"y":439,"w":972,"h":582},{"id":"7376db537268899b","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Add Alan","props":[{"p":"payload"},{"p":"user","v":"alanisawesome","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"date_of_birth\":\"June 23, 1912\",\"full_name\":\"Alan Turing\",\"nickname\":\"Alan The Machine\"}","payloadType":"json","x":220,"y":520,"wires":[["a8a4da4c647877d1"]]},{"id":"bd18e498f7c61507","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Add Steve","props":[{"p":"payload"},{"p":"user","v":"steveisapple","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"full_name\":\"Steve Jobs\",\"nickname\":\"Steve The King\",\"hobby\":\"Computer\"}","payloadType":"json","x":220,"y":580,"wires":[["a8a4da4c647877d1"]]},{"id":"19355c55dc280ad7","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Modify Alan Nickname","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"nickname\":\"Alan is Genius\"}","payloadType":"json","x":260,"y":700,"wires":[["108e033753b35f5a"]]},{"id":"0ef7c0721cf81927","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Remove Steve","props":[{"p":"user","v":"steveisapple","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":240,"y":860,"wires":[["d8baeef2707a77b5"]]},{"id":"29aaf3383098e09e","type":"debug","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.changes","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":540,"wires":[]},{"id":"735b562a594841f3","type":"debug","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.changes","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":700,"wires":[]},{"id":"6a90881898ed3551","type":"debug","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.changes","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":860,"wires":[]},{"id":"ee3a2b0bc367a47e","type":"debug","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":980,"y":980,"wires":[]},{"id":"ca1a112e5c6cbdb2","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Get All Users","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":230,"y":980,"wires":[["770a532dd82c2c5d"]]},{"id":"9acbf29beeba99c3","type":"debug","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"debug 7","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.docs","targetType":"msg","statusVal":"","statusType":"auto","x":560,"y":980,"wires":[]},{"id":"ce937eb6b8c8ca65","type":"comment","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Add Alan to \"users\"","info":"","x":250,"y":480,"wires":[]},{"id":"16d258ae4b97ca34","type":"comment","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Modify the Alan's Nickname","info":"","x":280,"y":660,"wires":[]},{"id":"78d3b0d5f0f884f4","type":"comment","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Remove Steve from \"users\"","info":"","x":280,"y":820,"wires":[]},{"id":"cf5f66733f714098","type":"comment","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Get All Users from \"users\"","info":"","x":270,"y":940,"wires":[]},{"id":"fe80dd5b71c8eebe","type":"comment","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Print All Users Changes","info":"","x":780,"y":940,"wires":[]},{"id":"a8a4da4c647877d1","type":"firestore-out","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Add User","database":"e8796a1869e179bc","collection":"users","collectionType":"str","document":"user","documentType":"msg","queryMethod":"set","queryOptions":{"merge":false},"x":400,"y":540,"wires":[]},{"id":"4d5563941ff8ff6e","type":"firestore-in","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"User added","database":"e8796a1869e179bc","collection":"users","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"","documentType":"str","filter":"added","inputs":0,"passThrough":false,"x":750,"y":540,"wires":[["29aaf3383098e09e"]]},{"id":"108e033753b35f5a","type":"firestore-out","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Update User Nickname","database":"e8796a1869e179bc","collection":"users","collectionType":"str","document":"alanisawesome","documentType":"str","queryMethod":"update","queryOptions":{"merge":true},"x":530,"y":700,"wires":[]},{"id":"6e2869b197f278f5","type":"firestore-in","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"User Modified","database":"e8796a1869e179bc","collection":"users","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"","documentType":"str","filter":"modified","inputs":0,"passThrough":false,"x":750,"y":700,"wires":[["735b562a594841f3"]]},{"id":"d8baeef2707a77b5","type":"firestore-out","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Remove User","database":"e8796a1869e179bc","collection":"users","collectionType":"str","document":"user","documentType":"msg","queryMethod":"delete","queryOptions":{"merge":false},"x":440,"y":860,"wires":[]},{"id":"1b5c69ac26a6eed7","type":"firestore-in","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"User Removed","database":"e8796a1869e179bc","collection":"users","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"","documentType":"str","filter":"removed","inputs":0,"passThrough":false,"x":760,"y":860,"wires":[["6a90881898ed3551"]]},{"id":"c87c1434f562e22c","type":"firestore-in","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"All Users Changes","database":"e8796a1869e179bc","collection":"users","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"","documentType":"str","filter":"none","inputs":0,"passThrough":false,"x":770,"y":980,"wires":[["ee3a2b0bc367a47e"]]},{"id":"770a532dd82c2c5d","type":"firestore-get","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Get Users","database":"e8796a1869e179bc","collection":"users","collectionType":"str","collectionGroup":"","collectionGroupType":"str","constraints":{},"document":"","documentType":"str","passThrough":false,"x":400,"y":980,"wires":[["9acbf29beeba99c3"]]},{"id":"1addc1cfbb75e991","type":"inject","z":"b5ce73e91740e4b2","g":"8f193a9c1fc939fb","name":"Remove Alan Nickname","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"nickname\":\"DELETE\"}","payloadType":"json","x":260,"y":740,"wires":[["108e033753b35f5a"]]},{"id":"3b1dbdd5845f591a","type":"global-config","env":[],"modules":{"@gogovega/node-red-contrib-cloud-firestore":"0.2.0"}}] ``` :: ::div --- style: "border: 2px solid #7fb7df; padding: 20px; border-radius: 10px; margin-top: 40px; background-color: #f5faff;" --- ### Try FlowFuse's Built-In Database Service [FlowFuse now includes a fully integrated database service that makes connecting and querying your data effortless](https://flowfuse.com/blog/2025/08/getting-started-with-flowfuse-tables/). With the FlowFuse Query Node, you do not need to configure the connection manually, the node sets itself up automatically. Even better, the [FlowFuse Expert allows you to query your tables using natural language](https://flowfuse.com/blog/2025/09/ai-assistant-flowfuse-tables/). Simply type your request, and it will generate the correct SQL for you based on your table. Deploy, manage, scale, and secure your Node-RED applications with FlowFuse, and take full control of your industrial workflows and data. [**Start with FlowFuse today**](https://app.flowfuse.com/){rel=""nofollow""} :: # Node-RED Database Integration Guides Node-RED is highly versatile and can be set up to work with a variety of databases, whether it is SQL (e.g., PostgreSQL, MySQL), NoSQL (e.g., MongoDB), or time-series databases (e.g., InfluxDB). This flexibility allows you to store and manage IoT data effectively, enabling the creation of interactive and data-driven applications for IoT environments. ## Resources Here are some resources to help you get started with Node-RED on diffrent types of databases. Each guide provides step-by-step instructions to help you get started, along with advanced techniques for optimizing performance and handling complex data operations.: - [Using DynamoDB with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/dynamodb/): Get started with AWS' NoSQL database DynamoDB with Node-RED - [Using Firebase with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/firebase/): Learn how to integrate Cloud Firestore with Node-RED to build real-time event-driven applications. This guide covers Firestore setup, reading, writing, and listening to data using Node-RED. - [Using InfluxDB with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/influxdb/): Node-RED has great support for InfluxDB. In this guide, we'll explain how to get your data flowing into one of the most popular time-series databases. - [Using MongoDB With Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/mongodb/): Learn how to seamlessly integrate MongoDB, a NoSQL database, into your Node-RED applications with this step-by-step documentation. - [Using MySQL with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/mysql/): Learn how to seamlessly integrate MySQL with Node-RED for efficient data management and application development. - [Using PostgreSQL with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/postgresql/): Learn how to seamlessly integrate PostgreSQL with Node-RED for efficient data management and application development. - [Using Redis with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/redis/): Learn how to integrate Redis with Node-RED for fast data storage, pub/sub messaging, JSON handling, Lua scripting, and advanced Redis operations in Node-RED flows. - [Using SQLite with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/sqlite/): Learn how to seamlessly integrate SQLite with Node-RED for efficient data management and application development. - [Using TimescaleDB with Node-RED (2026 Updated)](https://flowfuse.com/docs/node-red/database/timescaledb/): Learn how to integrate TimescaleDB with Node-RED for storing and managing time-series data efficiently. ::callout{icon="i-lucide-badge-check"} **A certified node for these databases.** The guides in this section use community packages. One database here is also covered by a FlowFuse certified node, maintained for production use: [redis](https://flowfuse.com/integrations/?certified=1) :: # Using InfluxDB with Node-RED (2026 Updated) InfluxDB is a time series database that is commonly used for storing and analysing IoT data. Node-RED is a visual programming tool that makes it easy to connect different data sources and create flows that automate tasks. In this documentation, we will show you how to write data to InfluxDB from a Node-RED flow. We will also provide you with a few tips for writing data to InfluxDB effectively. ::cta-image --- alt: Write to InfluxDB from every Node-RED instance you run, all from one place cta: sign-up reference: "Node-RED: Using InfluxDB with Node-RED" src: https://flowfuse.com/docs/node-red/database/images/influxdb-node-red-cta-1.png --- :: ## Step 1: Install the InfluxDB Node-RED package The first step is to install the InfluxDB Node-RED package. You can do this by opening the Node-RED editor and clicking on the Manage Palette button. In the search bar, type InfluxDB and select the package called node-red-contrib-influxdb. ## Step 2: Configure the InfluxDB node Once you have installed the InfluxDB node, you need to configure it. Drag an instance of 'influxdb out' onto your canvas and select 'Add new influxdb'. Follow the steps below to configure your connection. - Version: The version of InfluxDB you are using (we're using 2.0). - URL: The URL of your InfluxDB server. - Token: Your token to access your InfluxDB database. ![configuring the influxdb node step 1](https://flowfuse.com/docs/node-red/database/images/config-connection.png "configuring the influxdb node step 1") We can now configure the database. - Organization name. - Bucket (database) name. - Measurement (table) name. ![configuring the influxdb node step 2](https://flowfuse.com/docs/node-red/database/images/config-database.png "configuring the influxdb node step 2") ## Step 3: Create a data point A data point is a single piece of data that is written to InfluxDB. A data point consists of a measurement, a set of fields, and a set of tags. The measurement is the name of the data that you are writing. We've set it in the configuration of the InfluxDB above so we don't need to pass it in with each payload. The fields are the individual pieces of data that you are writing. The tags are used to categorise the data. You can import the flow below into Node-RED to see an example of a payload which will write all the required values to create a data point in InfluxDB: ::render-flow ```json [{"id":"cb3b0ecc762dbf93","type":"inject","z":"4542482476b9c71d","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"time\":1688718546,\"temperature\":24},{\"device\":\"dQBgXeWLRE\",\"deviceType\":\"Pi4\",\"deviceName\":\"demo-pi-rob\"}]","payloadType":"json","x":450,"y":420,"wires":[["87166c0dafdeea33"]]},{"id":"87166c0dafdeea33","type":"debug","z":"4542482476b9c71d","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":600,"y":420,"wires":[]}] ``` :: In this example, the time & temperature fields are hard coded, you will need to overwrite the values stored in `payload[0].time` & `payload[0].temperature` with real data if you were to connect this flow to a real IOT thermometer. ## Step 4: Write the data point to InfluxDB Once you have created a data point, you can write it to InfluxDB by using the InfluxDB node. - Data: The data point that you want to write. - Options: The configuration options for the InfluxDB node. This is an example valid payload: ```json [ { "time": 1688987984, "temperature": 24 }, { "device": "dQBgXeWLRE", "deviceType": "Pi4", "deviceName": "demo-pi-rob" } ] ``` You can import a demo, including the demo payload flow using the code below: ::render-flow ```json [{"id":"ecbb02face30cbcd","type":"influxdb out","z":"4542482476b9c71d","influxdb":"1c1a5edef41716e3","name":"InfluxDB","measurement":"temperature","precision":"","retentionPolicy":"","database":"database","precisionV18FluxV20":"s","retentionPolicyV18Flux":"","org":"organization","bucket":"my_data","x":360,"y":220,"wires":[]},{"id":"de83c2b49ba249fd","type":"inject","z":"4542482476b9c71d","name":"","props":[{"p":"measurement","v":"temperature","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"time\":1688987984,\"temperature\":24},{\"device\":\"dQBgXeWLRE\",\"deviceType\":\"Pi4\",\"deviceName\":\"demo-pi-rob\"}]","payloadType":"json","x":190,"y":160,"wires":[["aad6353f2f00333e","ecbb02face30cbcd"]]},{"id":"aad6353f2f00333e","type":"debug","z":"4542482476b9c71d","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":360,"y":160,"wires":[]},{"id":"1c1a5edef41716e3","type":"influxdb","hostname":"127.0.0.1","port":"8086","protocol":"http","database":"my_data","name":"","usetls":false,"tls":"","influxdbVersion":"2.0","url":"https://localhost","rejectUnauthorized":true}] ``` :: Bear in mind that you will need to edit the server and database details in your influxdb node for this demo to work. ## Step 5: Test your flow You should now be ready to test your flow is writing data to InfluxDB correctly. There is no output in Node-RED to confirm you data was written, so you will need to check directly on InfluxDB. :video{ariaLabel="Checking the data has arrived in InfluxDB" autoPlay="true" height="654" loop="true" muted="true" playsInline="true" preload="none" width="904"} Great, our data has arrived correctly and is ready to be used. ## 5 Tips for writing data to Node-RED from InfluxDB effectively 1. Choose the correct InfluxDB node. There are two InfluxDB nodes available in Node-RED: the 'influxdb out' node and the 'influx batch' node. The influxdb out node writes data to InfluxDB one point at a time, while the influx batch node writes data to InfluxDB in batches. The best node to use depends on the amount of data you are writing and the performance requirements of your application. If you are just getting started with InfluxDB, we suggest starting with influxdb out. 2. Set the correct measurement name. The measurement name is the name of the table in InfluxDB where the data will be stored. It is important to choose a meaningful measurement name that will help you to easily identify the data later. 3. Set the correct tags and fields. Tags are used to identify the data points, while fields are used to store the actual data values. It is important to set the correct tags and fields for your data so that you can easily query and analyse it later. 4. Set the correct timestamp. The timestamp is the time at which the data point was recorded. It is important to set the correct timestamp so that you can track the evolution of your data over time. 5. Use the correct precision. The precision is the number of decimal places that are stored for each data value. It is important to use the correct precision so that your data is easy to use. # Using MongoDB With Node-RED (2026 Updated) This guide provides implementation procedures for integrating MongoDB with Node-RED. It covers configuration requirements, operational patterns, and a complete implementation example using a customer relationship management system. ## Understanding MongoDB MongoDB is an open-source NoSQL database that stores data in flexible, JSON-like documents rather than rigid table structures. Each document can maintain its own schema, which allows for data model evolution without requiring database-wide migrations. This architectural approach suits applications where data structures change frequently or vary between records. The database uses a distributed architecture that supports horizontal scaling across multiple nodes. It handles high-volume workloads and provides query performance suitable for real-time operations. ### Data Organization MongoDB structures data into three hierarchical components, which differ from traditional relational databases: - **Collections** replace tables - **Documents** replace rows - **Fields** replace columns !["Annotomy of MongoDB document"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-annotomy-of-mongodb-document.png "Annotomy of MongoDB document"){dataZoomable=""} ## Technical Considerations Several factors make MongoDB appropriate for Node-RED integration: MongoDB's document model aligns directly with JSON data structures, which eliminates transformation overhead between Node-RED message objects and database records. The schema-less architecture supports rapid iteration without requiring schema migrations for each data model change. The database scales horizontally by distributing data across multiple servers. While some SQL databases support horizontal scaling, MongoDB's architecture implements this pattern natively. The query language uses a document-based syntax that matches common programming patterns. MongoDB Atlas provides managed database services with automated backups, security controls, and monitoring tools. The platform includes specialized implementations for industrial applications. Additional information is available at [MongoDB Atlas for Manufacturing and Automotive](https://www.mongodb.com/company/newsroom/press-releases/mongodb-atlas-for-manufacturing-and-automotive){rel=""nofollow""}. ## Configuration Procedures ### Installing the MongoDB Node 1. Open Node-RED Settings (top-right menu) 2. Select "Manage Palette" 3. Navigate to the "Install" tab 4. Search for `node-red-contrib-mongodb4` 5. Install the package ### Connection Parameters Collect the following configuration values before proceeding: - `Host`: Server IP address or hostname - `Port`: Connection port (default: 27017; may not be required for managed services) - `Database`: Target database name - `User`: Account username with appropriate database privileges - `Password`: Account password For TLS/SSL configuration and other advanced options, consult the [node documentation](https://flows.nodered.org/node/node-red-contrib-mongodb4){rel=""nofollow""}. ### Environment Variable Configuration Store connection credentials in environment variables rather than embedding them directly in flows. This prevents credential exposure in version control and exported flow definitions. Reference the guide [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/) for additional context. !["Screenshot displaying FlowFuse instance settings"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-flowfuse-instance-setting.png "Screenshot displaying FlowFuse instance settings"){dataZoomable=""} 1. Navigate to instance settings 2. Select the "Environment" tab 3. Add variables for each configuration parameter 4. Save the configuration 5. Restart the instance using the Actions menu ### Configuring the MongoDB Node Configure the node to use the environment variables: 1. Add a MongoDB4 node to the canvas 2. Open the node configuration 3. Click the edit icon next to the connection field 4. Reference environment variables as shown below !["Screenshot displaying connection configuration of MongoDB 4 node."](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-node-connection-configuration.png "Screenshot displaying connection configuration of MongoDB 4 node."){dataZoomable=""} ## Implementation Example: Customer Management System This section demonstrates MongoDB operations through a functional customer relationship management system. The implementation covers create, read, update, and delete operations using a representative data structure. ### Data Schema The customer records use the following structure: ```json { "_id": "NXaxeFEK", "firstname": "alice", "lastname": "demo", "email": "userdemo601@gmail.com", "phone": "+19876543561", "company": "self", "status": "Prospect", "source": "website" } ``` ### MongoDB Operations The implementation uses five core operations: - **InsertOne**: Adds a single document to a collection - **Find**: Retrieves documents matching specified criteria - **UpdateOne**: Modifies a single document based on query parameters - **DeleteOne**: Removes a single document matching query criteria - **Drop**: Deletes an entire collection Refer to the [MongoDB CRUD documentation](https://www.mongodb.com/basics/crud){rel=""nofollow""} for the complete operation set. ### Additional Dependencies #### NanoID Generator Install `node-red-contrib-friendly-id` through the palette manager. This package generates compact, URL-safe unique identifiers for customer records. The implementation uses NanoID instead of manual ID entry or sequential numbering. #### Dashboard Interface The example uses Node-RED Dashboard 2.0 for the user interface. Follow the [Dashboard setup instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to install and configure the dashboard nodes. ### Creating Customer Records 1. Add a ui-form widget to the canvas 2. Configure form elements for `firstname`, `lastname`, `email`, `phone`, `company`, `status`, and `source` !["Screenshot displaying form widget configuration to insert data in MongoDB"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-insert-data-form.png "Screenshot displaying form widget configuration to insert data in MongoDB"){dataZoomable=""} 3. Add a friendly-id node 4. Configure it to generate a random ID with your preferred length 5. Set output destination to `msg.payload._id` !["Screenshot displaying friend-id node configuration"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-friend-id-node.png "Screenshot displaying friend-id node configuration"){dataZoomable=""} 6. Add a change node 7. Set `msg.payload` to `[msg.payload]` using JSONata expression type 8. This wraps the payload in an array as required by the insertOne operation !["Screenshot displaying change node setting payload containing data that needs to be inserted in the database."](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-change-node-to-insert-data.png "Screenshot displaying change node setting payload containing data that needs to be inserted in the database."){dataZoomable=""} 9. Configure the MongoDB4 node: - Select the previously configured connection - Set collection name to "customers" - Set operation to "insertOne" !["Screenshot displaying configuration of MongoDB 4 node for inserting data"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-insertone-node-configuration.png "Screenshot displaying configuration of MongoDB 4 node for inserting data"){dataZoomable=""} 10. Wire the nodes as shown: !["Screenshot displaying connections of wires in the 'Insert Data into Database' flow"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-insertone-flow.png "Screenshot displaying connections of wires in the 'Insert Data into Database' flow"){dataZoomable=""} ### Retrieving Customer Records 1. Add an inject node 2. Configure it to send an empty object `{}` 3. Set the node to repeat at your preferred interval for automatic table updates 4. Add a MongoDB4 node 5. Select your connection and set operation to "find" !["Screenshot displaying configuration of MongoDB 4 node for retrieving data"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-find-node-configuration.png "Screenshot displaying configuration of MongoDB 4 node for retrieving data"){dataZoomable=""} 6. Add a ui-table widget 7. Configure the maximum rows according to your requirements !["Screenshot displaying ui-table widget configuration"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-table-widget.png "Screenshot displaying ui-table widget configuration"){dataZoomable=""} 8. Wire the nodes as shown: !["Screenshot displaying connections of wires in the 'Retrive Data from Database' flow"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-find-flow.png "Annotomy of MongoDB document"){dataZoomable=""} ### Updating Customer Records 1. Add a ui-form widget with fields for "id" and "status" !["Screenshot displaying form widget configuration to update data in MongoDB"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-insert-data-form.png "Screenshot displaying form widget configuration to update data in MongoDB"){dataZoomable=""} 2. Add a change node 3. Set `msg.payload` to the following JSON structure: ```json [ { "_id": msg.payload._id }, { "$set": { "status": msg.payload.status } } ] ``` The first object specifies the query criteria (which document to update). The second object defines the update operation using MongoDB's `$set` operator. !["Screenshot displaying the change node setting payload as an array containing a query and operation to perform an update operation in the database"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-change-node-to-update-data.png "Screenshot displaying the change node setting payload as an array containing a query and operation to perform an update operation in the database"){dataZoomable=""} 4. Add a MongoDB4 node and set operation to "updateOne" !["Screenshot displaying configuration of MongoDB 4 node for updating data"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-update-node-configuration.png "Screenshot displaying configuration of MongoDB 4 node for updating data"){dataZoomable=""} 5. Wire the nodes as shown: !["Screenshot displaying connections of wires in the 'Update Data from Database' flow"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-updateone-flow.png "Screenshot displaying connections of wires in the 'Update Data from Database' flow"){dataZoomable=""} ### Deleting Customer Records 1. Add a ui-form widget with fields for "id" and "firstname" !["Screenshot displaying form widget configuration to delete data in MongoDB"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-delete-data-form.png "Screenshot displaying form widget configuration to delete data in MongoDB"){dataZoomable=""} 2. Add a change node 3. Set `msg.payload` to the following structure: ```json [ { "_id": msg.payload._id, "firstname": msg.payload.firstname }, { "$delete": "" } ] ``` The query includes both ID and firstname to provide additional verification before deletion. !["Screenshot displaying the change node setting payload as an array containing a query and operation to perform an delete operation in the database"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-change-node-to-delete-data.png "Screenshot displaying the change node setting payload as an array containing a query and operation to perform an delete operation in the database"){dataZoomable=""} 4. Add a MongoDB4 node and set operation to "deleteOne" !["Screenshot displaying configuration of MongoDB 4 node for deleting data"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-update-node-configuration.png "Screenshot displaying configuration of MongoDB 4 node for deleting data"){dataZoomable=""} 5. Wire the nodes as shown: !["Screenshot displaying connections of wires in the 'Delete Data from Database' flow"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-updateone-flow.png "Screenshot displaying connections of wires in the 'Delete Data from Database' flow"){dataZoomable=""} ### Removing Collections 1. Add an inject node configured to send an empty object 2. Add a MongoDB4 node 3. Specify the collection name to remove 4. Set operation to "drop" !["Screenshot displaying configuration of MongoDB4 node for droping collection from database"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-drop-node-configuration.png "Screenshot displaying configuration of MongoDB4 node for droping collection from database"){dataZoomable=""} 5. Wire the nodes as shown: !["Screenshot displaying connections of wires in the 'Drop collecton from Database' flow"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-mongodb-drop-flow.png "Screenshot displaying connections of wires in the 'Drop collecton from Database' flow"){dataZoomable=""} ## Verification and Troubleshooting Connect a debug node to the output of any MongoDB4 node to monitor operation results and diagnose errors. The following examples show successful operation responses: ### Operation Response Messages ```js // Insert operation { "acknowledged": true, "insertedId": "BKoIzMuW" } // Update operation { "acknowledged": true, "modifiedCount": 1, "upsertedId": null, "upsertedCount": 0, "matchedCount": 1 } // Delete operation { "acknowledged": true, "deletedCount": 1 } // Drop operation returns boolean true ``` ### Deployment !["Screenshot displaying flow of CRM System"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-crm-system-node-red-flow.png "Screenshot displaying flow of CRM System"){dataZoomable=""} 1. Deploy the flow using the Deploy button 2. Open the dashboard using the button in the Dashboard 2.0 sidebar 3. Use inject nodes for retrieval and drop operations 4. Use the forms for create, update, and delete operations !["Screenshot displaying dashboard view of CRM System"](https://flowfuse.com/docs/node-red/database/images/using-mongodb-with-node-red-crm-system-node-red-dashboard-view.png "Screenshot displaying dashboard view of CRM System"){dataZoomable=""} # Using MySQL with Node-RED (2026 Updated) When discussing popular and widely used databases, MySQL inevitably stands out. This is especially evident within the Node-RED community, where the MySQL contrib node has the highest number of downloads among all database contrib nodes. However, popularity often brings its own set of challenges. We've prepared this comprehensive guide to help our Node-RED community members navigate these challenges. It covers all aspects of using MySQL with Node-RED, including an overview of what MySQL is, the differences between PostgreSQL and MySQL, when to choose one over the other, essential MySQL operations, and more. ## What is MySQL [MySQL](https://dev.mysql.com/doc/){rel=""nofollow""} is an open-source relational database management system (RDBMS) developed by MySQL AB, which Sun Microsystems later acquired and then Oracle Corporation. It uses SQL (Structured Query Language) to query and manage databases. MySQL is widely recognized for its performance, scalability, and ease of use. ## MySQL vs PostgreSQL | Feature | PostgreSQL | MySQL | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | **ACID Compliance** | Ensures all changes to data are reliable and consistent, even during unexpected events like system crashes. | Ensures data integrity but requires InnoDB or NDB Cluster for full ACID compliance. | | **Concurrency Control** | Handles multiple users updating data simultaneously without conflicting changes, using MVCC to manage versions of data. | Manages simultaneous data access differently across storage engines like InnoDB and MyISAM. | | **Indexes** | Structures data for quick retrieval; supports various index types like B-tree and hash for efficient queries. | Uses B-tree and R-tree indexes, suitable for different data structures and retrieval needs. | | **Data Types** | Supports complex data types like arrays and JSON, useful for applications needing flexible data handling. | Focuses on traditional relational data types (e.g., integers, strings) with less support for complex data structures. | | **Views** | Materialized views store query results, improving performance for complex queries that require frequent data summarization. | Standard views are available for simplifying query execution but lack advanced performance optimization. | | **Stored Procedures** | Allows defining custom functions in multiple programming languages, enhancing database functionality beyond SQL queries. | Supports SQL-based procedures for automating tasks like data validation and complex logic execution. | | **Triggers** | Triggers execute actions automatically when specific events occur (e.g., before or after data insertion or updates), enhancing database automation. | Offers triggers to automate tasks based on events, though functionality varies by storage engine and configuration. | ## Choosing Between PostgreSQL and MySQL Deciding between [PostgreSQL](https://flowfuse.com/docs/node-red/database/postgresql/) and MySQL depends on your project's needs and what each database system does best. PostgreSQL is ideal for big projects requiring complex data handling, reliability, and often updated data. It works well in environments where keeping data consistent is crucial. PostgreSQL's advanced features, like materialized views and support for writing procedures in different languages beyond SQL make it great for managing sophisticated data needs. On the other hand, MySQL is excellent for projects that prioritize fast data reading and are easy to set up and use. It's commonly used for smaller projects, quick prototypes, or applications where quick deployment is critical. MySQL offers flexibility with different storage options, like InnoDB for transactions and MyISAM for handling lots of reads simultaneously, making it versatile depending on your workload. Knowing what performance your project needs, how familiar your team is with each database, and how much your project might grow will help you pick the database that's best for you. ## Using MySQL with Node-RED This section of the article will cover how to configure MySQL with Node-RED, create and delete tables, and perform essential operations such as inserting, retrieving, updating, and deleting data. These operations are crucial for any application. Additionally, for the demonstration purpose, the article uses a simple weather data example, so make sure you update the SQL queries according to your data and application needs. Also, through the article, we have used the inject nodes for ease to set the example data and trigger, but instead, you could utilize the Node-RED Dashboard 2.0 to grab the data from the user and trigger it. ### Prerequisite Before proceeding further, ensure the following: - A running MySQL database instance, whether hosted in the cloud or locally, along with connection details, should be ready, and environment variables for those connection details should be added. For more information on how to add environment variables, refer to [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). - The MySQL custom node [node-red-contrib-mysql](https://flows.nodered.org/node/node-red-node-mysql){rel=""nofollow""} is installed in your Node-RED environment. ### Configuring MySQL Custom Node 1. Drag the MySQL node onto the canvas. 2. Double-click on it, then Click on the "+" button located next to the "Database" field. 3. Enter the Environment variables added for Host, Port, User, Password, and Database into their corresponding fields. 4. Keep the Charset to default as it is set to "UTF8" which is widely compatible and supports various languages and characters. 5. Click on "Add" to save the configuration. !["Screenshot of MySQL node property dialog with the environment variables added"](https://flowfuse.com/docs/node-red/database/images/mysql-node-config.png "Screenshot of MySQL node property dialog with the environment variables added") To check whether your configuration is correct and Node-RED can connect, deploy the flow by clicking on the top-right "Deploy" button. If the connection is successful, each MySQL node will show a green dot with "connected" text underneath. ### Creating Table in MySQL Database 1. Drag an Inject node on the canvas. 2. Drag a Template node onto the canvas and set its property to `msg.topic`. 3. Insert the following SQL into the Template node: ```sql -- Create a table to store weather data if it doesn't already exist CREATE TABLE IF NOT EXISTS weather_data ( id INT AUTO_INCREMENT PRIMARY KEY, -- Primary key with auto-increment location VARCHAR(100) NOT NULL, -- Location name, cannot be null date DATETIME NOT NULL, -- Date and time of the recorded data, cannot be null tem DECIMAL(5, 2) NOT NULL -- Temperature with precision 5, scale 2, cannot be null ); ``` 3. Connect the output of the Inject node to the input of the Template node and the output of the Template node to the input of the MySQL node. The MySQL node allows sending queries through the `msg.topic` property. We use the Template node because it enables us to use Mustache syntax, which is useful for setting things dynamically. This flexibility is not crucial when creating or deleting tables but is essential for operations like inserts. ### Inserting Data in the MySQL Database Table 1. Drag an Inject node onto the canvas, and set `msg.payload` to the data you want to insert. 2. Drag a Template node onto the canvas. Insert the following SQL into it. Note how the Template node dynamically inserts the value of `msg.payload` into the query. If you want to set the date and time using Node-RED, you can similarly set it up as you did for `msg.payload`, currently, we are using the MySQL function for setting time. ```sql -- Insert a record into the weather_data table INSERT INTO weather_data (location, date, tem) VALUES -- Insert the location as 'New York' ('New York', -- Insert the current time as the date CURTIME(), -- Insert the temperature value from the payload {{payload}}); ``` 3. Drag another MySQL node onto the canvas, and ensure that it is using the correct configuration by double-clicking on it. While you can use a single MySQL node for all operations, However separating and organizing nodes can aid in better management, understanding, and debugging if issues occur. 4. Connect the output of the Inject node to the input of the Template node, and connect the output of the Template node to the input of the MySQL node. ### Retrieving Data from the MySQL Database Table 1. Drag an Inject node onto the canvas. This node will trigger the retrieval process. 2. Drag a Template node onto the canvas. Insert the following SQL query into it to retrieve all records from the `weather_data` table. ```sql -- Retrieve all records from the weather_data table SELECT * -- Select all columns from the table FROM weather_data; -- Specify the table from which to retrieve the records ``` You can modify the query as needed to filter or sort the data. An example SQL query is provided to demonstrate this ```sql -- Retrieve records where the temperature is greater than 25 and sort by date in descending order SELECT * -- Select all columns from the table FROM weather_data -- Specify the table from which to retrieve the records WHERE tem > 25 -- Filter records to include only those where the temperature is greater than 25 ORDER BY date DESC; -- Sort the results by date in descending order (most recent first) ``` 4. Drag a MySQL node onto the canvas. 5. Connect the output of the Inject node to the input of the Template node, and connect the output of the Template node to the input of the MySQL node. ### Updating Data of the MySQL Database Table 1. Drag an Inject node onto the canvas and set the message.payload to the data you want to update 2. Drag a Template node onto the canvas. Insert the following SQL into it. In this SQL query, we are setting the `id` statically, which we are using to update the data, but you can also dynamically set it just like we did for `tem`. We have utilized the `WHERE` clause here, but there are plenty of other SQL clauses available for more complex operations. For more information, refer to this blog on \[SQL Clauses ({rel=""nofollow""}) ```sql -- Update the temperature for a specific record in the weather_data table UPDATE weather_data -- Specify the table to update SET tem = {{payload}} -- Set the temperature value to the value from the payload WHERE id = 3; -- Update only the record where the id is 3 ``` 3. Drag a MySQL node onto the canvas. 4. Connect the output of the Inject node to the input of the Template node, and connect the output of the Template node to the input of the MySQL node. ### Deleting Data from the MySQL Database Table 1. Drag an Inject node onto the canvas. 2. Drag a Template node onto the canvas. ```sql DELETE FROM weather_data -- Specify the table from which to delete records WHERE tem < 15 -- Filter records to include only those where the temperature is less than 15 AND location = 'New York'; -- Further filter records to include only those where the location is New York ``` 3. Drag a MySQL node onto the canvas. 4. Connect the output of the Inject node to the input of the Template node, and connect the output of the Template node to the input of the MySQL node. ### Deleting MySQL Database Table 1. Drag an Inject node onto the canvas. 2. Drag a Template node onto the canvas. Insert the following SQL into it: ```sql -- Drop the weather_data table if it exists DROP TABLE IF EXISTS weather_data; -- Remove the table named weather_data from the database if it already exists ``` 3. Drag a MySQL node onto the canvas. 4. Connect the output of the Inject node to the input of the Template node, and connect the output of the Template node to the input of the MySQL node. Below is the complete flow covering all the operations discussed throughout this blog. ::render-flow ```json [{"id":"01aeec8769243693","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":1660,"wires":[["e9ae7f0a3fdbfbc5"]]},{"id":"e9ae7f0a3fdbfbc5","type":"debug","z":"a9e5683585deb91e","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1160,"y":1660,"wires":[]},{"id":"faf229c38d675b2b","type":"inject","z":"a9e5683585deb91e","name":"Create the table","props":[],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":600,"y":1660,"wires":[["b407dbb1901749bf"]]},{"id":"b407dbb1901749bf","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"CREATE TABLE IF NOT EXISTS weather_data (\n id INT AUTO_INCREMENT PRIMARY KEY,\n location VARCHAR(100) NOT NULL,\n date DATETIME NOT NULL,\n tem DECIMAL(5, 2) NOT NULL\n);\n","output":"str","x":820,"y":1660,"wires":[["01aeec8769243693"]]},{"id":"cfc26503f0812a88","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":1760,"wires":[["236d40b1edb920d2"]]},{"id":"236d40b1edb920d2","type":"debug","z":"a9e5683585deb91e","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1160,"y":1760,"wires":[]},{"id":"25790e7a3aef22bf","type":"inject","z":"a9e5683585deb91e","name":"Insert the data into the table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"$random() * 100","payloadType":"jsonata","x":560,"y":1760,"wires":[["7a798b3539da9547"]]},{"id":"7a798b3539da9547","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"INSERT INTO weather_data (location, date, tem)\nVALUES ('New York', CURTIME(), {{ payload }});","output":"str","x":820,"y":1760,"wires":[["cfc26503f0812a88"]]},{"id":"0bbb4d17defe3067","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":1860,"wires":[["ef2de8cb0673de47"]]},{"id":"ef2de8cb0673de47","type":"debug","z":"a9e5683585deb91e","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":1860,"wires":[]},{"id":"027c2c3801bf71a3","type":"inject","z":"a9e5683585deb91e","name":"Retrieve data from table","props":[],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":580,"y":1860,"wires":[["079031046156b6a0"]]},{"id":"079031046156b6a0","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"SELECT * FROM weather_data;\n","output":"str","x":820,"y":1860,"wires":[["0bbb4d17defe3067"]]},{"id":"eb38532de73213cf","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":2080,"wires":[["281838d687cea172"]]},{"id":"281838d687cea172","type":"debug","z":"a9e5683585deb91e","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":2080,"wires":[]},{"id":"d4dfbbac635e8a3f","type":"inject","z":"a9e5683585deb91e","name":"Drop the table if it exist","props":[],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":580,"y":2080,"wires":[["4442ad0bbc50a256"]]},{"id":"4442ad0bbc50a256","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"DROP TABLE IF EXISTS weather_data;\n","output":"str","x":820,"y":2080,"wires":[["eb38532de73213cf"]]},{"id":"668ed2021704c543","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":1940,"wires":[["666152e3362a25d5"]]},{"id":"666152e3362a25d5","type":"debug","z":"a9e5683585deb91e","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":1940,"wires":[]},{"id":"fbcbad8039d05e8e","type":"inject","z":"a9e5683585deb91e","name":"Delete data from the table","props":[],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":570,"y":1940,"wires":[["aa929706ed813892"]]},{"id":"aa929706ed813892","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"DELETE FROM weather_data\nWHERE tem > 15\nAND location = 'New York';","output":"str","x":820,"y":1940,"wires":[["668ed2021704c543"]]},{"id":"9c2befca464db660","type":"mysql","z":"a9e5683585deb91e","mydb":"bceed1e54606b872","name":"MySQL","x":980,"y":2020,"wires":[["d28cb3472c5fc357"]]},{"id":"d28cb3472c5fc357","type":"debug","z":"a9e5683585deb91e","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":2020,"wires":[]},{"id":"49297922b9784f8e","type":"inject","z":"a9e5683585deb91e","name":"Update data from the table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"10","payloadType":"num","x":570,"y":2020,"wires":[["bbae98e54824b222"]]},{"id":"bbae98e54824b222","type":"template","z":"a9e5683585deb91e","name":"","field":"topic","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"UPDATE weather_data\nSET tem = {{payload}}\nWHERE id = 3","output":"str","x":820,"y":2020,"wires":[["9c2befca464db660"]]},{"id":"bceed1e54606b872","type":"MySQLdatabase","name":"Mysql config","host":"${HOST}","port":"${PORT}","db":"${DATABASE}","tz":"+5:30","charset":"UTF8"}] ``` :: ### Deploying the Flow 1. To test the imported flows, you need to deploy them. To do that, click the deploy button at the top right corner. After deploying the flow, you can test each operation, such as creating, deleting, updating, and executing other queries, by clicking the inject button. For debugging purposes, add debug nodes to the flow. Additionally, if you want to explore the integration of other databases with Node-RED, you can refer to our [database section](https://flowfuse.com/docs/node-red/database/) in the Node-RED learning resources, where we cover databases such as PostgreSQL, MongoDB, InfluxDB, DynamoDB, and more. # Using PostgreSQL with Node-RED (2026 Updated) PostgreSQL is a highly reliable open-source relational database known for its extensive features. It supports diverse data types, robust SQL, and ACID compliance, allowing high-performance systems. Over the years, it has demonstrated reliability, security, and compatibility, which makes it a popular choice for businesses worldwide. ## Getting Started The first thing we need to do to get things started is to install the PostgreSQL custom node and gain an understanding of PostgreSQL configuration details. 1. Install `node-red-contrib-postgresql` by the pallet manager. You can choose other nodes too, but we chose this node because it is part of the [certified node catalog by FlowFuse](https://flowfuse.com/certified-nodes/) which assures that the node is robust, secure, and developed with high quality. 2. Before connecting to your PostgreSQL database, ensure you have the following information ready and environment variables set up as discussed below in the `Adding environment variable` section: - Host: IP address or hostname of your PostgreSQL server. - Port: By default, PostgreSQL uses port 5432. Ensure this matches your PostgreSQL server configuration. - Database: The name of the PostgreSQL database you want to connect to. - User: Username with the necessary privileges to access the specified database. - Password: Corresponding password for the username. 3. Drag the PostgreSQL node onto the canvas, click on that node, and click on the edit icon next to the server input field to configure it. !["Configuring PostgreSQL Connection"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_pgconfig1.png "Configuring PostgreSQL Connection") !["Configuring PostgreSQL Security"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_pgconfig2.png "Configuring PostgreSQL Security") ## Adding environment variables Environment variables are used to securely manage sensitive configuration details, such as API keys, passwords, and secret keys, within your applications. This prevents exposing such information directly in the code or configuration files, for more details refer to [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). 1. Navigate to the instance's setting and then go to the environment section. 2. Click on the `add variable` button and add variables for each of the configuration data that we discussed in the above section. 3. Click on the save button and restart the instance by clicking on the top right `Action` button and selecting the restart option. !["Adding environment variables"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_nodred_environment_variable.png "Adding environment variables") ## Creating Table In this section, we will create a table in our database to store product data. 1. Drag an Inject node onto canvas, and keep it unchanged. 2. Click on the PostgreSQL node we added previously and paste the following SQL command into the query input field. (I have added comments for your understanding of SQL commands) ```sql -- Create a table named product_data if it does not already exist CREATE TABLE IF NOT EXISTS product_data (   -- Define a column named id as a SERIAL type, which serves as the primary key id SERIAL PRIMARY KEY, -- SERIAL data type automatically generates unique integer values for each row inserted into the table     -- Define a column named name to store product names as variable-length character strings with a maximum length of 100 characters, ensuring it's not null   name varchar(100) NOT NULL,     -- Define a column named price to store product prices, ensuring it's not null price int NOT NULL,     -- Define a column named stock to store product stock levels, ensuring it's not null stock int NOT NULL ); ``` !["Creating table for product data"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_nodered_create_table.png "Creating table for product data") 3. Connect the inject node’s output to the PostgreSQL node’s input. ## Installing Dashboard 2.0 Install Dashboard 2.0. Follow these [instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to get up and running. ## Inserting Product Data into the Database In this section, we will add a Form interface that will enable us to obtain product data that we need to insert into the database. Moreover, we will use the PostgreSQL node to interact with the database. !["Adding form to insert data"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_form1.png "Adding form to insert data") 1. Drag a `ui-form` widget onto the canvas and select the created `ui-group`. 2. Add an element for all required input data in the form widget and give it a name, label, and select type, I have selected 'number' as a type for 'price' and 'stock', and 'text' for 'name', but feel free to adjust according to your preference and data requirements. 3. Drag the function node onto Canvas and paste the following script. ```javascript // Destructure the properties from msg.payload (obtained data by using form) const {name, price, stock } = msg.payload; // Create an array containing name, price, and stock // The order of the array items in msg.params will correspond to the placeholders in the SQL query // For example, $1 will be replaced by the value of name, $2 will be replaced by the value of the price, and so on msg.params = [name, price, stock]; return msg; ``` 4. Drag a PostgreSQL node onto the Canvas and click on that node and paste the following SQL command into the query input field ```sql -- This is an SQL INSERT statement used to add data into the product_data table. INSERT INTO product_data ( name, price, stock) -- This line specifies the columns into which data will be inserted. The columns are name, price, and stock. -- It's important to match the columns in the same order as the values in the next line. VALUES ($1, $2, $3); ``` !["Inserting data into database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_insert_data.png "Inserting data into database") 5. Connect ui-form’s output to the function node’s input and the function node's output to the PostgreSQL node’s input. ## Displaying product data on Dashboard 2.0 In this section, we will retrieve all data from our database table and display it on Dashboard 2.0 using the ui-table widget. 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the Canvas and click on that node and paste the following SQL command into the query input field. 3. Drag a ui-table widget onto the canvas and create a new ui-group for it. 4. Connect the inject node's output to the PostgreSQL node’s input and the PostgreSQL node's output to the ui-table's input. ```sql -- Retrieve all data from the product_data table SELECT * FROM product_data; ``` !["Retriving all product data from database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_retrive_data.png "Retriving all product data from database") ## Updating product data to the Database !["Adding form to update product data"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_form2.png "adding form to update product data") In this section, we will add a form interface to collect the product ID and the new stock value for the update process. Feel free to select other data fields that you need to update. To achieve this, we will add a form interface using Dashboard 2.0. Additionally, we will interact with the database using the same PostgreSQL node that we have used so far in this guide. 1. Drag a ui-form widget onto the canvas and create a new ui-group for it. 2. Add elements for product id and stock in the form widget and give it a name, label, and select type. 3. Drag a function node onto Canvas and paste the following script. ```javascript // Destructure the properties from msg.payload const { id, stock } = msg.payload; // Create an array containing id and stock // The order of the array items in msg.params will correspond to the placeholders in the SQL query // For example, $1 will be replaced by the value of id, $2 will be replaced by the value of stock msg.params = [id, stock]; return msg; ``` 4. Drag a PostgreSQL node on canvas, click on that node and paste the following SQL command into the query input field. ```sql -- UPDATE statement to modify data in the product_data table UPDATE product_data -- Specifies the table to be updated (product_data) SET stock = $2 -- Sets the value of the "stock" column to the value represented by the parameter $2. -- The value to be set is typically provided externally, In our context, we get this parameter by "msg.params" WHERE id = $1; -- Specifies the condition that must be met for the update to occur. -- In this case, it updates rows where the "id" column matches the value represented by the parameter $1. ``` !["Updating product data to the database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_update_data.png "Updating product data to the database") 5. Connect ui-form’s output to the function node’s input and the function node's output to the PostgreSQL node’s input. ## Deleting product data from the database !["Deleting product data to the database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_form3.png "Deleting product data to the database") In this section, we'll cover how to delete product data from the database. We will use Dashboard 2.0's form interface to collect essential information like the product id and name. While the product id alone is sufficient to delete a product from the database, we include the product name as an additional precaution to prevent accidental deletion of product data. 1. Drag a ui-form widget onto the canvas and create a new ui-group for it. 2. Add elements for product id and name in the form widget and give it a name, label, and select type. 3. Drag a function node onto Canvas and paste the following script. ```javascript // Destructure the properties from msg.payload const { id, name } = msg.payload; // Create an array containing id and name // The order of the array items in msg.params will correspond to the placeholders in the SQL query // For example, $1 will be replaced by the value of id, $2 will be replaced by the value of name msg.params = [id, name ]; return msg; ``` 4. Drag a PostgreSQL node on canvas, click on that node, and paste the following SQL command into the query input field. ```sql -- Deletes rows from the "product_data" table where both "id" and "name" match the given parameters DELETE FROM product_data -- Specifies the conditions for deletion WHERE id = $1 AND name = $2; ``` !["Deleting product data to the database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_delete_data.png "Deleting product data to the database") 5. Connect ui-form’s output to the function node’s input and the function node's output to the PostgreSQL node’s input. ## Dropping Table This section will explain how to drop ( delete ) tables from the database. 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto canvas and paste the following SQL command into the query input field. ```sql -- Drop the table 'product_data' if it exists to avoid conflicts. DROP TABLE IF EXISTS product_data; -- Note: 'IF EXISTS' is used to check if the table exists in the database before attempting to drop it. ``` !["Droping product\_data from the database"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_drop_tables.png "Droping product_data from the database") ## Deploying Flow !["Deploying Inventory management system's flow"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_nodred_environment_variable_ff_editor.png "Deploying Inventory management system's flow") Our Inventory Management System is now complete and ready for deployment. To initiate the deployment process, locate the red 'Deploy' button positioned in the top right corner. To create, drop tables, and retrieve table data, click on the 'Inject Node' button. For product data insertion, updates, and deletions, navigate to `https://.flowfuse.cloud/dashboard`. !["Inventory management system"](https://flowfuse.com/docs/node-red/database/images/postgresql_with_node-red_Inventory_management_system.png "Inventory management system") ### Best practices to follow 1. Connection Pooling: Implementing connection pooling can significantly enhance the performance of PostgreSQL. It allows multiple clients to reuse database connections, reducing the overhead of establishing new connections for each query. By configuring PostgreSQL to use connection pooling, you can optimize resource usage and improve overall system performance. In this guide, we have configured our PostgreSQL to use connection pooling via the Postgres Config node/tab. 2. Environment Variables: The [Twelve Factors](https://12factor.net/){rel=""nofollow""} emphasize the importance of separating configuration details from the code (flow) to ensure better security. Storing database credentials within the codebase can pose a security risk. Instead, expose the configuration details, as environment variables. This ensures that sensitive information remains secure and can be managed separately from the codebase. 3. Credential Rotation: Regularly rotating database credentials is essential for maintaining robust security practices. This includes changing login information for managed databases and other database access points. Implementing a scheduled credential rotation process, such as quarterly 'rotation days,' streamlines the task and reduces the risk of unauthorized access. # Using Redis with Node-RED (2026 Updated) Redis is a powerful in-memory data structure store that can be used as a database, cache, message broker, and streaming engine. When combined with Node-RED, Redis provides a fast and efficient way to store and retrieve data, manage session states, implement pub/sub messaging patterns, and share data across multiple Node-RED instances. This documentation will walk you through integrating Redis with Node-RED, from basic setup to advanced use cases. ::callout{icon="i-lucide-badge-check"} **Certified nodes for this technology.** FlowFuse maintains certified nodes for production use covering this technology. [redis](https://flowfuse.com/integrations/?certified=1) :: ## Getting Started ### Prerequisites Before you begin, make sure you have the following: - Ensure you have a running Node-RED instance. The quickest and easiest way to have a manageable and scalable Node-RED instance is by [signing up on FlowFuse](https://app.flowfuse.com/){rel=""nofollow""} and creating an instance. - Install the `node-red-contrib-redis` package using the Palette Manager. - Make sure you have your Redis server details ready. ### Configuring the Redis Connection Before using Redis nodes, you need to configure the connection: 1. Drag any **redis** node onto your canvas 2. Double-click the node to open its configuration 3. Click the pencil icon next to "Server" to add a new Redis server configuration 4. Fill in your Redis server details: - **Name**: A friendly name for this connection - **Connection Options**: Can be a connection string (e.g., `redis://localhost:6379`) or a JSON object with IORedis options - **Cluster**: Enable if using Redis Cluster 5. Click **Add** to save the configuration 6. Click **Done** to close the node configuration **Example Connection Options (JSON format):** ```json { "host": "localhost", "port": 6379, "db": 0 } ``` ### Understanding the Nodes in the Package The `node-red-contrib-redis` package provides five specialized nodes: - **redis-command**: Executes any Redis command like SET, GET, or hash operations - **redis-in**: Subscribes to pub/sub channels or blocks on list operations for building queue consumers - **redis-out**: Publishes messages or pushes to lists - **redis-lua-script**: Runs Lua scripts on the server for atomic operations - **redis-instance**: Injects a Redis client into your context for direct API access in function nodes ## Your First Redis Flow Let's create a simple flow that stores and retrieves data from Redis. ### Storing Data 1. Drag an **inject** node onto the canvas and clear the Inject node so it has no `msg.payload` or `msg.topic` set. 2. Drag a **redis-command** node next to it 3. Double-click the redis-command node and configure: - **Command**: `set` - **Server**: Your Redis configuration - **Topic/Key**: `mykey` - **Params**: `["Hello from Node-RED"]` (as a JSON array) 4. Add a **debug** node and connect it to the output of the redis-command node 5. Connect the inject node to the redis-command node, then connect the redis-command node to the debug node 6. Click **Deploy** 7. Click the inject button You should see "OK" in the debug panel, which means your data has been successfully stored in Redis! ### Retrieving Data Now let's retrieve the data we just stored: 1. Add another cleared **inject** node to the canvas 2. Add a **redis-command**node and configure: - **Command**: `get` - **Server**: Your Redis configuration - **Topic/Key**: `mykey` 3. Add a **debug** node 4. Connect the inject node to the redis-command node, then connect the redis-command node to the debug node 5. Click **Deploy** 6. Click the inject button You should now see "Hello from Node-RED" in the debug panel - the value you stored earlier! ## Working with JSON Data Redis stores values as strings, so you need to convert JSON objects before storing them. You’ll also learn how to send the topic and value dynamically. ### Storing JSON 1. Drag an **inject** node onto the canvas. 2. Drag a **change** node onto the canvas and configure it to: - Set `msg.topic` to `sensor:data` - Set `msg.payload` to the following JSONata expression: ```json { "temperature": 22.5, "humidity": 65, "timestamp": $now() } ``` 3. Drag a **JSON** node, This will **stringify** the JSON object so it can be stored in Redis. 4. Drag a **redis-command** node and set the command to `set`. 5. Connect the **inject** node to the **change** node, then the **change** node to the **JSON** node, and finally the **JSON** node to the **redis-command** node. 6. Click **Deploy**, then click the inject button to store the JSON in Redis. ### Reading JSON Back 1. Drag an **inject** node onto the canvas 2. Drag a **change**node and configure it to: - Set `msg.topic` to string `sensor:data` - Set `msg.payload` to JSON `[]` 3. Drag a **redis-command** node and set command to `get` 4. Drag a **json** node (this converts between JSON string and object) 5. Drag a **debug** node 6. Connect the inject node to the change node, then connect the change node to the redis-command node, then connect the redis-command node to the json node, and finally connect the json node to the debug node 7. Click **Deploy** and then click the inject button Check the debug panel. You should see your JSON object with temperature, humidity, and timestamp. If you want to explore more Redis commands beyond `SET` and `GET`, check the official [Redis command reference](https://redis.io/docs/latest/commands/){rel=""nofollow""}. ## Pub/Sub Messaging Redis pub/sub allows different Node-RED flows or instances to communicate in real-time. One flow publishes a message, and any subscribed flows receive it instantly. ### Publishing Temperature Alerts Let's create a flow that publishes alerts when temperature exceeds a threshold: 1. Drag an **inject** node onto the canvas 2. Drag a **change** node and set `msg.payload` to string `"ALERT: Temperature critical in Zone A: 85°C - Equipment shutdown initiated"` 3. Drag a **redis-out**node and configure: - **Method**: `PUBLISH` - **Topic**: `alerts:temperature` - **Server**: Your Redis configuration 4. Connect the inject node to the change node, then connect the change node to the redis-out node 5. Click **Deploy** ### Subscribing to Alert Messages Now create another flow that listens for these alerts (this could be on the same or a different Node-RED instance monitoring the facility): 1. Drag a **redis-in** node onto the canvas and configure it: - **Method**: `SUBSCRIBE` - **Topic**: `alerts:temperature` - **Timeout**: *(optional)* How long the node should listen for messages before automatically stopping. - **Server**: Your Redis connection 2. Drag a **debug** node 3. Connect the **redis-in** node to the **debug** node 4. Click **Deploy** When you click the Inject button in your publisher flow, the alert message will appear in the Debug panel. The subscriber will automatically receive all alerts published to the channel until the timeout (if configured) expires. ## Using Lua Scripts for Atomic Operations Redis Lua scripts allow you to execute multiple Redis operations atomically on the server side. This ensures data consistency and reduces network overhead by bundling multiple commands into a single server-side operation. ### Atomic Counter with Rollback Let's create an inventory system that atomically checks stock and decrements it only if available: 1. Drag an **inject** node onto the canvas 2. Drag a **function** node to prepare the script arguments: ```javascript msg.productId = "inventory:product:SKU-12345"; msg.quantityRequested = 3; msg.payload = [ msg.productId, msg.quantityRequested ]; return msg; ``` 3. Drag a **redis-lua-script**node and configure: - **Keys**: `1` - **Script**: ```lua local key = KEYS[1] local requested = tonumber(ARGV[1]) local current = tonumber(redis.call('GET', key) or "0") if current >= requested then redis.call('DECRBY', key, requested) return {1, current - requested} else return {0, current} end ``` - **Server**: Your Redis configuration 4. Drag a **function** node to process the result: ```javascript const result = msg.payload; const success = result[0]; const remaining = result[1]; if (success === 1) { msg.payload = { status: "success", message: `Order processed. Remaining stock: ${remaining}`, remaining: remaining }; } else { msg.payload = { status: "failed", message: `Insufficient stock. Available: ${remaining}`, available: remaining }; } return msg; ``` 5. Drag a **debug** node 6. Connect the inject node to the first function node, then to the redis-lua-script node, then to the second function node, and finally to the debug node 7. Click **Deploy** Before testing, set the initial inventory using a redis-command node: Command = `SET`, Topic/Key = `inventory:product:SKU-12345`, Params = `10`. Then trigger the Inject node to initialize the value and process orders atomically. ## Direct Redis Client Access with redis-instance The redis-instance node provides direct access to the IORedis client API in function nodes. This is useful for advanced operations, custom commands, or when you need programmatic control over Redis operations. ### Setting Up Redis Instance in Context 1. Drag a **redis-instance**node onto the canvas and configure: - **Name**: `redis` - **Server**: Your Redis configuration - **Topic**: Enter a topic name to identify the Redis instance in the chosen context (e.g., `redis`). This is the name you will use in function nodes to access the client. - **Context**: `flow` (makes it available to all nodes in the flow) 2. Click **Deploy** The Redis client is now available in the flow context for use in function nodes. ### Advanced Pipeline Operations Pipelines allow you to send multiple commands to Redis in a single network round trip, significantly improving performance for batch operations: 1. Drag an **inject** node onto the canvas 2. Drag a **function** node with this code: ```javascript const redis = flow.get('redis'); // Replace 'redis' with your topic if different // Create a pipeline const pipeline = redis.pipeline(); // Add multiple sensor readings in one batch const sensors = [ { id: 'temp-01', value: 23.5, unit: 'C' }, { id: 'temp-02', value: 24.1, unit: 'C' }, { id: 'humidity-01', value: 65, unit: '%' }, { id: 'pressure-01', value: 1013, unit: 'hPa' } ]; sensors.forEach(sensor => { const key = `sensor:${sensor.id}:latest`; const data = JSON.stringify({ value: sensor.value, unit: sensor.unit, timestamp: Date.now() }); pipeline.set(key, data, 'EX', 3600); // Expire in 1 hour }); // Execute all commands at once pipeline.exec((err, results) => { if (err) { node.error(err, msg); return; } msg.payload = { message: `Stored ${results.length} sensor readings`, results: results }; node.send(msg); }); ``` 3. Drag a **debug** node 4. Connect the inject node to the function node, then connect the function node to the debug node 5. Click **Deploy** and click the inject button All sensor readings are stored in a single efficient batch operation. ### Scanning Keys with Cursor When you need to find keys matching a pattern without blocking Redis (important for production systems), use the SCAN command: 1. Drag an **inject** node onto the canvas 2. Drag a **function** node with this code: ```javascript const redis = flow.get('redis'); // Replace 'redis' with your topic if different async function scanKeys() { const matchPattern = 'sensor:*:latest'; const allKeys = []; let cursor = '0'; try { do { // Scan with pattern matching const result = await redis.scan( cursor, 'MATCH', matchPattern, 'COUNT', 100 ); cursor = result[0]; const keys = result[1]; allKeys.push(...keys); } while (cursor !== '0'); msg.payload = { pattern: matchPattern, count: allKeys.length, keys: allKeys }; node.send(msg); } catch (err) { node.error(err, msg); } } scanKeys(); ``` 3. Drag a **debug** node 4. Connect the inject node to the function node, then connect the function node to the debug node 5. Click **Deploy** and click the inject button This safely scans all sensor keys without blocking Redis operations, making it suitable for production environments with large datasets. For more Redis commands, patterns, and advanced capabilities, refer to the [official Redis documentation](https://redis.io/docs/latest/){rel=""nofollow""}. Below is the complete example that we covered in this document. ::render-flow ```json [{"id":"2c33ebeb73062ab3","type":"group","z":"d4f60c79eff5211d","name":"Your First Redis Flow","style":{"label":true},"nodes":["e5f3326ef736cef6","8486e88d8623b42a","d1ebde22d0e224a8","22db25c60af16ab2","fe2be532c53402b3","ebc1e8fc0f1ba2d6","adb95c63d206c633"],"x":514,"y":359,"w":712,"h":162},{"id":"e5f3326ef736cef6","type":"redis-command","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"mykey","params":"[\"Hello from Node-RED\"]","paramsType":"json","payloadType":"json","block":false,"x":810,"y":400,"wires":[["8486e88d8623b42a"]]},{"id":"8486e88d8623b42a","type":"debug","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":970,"y":400,"wires":[]},{"id":"d1ebde22d0e224a8","type":"redis-command","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","server":"e370dc92b39a7ba4","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1000,"y":480,"wires":[["22db25c60af16ab2"]]},{"id":"22db25c60af16ab2","type":"debug","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1130,"y":480,"wires":[]},{"id":"fe2be532c53402b3","type":"change","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","name":"Set Key for GET","rules":[{"t":"set","p":"topic","pt":"msg","to":"mykey","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"[]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":480,"wires":[["d1ebde22d0e224a8"]]},{"id":"ebc1e8fc0f1ba2d6","type":"inject","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":400,"wires":[["e5f3326ef736cef6"]]},{"id":"adb95c63d206c633","type":"inject","z":"d4f60c79eff5211d","g":"2c33ebeb73062ab3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":480,"wires":[["fe2be532c53402b3"]]},{"id":"e370dc92b39a7ba4","type":"redis-config","name":"Local","options":"{\"host\":\"localhost\",\"port\":6379,\"db\":0}","cluster":false,"optionsType":"json"},{"id":"5e2842a4730589c0","type":"group","z":"d4f60c79eff5211d","name":"Working with JSON Data","style":{"label":true},"nodes":["6acc07a393e5ee26","3255e8a9122253b7","a3b0b4a74133778e","4faba929a84fb358","e5c359b934174c8e","abf011c2e81960c4","22a2b54caae5869d","b9598f313e2a9fd2","d0051f637fd1621a","9e59f275496d722d"],"x":514,"y":539,"w":852,"h":162},{"id":"6acc07a393e5ee26","type":"redis-command","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1140,"y":580,"wires":[["3255e8a9122253b7"]]},{"id":"3255e8a9122253b7","type":"debug","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1270,"y":580,"wires":[]},{"id":"a3b0b4a74133778e","type":"inject","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":580,"wires":[["d0051f637fd1621a"]]},{"id":"4faba929a84fb358","type":"redis-command","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","server":"e370dc92b39a7ba4","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1000,"y":660,"wires":[["b9598f313e2a9fd2"]]},{"id":"e5c359b934174c8e","type":"debug","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1270,"y":660,"wires":[]},{"id":"abf011c2e81960c4","type":"change","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"Set Key for GET","rules":[{"t":"set","p":"topic","pt":"msg","to":"sensor:data","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"[]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":660,"wires":[["4faba929a84fb358"]]},{"id":"22a2b54caae5869d","type":"inject","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":660,"wires":[["abf011c2e81960c4"]]},{"id":"b9598f313e2a9fd2","type":"json","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"","property":"payload","action":"","pretty":false,"x":1130,"y":660,"wires":[["e5c359b934174c8e"]]},{"id":"d0051f637fd1621a","type":"change","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"Set Key","rules":[{"t":"set","p":"topic","pt":"msg","to":"sensor:data","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"{\t \"temperature\": 22.5,\t \"humidity\": 65,\t \"timestamp\": $now()\t}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":780,"y":580,"wires":[["9e59f275496d722d"]]},{"id":"9e59f275496d722d","type":"json","z":"d4f60c79eff5211d","g":"5e2842a4730589c0","name":"","property":"payload","action":"","pretty":false,"x":990,"y":580,"wires":[["6acc07a393e5ee26"]]},{"id":"39de5da95585227f","type":"group","z":"d4f60c79eff5211d","name":"Pub/Sub Messaging","style":{"label":true},"nodes":["7756558fd542d657","bf2dc26107eff967","9856ddb8e7bc28e8","bcd9b22fd67ecf7c","22f0526fab3b589b"],"x":514,"y":719,"w":632,"h":162},{"id":"7756558fd542d657","type":"inject","z":"d4f60c79eff5211d","g":"39de5da95585227f","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":760,"wires":[["9856ddb8e7bc28e8"]]},{"id":"bf2dc26107eff967","type":"redis-out","z":"d4f60c79eff5211d","g":"39de5da95585227f","server":"e370dc92b39a7ba4","command":"publish","name":"","topic":"alerts:temperature","obj":true,"x":1030,"y":760,"wires":[]},{"id":"9856ddb8e7bc28e8","type":"change","z":"d4f60c79eff5211d","g":"39de5da95585227f","name":"Set Alert Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"ALERT: Temperature critical in Zone A: 85°C - Equipment shutdown initiated","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":810,"y":760,"wires":[["bf2dc26107eff967"]]},{"id":"bcd9b22fd67ecf7c","type":"redis-in","z":"d4f60c79eff5211d","g":"39de5da95585227f","server":"e370dc92b39a7ba4","command":"subscribe","name":"","topic":"alerts:temperature","obj":true,"timeout":0,"x":630,"y":840,"wires":[["22f0526fab3b589b"]]},{"id":"22f0526fab3b589b","type":"debug","z":"d4f60c79eff5211d","g":"39de5da95585227f","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":990,"y":840,"wires":[]},{"id":"adc4b0df6b0e84e3","type":"group","z":"d4f60c79eff5211d","name":"Using Lua Scripts for Atomic Operations","style":{"label":true},"nodes":["ca8a523710138a11","8804755befa1fbc8","0898e80e28b7d276","0b16690b3c2c10df","9a03ebdea51eefdc","c641d8e54620a73a","7c11a5f1c9716527","a07a025db962be50"],"x":514,"y":899,"w":1112,"h":142},{"id":"ca8a523710138a11","type":"inject","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":1000,"wires":[["8804755befa1fbc8"]]},{"id":"8804755befa1fbc8","type":"function","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"Prepare Lua Script Arguments","func":"// msg.payload format: [keys..., args...]\n// First element(s) are the keys, remaining elements are arguments\nmsg.payload = [\n \"inventory:product:SKU-12345\", // KEYS[1]\n 3 // ARGV[1] - quantity requested\n];\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":850,"y":1000,"wires":[["0898e80e28b7d276"]]},{"id":"0898e80e28b7d276","type":"redis-lua-script","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","server":"e370dc92b39a7ba4","name":"","keyval":"1","func":"local key = KEYS[1]\nlocal requested = tonumber(ARGV[1])\n\nlocal current = tonumber(redis.call('GET', key) or \"0\")\n\nif current >= requested then\n redis.call('DECRBY', key, requested)\n return {1, current - requested}\nelse\n return {0, current}\nend","stored":false,"block":false,"x":1140,"y":1000,"wires":[["0b16690b3c2c10df"]]},{"id":"0b16690b3c2c10df","type":"function","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"Format Lua Script Response","func":"const result = msg.payload;\nconst success = result[0];\nconst remaining = result[1];\n\nif (success === 1) {\n msg.payload = {\n status: \"success\",\n message: `Order processed. Remaining stock: ${remaining}`,\n remaining: remaining\n };\n} else {\n msg.payload = {\n status: \"failed\",\n message: `Insufficient stock. Available: ${remaining}`,\n available: remaining\n };\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1340,"y":1000,"wires":[["9a03ebdea51eefdc"]]},{"id":"9a03ebdea51eefdc","type":"debug","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1530,"y":1000,"wires":[]},{"id":"c641d8e54620a73a","type":"redis-command","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","server":"e370dc92b39a7ba4","command":"SET","name":"","topic":"inventory:product:SKU-12345","params":"[10]","paramsType":"json","payloadType":"json","block":false,"x":880,"y":940,"wires":[["7c11a5f1c9716527"]]},{"id":"7c11a5f1c9716527","type":"debug","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1130,"y":940,"wires":[]},{"id":"a07a025db962be50","type":"inject","z":"d4f60c79eff5211d","g":"adc4b0df6b0e84e3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":610,"y":940,"wires":[["c641d8e54620a73a"]]},{"id":"01a96fb2044d723f","type":"group","z":"d4f60c79eff5211d","name":"Direct Redis Client Access with redis-instance","style":{"label":true},"nodes":["745f41f352fa2212","a9ec9a8a5a66daa3","e3a5634ef0ebcb78","5192081309e08db9","4f169053bbd7add9","15f0b0462f795828","4a394700c6914961"],"x":514,"y":1059,"w":732,"h":222},{"id":"745f41f352fa2212","type":"redis-instance","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","server":"e370dc92b39a7ba4","name":"","topic":"redis","location":"flow","x":590,"y":1100,"wires":[]},{"id":"a9ec9a8a5a66daa3","type":"inject","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":620,"y":1180,"wires":[["e3a5634ef0ebcb78"]]},{"id":"e3a5634ef0ebcb78","type":"function","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"Batch Store Sensor Readings (Pipeline)","func":"const redis = flow.get('redis');\n\n// Create a pipeline\nconst pipeline = redis.pipeline();\n\n// Add multiple sensor readings in one batch\nconst sensors = [\n { id: 'temp-01', value: 23.5, unit: 'C' },\n { id: 'temp-02', value: 24.1, unit: 'C' },\n { id: 'humidity-01', value: 65, unit: '%' },\n { id: 'pressure-01', value: 1013, unit: 'hPa' }\n];\n\nsensors.forEach(sensor => {\n const key = `sensor:${sensor.id}:latest`;\n const data = JSON.stringify({\n value: sensor.value,\n unit: sensor.unit,\n timestamp: Date.now()\n });\n pipeline.set(key, data, 'EX', 3600); // Expire in 1 hour\n});\n\n// Execute all commands at once\npipeline.exec((err, results) => {\n if (err) {\n node.error(err, msg);\n return;\n }\n\n msg.payload = {\n message: `Stored ${results.length} sensor readings`,\n results: results\n };\n node.send(msg);\n});","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":880,"y":1180,"wires":[["5192081309e08db9"]]},{"id":"5192081309e08db9","type":"debug","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1140,"y":1180,"wires":[]},{"id":"4f169053bbd7add9","type":"inject","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":620,"y":1240,"wires":[["15f0b0462f795828"]]},{"id":"15f0b0462f795828","type":"function","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"Scan and List Latest Sensor Keys","func":"const redis = flow.get('redis');\n\nasync function scanKeys() {\n const matchPattern = 'sensor:*:latest';\n const allKeys = [];\n let cursor = '0';\n\n try {\n do {\n // Scan with pattern matching\n const result = await redis.scan(\n cursor,\n 'MATCH', matchPattern,\n 'COUNT', 100\n );\n\n cursor = result[0];\n const keys = result[1];\n allKeys.push(...keys);\n\n } while (cursor !== '0');\n\n msg.payload = {\n pattern: matchPattern,\n count: allKeys.length,\n keys: allKeys\n };\n\n node.send(msg);\n\n } catch (err) {\n node.error(err, msg);\n }\n}\n\nscanKeys();","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":1240,"wires":[["4a394700c6914961"]]},{"id":"4a394700c6914961","type":"debug","z":"d4f60c79eff5211d","g":"01a96fb2044d723f","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1140,"y":1240,"wires":[]},{"id":"d7c53907f5b90ca5","type":"global-config","env":[],"modules":{"node-red-contrib-redis":"1.4.0"}}] ``` :: # Using SQLite with Node-RED (2026 Updated) SQLite is a lightweight, self-contained database that does not require a server. It is ideal for quick setups, small applications, and IoT devices, as it stores data directly in files and is easy to manage. ## Getting Started ### Prerequisites Before you begin, make sure you have the following: - Ensure you have a running Node-RED instance. The quickest and easiest way to have a manageable and scalable Node-RED instance is by [signing up on FlowFuse](https://app.flowfuse.com/){rel=""nofollow""} and creating an instance. - Install the `node-red-node-sqlite` package using the **Palette Manager**. This is the official Node-RED SQLite node maintained by the Node-RED team, ensuring reliability and compatibility. ### Configuring SQLite in Node-RED 1. Drag the **sqlite** node onto the Node-RED canvas and double-click it. 2. In the **Database** field, click the **+**icon to add a new configuration. Enter the database name. - Example: `yourdbname` - For a temporary database `/tmp/yourdbname`. 3. Select the appropriate **Mode/Permissions**: - **Read-Write-Create** - **Read-Write** - **Read-Only** 4. Click **Add**, then **Done** to save the configuration. ### Understanding Types of SQL Queries Available The SQLite node provides four options to specify the SQL query type: - **Fixed Statement** – A static SQL query defined directly in the node. - **Via `msg.topic`** – Accepts the SQL query dynamically from the incoming message's `msg.topic`. - **Prepared Statement** – Uses placeholders in the SQL query, with values supplied via `msg.params` for safer and reusable queries. - **Batch Without Response** – Executes multiple queries in a batch without returning any results, useful for bulk inserts or updates. Most users are familiar with **Fixed Statement** and **Via `msg.topic`**, so let us explore the other two options in more detail. #### Prepared Statement Prepared statements let you safely insert values into queries without worrying about SQL injection. Instead of concatenating strings, you use placeholders and pass the actual values separately. There are two ways to pass parameters: **1. Using Named Parameters (Object)** Use `$` placeholders in your query and pass values as an object in `msg.params`: ```javascript // Function node before sqlite msg.params = { $id: 1, $name: "John Doe" } return msg; ``` **SQL Query:** ```sql INSERT INTO user_table (user_id, user) VALUES ($id, $name); ``` **2. Using Positional Parameters (Array)** Use item-number placeholders ( [[]{.katex-mathml}[[[]{.strut style="height:0.8389em;vertical-align:-0.1944em;"}[1]{.mord}[,]{.mpunct}]{.base}]{.katex-html ariaHidden="true"}]{.katex} 2, etc.) and pass the values as an array in `msg.params`. ```javascript // Function node before sqlite msg.params = [1, "John Doe"]; return msg; ``` **SQL Query:** ```sql INSERT INTO user_table (user_id, user) VALUES ($1, $2); ``` #### Batch Without Response Batch mode allows you to execute **multiple SQL statements in a single operation** without retrieving any results. This is ideal for bulk inserts, updates, deletes, or schema changes where you don't need data returned. The SQL statements are **dynamically generated in code** and passed as a single string in `msg.topic`, with each statement separated by a semicolon. **Example - Simple Batch:** ```javascript // Function node before sqlite // Generate multiple SQL statements msg.topic = ` INSERT INTO user_table (user_id, user) VALUES (1, 'Alice'); INSERT INTO user_table (user_id, user) VALUES (2, 'Bob'); UPDATE user_table SET user = 'Alice Smith' WHERE user_id = 1; `; return msg; ``` **Example - Programmatically Generated Batch:** ```javascript // Function node before sqlite // Build SQL from array of data const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' } ]; // Generate INSERT statements dynamically const inserts = users.map(u => `INSERT INTO user_table (user_id, user) VALUES (${u.id}, '${u.name}');` ).join('\n'); msg.topic = inserts; return msg; ``` > **Note:** If any of the values used to build batch SQL statements come from user input or external sources, always sanitize them or switch to prepared statements. Building SQL strings directly can expose your application to SQL injection vulnerabilities. ### Complete CRUD Operations Example This section demonstrates a simple workflow for Create, Read, Update, and Delete operations using SQLite in Node-RED. Each operation will demonstrate a different SQL query type available in the SQLite node, giving you practical experience with all four methods: Fixed Statement, Via msg.topic, Prepared Statement, and Batch Without Response with important CRUD operations. > **Note:** The following examples use a `devices` table instead of the `user_table` shown in the Prepared Statement and Batch examples above. This provides a more complete, real-world scenario for demonstrating CRUD operations. The importable flow at the end of this section includes all the necessary table creation and operations for the `devices` table. #### Create Table 1. Drag an **inject** node onto the canvas. 2. Drag the **sqlite** node onto the canvas and double-click it. 3. Select the correct SQLite configuration from the database dropdown. 4. Choose **fixed statement** as the SQL query type. 5. Enter the following SQL in the query field: ```sql CREATE TABLE IF NOT EXISTS devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL UNIQUE, name TEXT, status TEXT, location TEXT, last_seen DATETIME DEFAULT CURRENT_TIMESTAMP ); ``` 6. Add a **debug** node and connect all nodes. 7. Deploy the flow and click the inject button. #### Insert 1. Drag an **inject** node onto the canvas. 2. Drag a **function** node and add the following code: ```javascript const devices = [ { id: 'DEV002', name: 'Pressure Sensor', status: 'offline', location: 'Factory Floor 2' }, { id: 'DEV003', name: 'Humidity Sensor', status: 'online', location: 'Warehouse 1' }, { id: 'DEV004', name: 'Vibration Sensor', status: 'online', location: 'Factory Floor 3' }, { id: 'DEV005', name: 'Flow Sensor', status: 'offline', location: 'Plant 1' }, { id: 'DEV006', name: 'Level Sensor', status: 'online', location: 'Tank 1' } ]; const batchSQL = devices.map(d => `INSERT INTO devices (device_id, name, status, location) VALUES ('${d.id}', '${d.name}', '${d.status}', '${d.location}');` ).join('\n'); msg.topic = batchSQL; return msg; ``` 3. Drag the **sqlite**node and configure it: - Select **batch without response** as the SQL query type. 4. Add a **debug** node and connect all nodes. 5. Deploy the flow and click the inject button. #### Read 1. Drag an **inject** node onto the canvas. 2. Drag the **sqlite** node onto the canvas and double-click it. 3. Select the correct SQLite configuration from the database dropdown. 4. Choose **fixed statement** as the SQL query type. 5. Enter the following SQL in the query field: ```sql SELECT * FROM devices; ``` 6. Add a **debug** node and connect all nodes. 7. Deploy the flow and click the inject button. #### Update 1. Drag an **inject** node onto the canvas. 2. Drag a **change**node and configure it: - Set `msg.params` to `["offline", "DEV002"]` (type: JSON) 3. Drag the **sqlite**node and configure it: - Select **prepared statement** as the SQL query type. - Enter the SQL query: ```sql UPDATE devices SET status = $1 WHERE device_id = $2; ``` 4. Add a **debug** node and connect all nodes. 5. Deploy the flow and click the inject button. #### Delete 1. Drag an **inject** node onto the canvas. 2. Drag a **change**node and configure it: - Set `msg.params` to `{}` (type: JSON) - Set `msg.params.$device_id` to `DEV006` (type: string) 3. Drag the **sqlite**node and configure it: - Select **prepared statement** as the SQL query type. - Enter the SQL query: ```sql DELETE FROM devices WHERE device_id = $id; ``` 4. Add a **debug** node and connect all nodes. 5. Deploy the flow and click the inject button. Below is the complete flow created throughout this guide. You can import it into Node-RED and experiment with it. ::render-flow ```json [{"id":"7397d3636bf9eec9","type":"group","z":"9cf82b68bb89e8ce","name":"Create Table ( fixed statement )","style":{"label":true},"nodes":["843c6bddd1fa88dd","c7134f3054775ed9","462a0643f5f85a85"],"x":1034,"y":4459,"w":732,"h":82},{"id":"843c6bddd1fa88dd","type":"inject","z":"9cf82b68bb89e8ce","g":"7397d3636bf9eec9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1140,"y":4500,"wires":[["c7134f3054775ed9"]]},{"id":"c7134f3054775ed9","type":"sqlite","z":"9cf82b68bb89e8ce","g":"7397d3636bf9eec9","mydb":"c91139d411507971","sqlquery":"fixed","sql":"CREATE TABLE IF NOT EXISTS devices (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n device_id TEXT NOT NULL UNIQUE,\n name TEXT,\n status TEXT,\n location TEXT,\n last_seen DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n","name":"Create Table","x":1490,"y":4500,"wires":[["462a0643f5f85a85"]]},{"id":"462a0643f5f85a85","type":"debug","z":"9cf82b68bb89e8ce","g":"7397d3636bf9eec9","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1660,"y":4500,"wires":[]},{"id":"c91139d411507971","type":"sqlitedb","db":"/tmp/sqlite","mode":"RWC"},{"id":"191f069422d0f9b4","type":"group","z":"9cf82b68bb89e8ce","name":"Read Records","style":{"label":true},"nodes":["f145276495e6a7bd","b6e61ecb0379b93b","02c7723103391825"],"x":1034,"y":4559,"w":732,"h":82},{"id":"f145276495e6a7bd","type":"inject","z":"9cf82b68bb89e8ce","g":"191f069422d0f9b4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1140,"y":4600,"wires":[["b6e61ecb0379b93b"]]},{"id":"b6e61ecb0379b93b","type":"sqlite","z":"9cf82b68bb89e8ce","g":"191f069422d0f9b4","mydb":"c91139d411507971","sqlquery":"fixed","sql":"SELECT * FROM devices;\n","name":"Read","x":1470,"y":4600,"wires":[["02c7723103391825"]]},{"id":"02c7723103391825","type":"debug","z":"9cf82b68bb89e8ce","g":"191f069422d0f9b4","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1660,"y":4600,"wires":[]},{"id":"c9bf8c21a73cf4f1","type":"group","z":"9cf82b68bb89e8ce","name":"Batch without response","style":{"label":true},"nodes":["d8d2fd8ff4da64fe","7d7556aa27fa6ca8","d15a64daed322158","ea5920b64362e412"],"x":1034,"y":4659,"w":732,"h":82},{"id":"d8d2fd8ff4da64fe","type":"inject","z":"9cf82b68bb89e8ce","g":"c9bf8c21a73cf4f1","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1140,"y":4700,"wires":[["ea5920b64362e412"]]},{"id":"7d7556aa27fa6ca8","type":"sqlite","z":"9cf82b68bb89e8ce","g":"c9bf8c21a73cf4f1","mydb":"c91139d411507971","sqlquery":"batch","sql":"INSERT INTO devices (device_id, name, status, location)\nVALUES ($id, $name, $status, $location);","name":"Insert","x":1470,"y":4700,"wires":[["d15a64daed322158"]]},{"id":"d15a64daed322158","type":"debug","z":"9cf82b68bb89e8ce","g":"c9bf8c21a73cf4f1","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1660,"y":4700,"wires":[]},{"id":"ea5920b64362e412","type":"function","z":"9cf82b68bb89e8ce","g":"c9bf8c21a73cf4f1","name":"Batch Insert","func":"// Sample 10 devices\nconst devices = [\n { id: 'DEV002', name: 'Pressure Sensor', status: 'offline', location: 'Factory Floor 2' },\n { id: 'DEV003', name: 'Humidity Sensor', status: 'online', location: 'Warehouse 1' },\n { id: 'DEV004', name: 'Vibration Sensor', status: 'online', location: 'Factory Floor 3' },\n { id: 'DEV005', name: 'Flow Sensor', status: 'offline', location: 'Plant 1' },\n { id: 'DEV006', name: 'Level Sensor', status: 'online', location: 'Tank 1' },\n { id: 'DEV007', name: 'Pressure Gauge', status: 'online', location: 'Plant 2' },\n { id: 'DEV008', name: 'Temperature Sensor 2', status: 'offline', location: 'Warehouse 2' },\n { id: 'DEV009', name: 'Humidity Sensor 2', status: 'online', location: 'Plant 3' },\n { id: 'DEV010', name: 'Flow Meter', status: 'online', location: 'Factory Floor 4' }\n];\n\n// Generate batch SQL\nconst batchSQL = devices.map(d =>\n `INSERT INTO devices (device_id, name, status, location) VALUES \n ('${d.id}', '${d.name}', '${d.status}', '${d.location}');`\n).join('\\n');\n\nmsg.topic = batchSQL;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1310,"y":4700,"wires":[["7d7556aa27fa6ca8"]]},{"id":"e199d35406609471","type":"group","z":"9cf82b68bb89e8ce","name":"Update Record ( via prepared statement with array )","style":{"label":true},"nodes":["9688278c29895ab0","ea71fbc3ef5cc8bd","9208e6ef30218e35","1ad5c81d925a0310"],"x":1034,"y":4759,"w":732,"h":82},{"id":"9688278c29895ab0","type":"inject","z":"9cf82b68bb89e8ce","g":"e199d35406609471","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1140,"y":4800,"wires":[["9208e6ef30218e35"]]},{"id":"ea71fbc3ef5cc8bd","type":"sqlite","z":"9cf82b68bb89e8ce","g":"e199d35406609471","mydb":"c91139d411507971","sqlquery":"prepared","sql":"UPDATE devices SET status = $1 WHERE device_id = $2;","name":"Update","x":1480,"y":4800,"wires":[["1ad5c81d925a0310"]]},{"id":"9208e6ef30218e35","type":"change","z":"9cf82b68bb89e8ce","g":"e199d35406609471","name":"Set Params","rules":[{"t":"set","p":"params","pt":"msg","to":"[\"offline\", \"DEV002\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":1310,"y":4800,"wires":[["ea71fbc3ef5cc8bd"]]},{"id":"1ad5c81d925a0310","type":"debug","z":"9cf82b68bb89e8ce","g":"e199d35406609471","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1660,"y":4800,"wires":[]},{"id":"67085d9fa9b7b5dd","type":"group","z":"9cf82b68bb89e8ce","name":"Delete Record ( via prepared statement with object )","style":{"label":true},"nodes":["c551a497bf776c15","1767cd99e3b03bd2","b3570250013c8703","9ec536a39e055ffe"],"x":1034,"y":4859,"w":732,"h":82},{"id":"c551a497bf776c15","type":"inject","z":"9cf82b68bb89e8ce","g":"67085d9fa9b7b5dd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1140,"y":4900,"wires":[["b3570250013c8703"]]},{"id":"1767cd99e3b03bd2","type":"sqlite","z":"9cf82b68bb89e8ce","g":"67085d9fa9b7b5dd","mydb":"c91139d411507971","sqlquery":"prepared","sql":"DELETE FROM devices WHERE device_id = $device_id;","name":"Delete","x":1470,"y":4900,"wires":[["9ec536a39e055ffe"]]},{"id":"b3570250013c8703","type":"change","z":"9cf82b68bb89e8ce","g":"67085d9fa9b7b5dd","name":"Set Params","rules":[{"t":"set","p":"params","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"params.$device_id","pt":"msg","to":"DEV006","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1310,"y":4900,"wires":[["1767cd99e3b03bd2"]]},{"id":"9ec536a39e055ffe","type":"debug","z":"9cf82b68bb89e8ce","g":"67085d9fa9b7b5dd","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1660,"y":4900,"wires":[]},{"id":"4c464964391e1c1c","type":"global-config","env":[],"modules":{"node-red-node-sqlite":"1.1.1"}}] ``` :: # Using TimescaleDB with Node-RED (2026 Updated) In the context of IoT and IIoT applications, time series databases are essential for storing data based on timestamps. While InfluxDB has been a popular choice for a long time, another time series database, TimescaleDB, is gaining popularity. This guide will cover how to use TimescaleDB with Node-RED, how TimescaleDB works, and the queries needed when building IoT applications. If you prefer video tutorials, a few months ago, Grey, OT Data & Community Strategist at Flowfuse, conducted a [live session on TimescaleDB](https://www.youtube.com/watch?v=MD1U6LDqJ1c){rel=""nofollow""}. ## What is TimeScaleDB TimescaleDB is a time-series database built on PostgreSQL for efficiently handling large volumes of event data. This means that a TimescaleDB runs within an overall PostgreSQL instance which enables it to take advantage of many of the attributes of PostgreSQL such as reliability, security, and connectivity to a wide range of third-party tools. !["Image displaying regular postgreSQL table and TimescaleDB hypertable"](https://flowfuse.com/docs/node-red/database/images/timescaledb-with-node-red-hypertables.png "Image displaying regular postgreSQL table and TimescaleDB hypertable"){dataZoomable=""} Unlike PostgreSQL, TimescaleDB uses a distributed hypertable architecture that automatically partitions your data by time. You interact with hypertables in the same way as regular PostgreSQL tables, but with extra features that make managing your time-series data much easier. Each hypertable consists of multiple PostgreSQL tables (chunks). Each chunk is assigned a range of time and only contains data from that range. ## Setting up TimescaleDB environment ### Installing TimescaleDB locally If you want to install TimescaleDB locally, you can follow their official documentation on [Install TimescaleDB](https://docs.timescale.com/self-hosted/latest/install/){rel=""nofollow""}. ### Using TimescaleDB cloud option TimescaleDB also offers a cloud option that simplifies deployment and management. Here’s how to set it up: 1. Go to the [Timescale Cloud](https://console.cloud.timescale.com/signup){rel=""nofollow""} website and sign up for an account. 2. Once logged in, create a new TimescaleDB service by following the on-screen instructions. 3. Choose your service settings, such as region, CPU, memory, and storage requirements, based on your application's needs. 4. After creating the service, you’ll see the connection details. If you cannot see them, go to the "Services" option in the sidebar, click on the created service, and then in the "Overview" tab at the bottom, you will see your configuration details. ## Using TimescaleDB with Node-RED In this section of the guide, we will explore integrating TimescaleDB with Node-RED. We'll cover creating and deleting Hypertables, and inserting, updating, and deleting data from these tables. Additionally, we'll delve into advanced queries for comprehensive data analysis. Throughout this guide, we'll use a temperature example to illustrate each operation. ### Installing PostgreSQL Custom Node Since TimescaleDB is built on top of PostgreSQL, we can use the PostgreSQL node. 1. Click the Node-RED Settings (top-right). 2. Click "Manage Palette". 3. Search for `node-red-contrib-postgresql`. 4. Click "Install". ### Configuring PostgreSQL node with TimescaleDB configurations Before proceeding, make sure you have added environment variables for your TimescaleDB configuration details. For more information, refer to [Using environment variables with Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). 1. Drag a PostgreSQL node onto the canvas and double-click on it. 2. Click on the edit icon next to the Server input field to add configuration details in the PostgreSQL config node. 3. Enter the environment variables set for each of your configuration details in the corresponding input fields. !["Screenshot the FlowFuse instance setting's environment tab"](https://flowfuse.com/docs/node-red/database/images/using-timescaledb-with-node-red-environment-variables.png "Screenshot the FlowFuse instance setting's environment tab"){dataZoomable=""} !["Screenshot showing PostgreSQL config node's connection tab"](https://flowfuse.com/docs/node-red/database/images/using-timescaledb-with-node-red-postgresql-config-node-connection-tab.png "Screenshot showing PostgreSQL config node's connection tab"){dataZoomable=""} !["Screenshot showing PostgreSQL config node's security tab"](https://flowfuse.com/docs/node-red/database/images/using-timescaledb-with-node-red-postgresql-config-node-security-tab.png "Screenshot showing PostgreSQL config node's security tab"){dataZoomable=""} ### Creating Hypertables To create a hypertable, start with creating a standard PostgreSQL table and convert it into a hypertable. 1. Insert the following SQL commands into the PostgreSQL node's query field. ```sql -- Create a standard PostgreSQL table CREATE TABLE sensor_data (         time TIMESTAMPTZ NOT NULL,         location STRING, temperature DOUBLE PRECISION ); -- Convert the table into a hypertable for efficient time-series data management SELECT create_hypertable('sensor_data', 'time'); ``` 2. Drag an Inject node onto the canvas, which we will use to trigger the operation. 3. Connect the Inject node's output to the input of the PostgreSQL node. ::render-flow ```json [{"id":"d766709f13c8410b","type":"inject","z":"7748186d67ad0a58","name":"Create hypertable","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":140,"wires":[["a977cb646de2a30b"]]},{"id":"a977cb646de2a30b","type":"postgresql","z":"7748186d67ad0a58","name":"PostgreSQL","query":"CREATE TABLE sensor_data (\n time TIMESTAMPTZ NOT NULL,\n location TEXT,\n temperature DOUBLE PRECISION\n);\n\nSELECT create_hypertable('sensor_data', 'time');\n","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":490,"y":140,"wires":[["c843732f2088f83e"]]},{"id":"c843732f2088f83e","type":"debug","z":"7748186d67ad0a58","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":140,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Inserting Data into the Table The steps to insert data into a TimescaleDB Hypertable are similar to inserting data into a standard PostgreSQL table. 1. Drag the Inject nodes onto the canvas. 2. Set the `msg.payload.temperature` to the JSONata expression `$floor(($random() * 21) + 30)` which will generate random data for us,  and `msg.payload.location` to "New York" for the first Inject node, and do the same for the second Inject node but with a different location. !["Screenshot of the inject node generating sensor data for new york city"](https://flowfuse.com/docs/node-red/database/images/using-timescaledb-with-node-red-inject-node-2.png "Screenshot of the inject node generating sensor data for new york city"){dataZoomable=""} !["Screenshot of the inject node generating sensor data for new york city"](https://flowfuse.com/docs/node-red/database/images/using-timescaledb-with-node-red-inject-node-1.png "Screenshot of the inject node generating sensor data for new york city"){dataZoomable=""} 3. For both Inject nodes, set the repeat interval to 5 seconds, which inserts data every 5 seconds. 4. Drag a PostgreSQL node onto the canvas and insert the following SQL command into the query field: ```sql -- Insert a new row into the sensor_data table INSERT INTO sensor_data (time, location, temperature) VALUES (     now(), -- Current timestamp     '{{msg.payload.location}}', -- Location of the sensor reading     '{{msg.payload.temperature}}' -- Temperature recorded by the sensor ); ``` 5. Drag a Debug node onto the canvas. 6. Connect the output of the Inject nodes to the input of the PostgreSQL node and the output of PostgreSQL to the input of the Debug node. ::render-flow ```json [{"id":"c42dfdaa44a02eda","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"INSERT INTO sensor_data (time, location, temperature)\nVALUES (now(), '{{msg.payload.location}}', '{{msg.payload.temperature}}');\n","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":490,"y":540,"wires":[["f88822fc9fdf90dc"]]},{"id":"f88822fc9fdf90dc","type":"debug","z":"7748186d67ad0a58","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":700,"y":540,"wires":[]},{"id":"b3b5fc6ad830145e","type":"inject","z":"7748186d67ad0a58","name":"Sensor placed in the New York","props":[{"p":"payload.location","v":"New York","vt":"str"},{"p":"payload.temperature","v":"$floor(($random() * 21) + 30)","vt":"jsonata"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":580,"wires":[["c42dfdaa44a02eda"]]},{"id":"2d07d8d73ed43f37","type":"inject","z":"7748186d67ad0a58","name":"Sensor placed in the Chicago","props":[{"p":"payload.location","v":"Chicago","vt":"str"},{"p":"payload.temperature","v":"$floor(($random() * 21) + 30)","vt":"jsonata"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":520,"wires":[["c42dfdaa44a02eda"]]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Updating data to the table When you need to update multiple rows of a table based on specific conditions, you can do so as follows. In the following flow, we are updating the temperature of rows where the time falls within the specified time range to increase by 0.1 degree: 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL command into the query field: ```sql -- Update temperature data in the sensor_data table UPDATE sensor_data   SET temperature = temperature + 0.1   WHERE time >= '2024-05-29 16:40' -- Starting timestamp for the update     AND time < '20124-05-29 16:50'; -- Ending timestamp for the update ``` 3. Drag a Debug node onto the canvas. 4. Connect the output of the Inject node to the input of the PostgreSQL node and the output of the PostgreSQL node to the input of the Debug node. ::render-flow ```json [{"id":"702b9169ff00396c","type":"inject","z":"7748186d67ad0a58","name":"Updating data based on condition","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":1120,"wires":[["81dcb2d851bb9316"]]},{"id":"81dcb2d851bb9316","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"-- Update temperature data in the sensor_data table\nUPDATE sensor_data\n SET temperature = temperature + 0.1\n WHERE time >= '2024-05-29T11:50:25.859Z' -- Starting timestamp for the update\n AND time < '2024-05-29T12:17:43.305Z'; -- Ending timestamp for the update\n","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":530,"y":1120,"wires":[["29e28595a9f5a0df"]]},{"id":"29e28595a9f5a0df","type":"debug","z":"7748186d67ad0a58","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":1120,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Deleting data to the table 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL into the query field: ```sql -- Delete rows from the sensor_data table where the temperature is below 35 degrees Celsius or humidity is below 60% DELETE FROM sensor_data WHERE temperature < 35 -- Delete rows where the temperature is less than 35 degrees Celsius ``` 3. Drag a Debug node onto the canvas. 4. Connect the output of the Inject node to the input of the PostgreSQL node and the output of the PostgreSQL node to the input of the Debug node. ::render-flow ```json [{"id":"702b9169ff00396c","type":"inject","z":"7748186d67ad0a58","name":"Delete data based on condition","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":1120,"wires":[["81dcb2d851bb9316"]]},{"id":"81dcb2d851bb9316","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"-- Delete rows from the sensor_data table where the temperature is below 35 degrees Celsius or humidity is below 60%\nDELETE FROM sensor_data\nWHERE temperature < 35 -- Delete rows where the temperature is less than 35 degrees Celsius","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":530,"y":1120,"wires":[["29e28595a9f5a0df"]]},{"id":"29e28595a9f5a0df","type":"debug","z":"7748186d67ad0a58","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":1120,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Retrieving all data from the table 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL into the query field: ```sql -- Retrieve all rows from the sensor_data table SELECT * FROM sensor_data; ``` 3. Drag a Debug node onto the canvas. 4. Connect the output of the Inject node to the input of the PostgreSQL node and the output of the PostgreSQL node to the input of the Debug node. ::render-flow ```json [{"id":"d551e15f7013e970","type":"inject","z":"7748186d67ad0a58","name":"Retrieve all data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":660,"wires":[["858063c8e7d90f50"]]},{"id":"858063c8e7d90f50","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"SELECT * FROM sensor_data;","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":550,"y":660,"wires":[["79156835d5ad4c4d"]]},{"id":"79156835d5ad4c4d","type":"debug","z":"7748186d67ad0a58","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":660,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Retrieve Recent Data In situations where you need to quickly access the most recent data, such as monitoring real-time sensor readings or analyzing recent transactions, you can follow these steps: 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL into the query field: ```sql -- Retrieve the most recent 100 rows from the sensor_data table, ordered by timestamp in descending order SELECT * FROM sensor_data ORDER BY time DESC -- Order the results by timestamp in descending order LIMIT 100; -- Limit the results to 100 rows ``` 3. Drag a Debug node onto the canvas. 4. Connect the output of the Inject node to the input of the PostgreSQL node and the output of the PostgreSQL node to the input of the Debug node. ::render-flow ```json [{"id":"9436c79e9d9e3593","type":"inject","z":"7748186d67ad0a58","name":"Retrieve last 100 data ordered by time","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":720,"wires":[["6d26117dd61d0ecc"]]},{"id":"6d26117dd61d0ecc","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"SELECT * FROM sensor_data ORDER BY time DESC LIMIT 100;","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":550,"y":720,"wires":[["092a08307ae6d63c"]]},{"id":"092a08307ae6d63c","type":"debug","z":"7748186d67ad0a58","name":"debug 10","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":720,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Retrieve Data Based on Time Range When you need to retrieve historical records within a specific time frame, follow these steps: 1. Drag an Inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL command into the query field: ```sql -- Retrieve data from the sensor_data table where the timestamp is within the last 400 seconds SELECT * FROM sensor_data WHERE time > NOW() - INTERVAL '400 SECONDS'; ``` 3. Drag a Debug node onto the canvas. 4. Connect the output of the inject node to the input of the PostgreSQL node, and connect the output of the PostgreSQL node to the input of the debug node. ::render-flow ```json [{"id":"60e111b1f6e09613","type":"inject","z":"7748186d67ad0a58","name":"Retrieve data based on time range","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":880,"wires":[["0b1ec338dbb164f9"]]},{"id":"0b1ec338dbb164f9","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"-- Aggregate data into specific time buckets\nSELECT time_bucket('15 minutes', time) AS fifteen_min, -- Create time buckets of 15 minutes\n location, -- Location of the sensor\n MAX(temperature) AS max_temp -- Calculate the maximum temperature within each time bucket\nFROM sensor_data -- Select data from the conditions table\nWHERE time > NOW() - INTERVAL '3 hours' -- Filter data to include only the last 3 hours\nGROUP BY fifteen_min, location -- Group data by time buckets and location\nORDER BY fifteen_min DESC, max_temp DESC; -- Order the results by time bucket in descending order, and then by maximum temperature in descending order\n","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":550,"y":880,"wires":[["f6f6c4fa651ee351"]]},{"id":"f6f6c4fa651ee351","type":"debug","z":"7748186d67ad0a58","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":880,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Aggregating data into specific time bucket Aggregating data involves combining multiple data points into summary statistics, usually over a specified time period or category. In the following flow, we aggregate sensor data from the last three hours into 15-minute intervals, computing summary statistics such as the maximum temperature per interval for each location. 1. Drag an inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL query into the query field: ```sql -- Aggregate data into specific time buckets SELECT time_bucket('15 minutes', time) AS fifteen_min, -- Create time buckets of 15 minutes        location, -- Location of the sensor        *, -- Select all columns        MAX(temperature) AS max_temp -- Calculate the maximum temperature within each time bucket FROM conditions -- Select data from the conditions table WHERE time > NOW() - INTERVAL '3 hours' -- Filter data to include only the last 3 hours GROUP BY fifteen_min, location -- Group data by time buckets and location ORDER BY fifteen_min DESC, max_temp DESC; -- Order the results by time bucket in descending order, and then by maximum temperature in descending order ``` 3. Drag the Debug node onto the canvas. 4. Connect the output of the inject node to the input of the PostgreSQL node, and connect the output of the PostgreSQL node to the input of the debug node. ::render-flow ```json [{"id":"60e111b1f6e09613","type":"inject","z":"7748186d67ad0a58","name":"Aggregating data into specific time bucket","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":880,"wires":[["0b1ec338dbb164f9"]]},{"id":"0b1ec338dbb164f9","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"-- Aggregate data into specific time buckets\nSELECT time_bucket('15 minutes', time) AS fifteen_min, -- Create time buckets of 15 minutes\n location, -- Location of the sensor\n MAX(temperature) AS max_temp -- Calculate the maximum temperature within each time bucket\nFROM sensor_data -- Select data from the conditions table\nWHERE time > NOW() - INTERVAL '3 hours' -- Filter data to include only the last 3 hours\nGROUP BY fifteen_min, location -- Group data by time buckets and location\nORDER BY fifteen_min DESC, max_temp DESC; -- Order the results by time bucket in descending order, and then by maximum temperature in descending order\n","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":550,"y":880,"wires":[["f6f6c4fa651ee351"]]},{"id":"f6f6c4fa651ee351","type":"debug","z":"7748186d67ad0a58","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":780,"y":880,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ### Dropping the table 1. Drag an inject node onto the canvas. 2. Drag a PostgreSQL node onto the canvas and insert the following SQL query into the query field: ```sql -- Drop the table if it exists DROP TABLE IF EXISTS sensor_data; ``` 3. Drag the Debug node onto the canvas. 4. Connect the output of the inject node to the input of the PostgreSQL node, and connect the output of the PostgreSQL node to the input of the debug node. ::render-flow ```json [{"id":"5275332fecd1c715","type":"inject","z":"7748186d67ad0a58","name":"Drop table","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":960,"wires":[["382c26de90712487"]]},{"id":"382c26de90712487","type":"postgresql","z":"7748186d67ad0a58","name":"","query":"DROP TABLE IF EXISTS sensor_data;","postgreSQLConfig":"ea1f8e3d9db95245","split":false,"rowsPerMsg":1,"outputs":1,"x":450,"y":960,"wires":[["d7eb0c9020c968c5"]]},{"id":"d7eb0c9020c968c5","type":"debug","z":"7748186d67ad0a58","name":"debug 12","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":720,"y":960,"wires":[]},{"id":"ea1f8e3d9db95245","type":"postgreSQLConfig","name":"TimescaleDB Configurations","host":"${HOST}","hostFieldType":"str","port":"${PORT}","portFieldType":"num","database":"${DATABASE}","databaseFieldType":"str","ssl":"false","sslFieldType":"bool","applicationName":"","applicationNameType":"str","max":"10","maxFieldType":"num","idle":"1000","idleFieldType":"num","connectionTimeout":"10000","connectionTimeoutFieldType":"num","user":"${USERNAME}","userFieldType":"str","password":"${PASSWORD}","passwordFieldType":"str"}] ``` :: ## Deploying the Flow 1. To test the imported flows, you need to deploy them. To do that, click on the deploy button located in the top right corner. After deploying the flow, you can test each operation such as creating, deleting, updating, and other queries by clicking on the inject button. Upon successful operation, you will be able to see the results in the debug panel of the sidebar. If you want to learn any additional information about PostgreSQL, you can refer to the [Using PostgreSQL with Node-RED](https://flowfuse.com/docs/node-red/database/postgresql/) where you will also find the section which shows the messages received after a successful operation by the PostgresWQL node. # Working with Dates and Times in Node-RED Working with dates and times comes up constantly in Node-RED. Whether you're logging events, scheduling tasks, checking business hours, displaying the current time, or pulling historical data, it all relies on handling timestamps correctly. The best part is that you can manage all of this using visual nodes, without writing any code. This documentation walks you through everything you need to know about working with dates and times in Node-RED. You'll learn how to generate timestamps, format them for display, work with different timezones, and perform time-based calculations. ## Getting the Current Time in Node-RED The most straightforward way to get the current time is with the **inject** node or **change** node. ### Using Inject and Change Nodes Both the **inject** node and **change** node can generate timestamps. Use **inject** when you want to trigger a flow with a timestamp, and use **change** when you need to add a timestamp to a message that's already flowing through. #### Basic Timestamp Options (Step-by-Step): 1. Open an **inject** or **change** node configuration window. 2. Locate the dropdown menu next to `msg.payload`. 3. Select **timestamp**. This setting gives you the current time as milliseconds since the epoch (e.g., `1702310400000`). 4. To see other formats, click the small arrow on the right side to expand more options: - **milliseconds since epoch** - A number representing the timestamp (`1702310400000`) - **YYYY-MM-DDTHH\:mm\:ss.sssZ** - An ISO 8601 string (`"2024-12-11T15:45:30.000Z"`) - **JavaScript Date object** - Shows as `[object Object]` in the debug panel > **Tip:** For most work, use **milliseconds since epoch**. It's the simplest format and works everywhere. #### Using JSONata: Both inject and change nodes support JSONata expressions, which gives you more control: - `$millis()` - Gets the current timestamp (Unix Epoch in milliseconds) - `$now()` - Gets the current time as an ISO string - `$moment()` - Gets a date object using the Moment library In the inject/change node, select **JSONata expression** from the payload type dropdown, then enter your expression. This JSONata approach works identically in both nodes, use whichever fits your flow better. ::render-flow ```json [{"id":"9341cec1a1f9e50e","type":"group","z":"d7101f3a4d45deed","name":"Getting the Current Time in Node-RED","style":{"label":true},"nodes":["89cb30afb7fec451","51937bef067f5257","547ec4b9d6a389c9"],"x":108,"y":73,"w":804,"h":694},{"id":"89cb30afb7fec451","type":"group","z":"d7101f3a4d45deed","g":"9341cec1a1f9e50e","name":"Using Change node ( JSONata )","style":{"label":true},"nodes":["b5bdfd1b2a3b9c27","444d17c32b8bd1c1","276e0fcf607c944d","07fbc83f895b7aa4","ac8e5e28a896f52c","8d4143fb8361c469","aff528c64b9568ad"],"x":134,"y":539,"w":752,"h":202},{"id":"b5bdfd1b2a3b9c27","type":"inject","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"Inject","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":640,"wires":[["444d17c32b8bd1c1","276e0fcf607c944d","07fbc83f895b7aa4"]]},{"id":"444d17c32b8bd1c1","type":"change","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"milliseconds since epoch","rules":[{"t":"set","p":"payload","pt":"msg","to":"$millis()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":580,"wires":[["ac8e5e28a896f52c"]]},{"id":"276e0fcf607c944d","type":"change","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"YYYY-MM-DDTHH:mm:ss.sssZ","rules":[{"t":"set","p":"payload","pt":"msg","to":"$now()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":640,"wires":[["8d4143fb8361c469"]]},{"id":"07fbc83f895b7aa4","type":"change","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"JavaScript Date object","rules":[{"t":"set","p":"payload","pt":"msg","to":"$moment()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":700,"wires":[["aff528c64b9568ad"]]},{"id":"ac8e5e28a896f52c","type":"debug","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":580,"wires":[]},{"id":"8d4143fb8361c469","type":"debug","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":640,"wires":[]},{"id":"aff528c64b9568ad","type":"debug","z":"d7101f3a4d45deed","g":"89cb30afb7fec451","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":700,"wires":[]},{"id":"51937bef067f5257","type":"group","z":"d7101f3a4d45deed","g":"9341cec1a1f9e50e","name":"Using Change node ( Timestamp option )","style":{"label":true},"nodes":["375d143d48695d37","72fcba815fa5cfdd","4e50e0a2e436b658","d41aa61b456bd3a1","f5bec9511bc8f77f","9d3fb7b89adafa8d","7e60fb224e4ff747"],"x":134,"y":319,"w":752,"h":202},{"id":"375d143d48695d37","type":"inject","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"Inject","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":420,"wires":[["72fcba815fa5cfdd","4e50e0a2e436b658","d41aa61b456bd3a1"]]},{"id":"72fcba815fa5cfdd","type":"change","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"milliseconds since epoch","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":360,"wires":[["f5bec9511bc8f77f"]]},{"id":"4e50e0a2e436b658","type":"change","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"YYYY-MM-DDTHH:mm:ss.sssZ","rules":[{"t":"set","p":"payload","pt":"msg","to":"iso","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":420,"wires":[["9d3fb7b89adafa8d"]]},{"id":"d41aa61b456bd3a1","type":"change","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"JavaScript Date object","rules":[{"t":"set","p":"payload","pt":"msg","to":"object","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":480,"wires":[["7e60fb224e4ff747"]]},{"id":"f5bec9511bc8f77f","type":"debug","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":360,"wires":[]},{"id":"9d3fb7b89adafa8d","type":"debug","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":420,"wires":[]},{"id":"7e60fb224e4ff747","type":"debug","z":"d7101f3a4d45deed","g":"51937bef067f5257","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":480,"wires":[]},{"id":"547ec4b9d6a389c9","type":"group","z":"d7101f3a4d45deed","g":"9341cec1a1f9e50e","name":"Using Inject Nodes ( Timestamp Option )","style":{"label":true},"nodes":["499ab581537f5598","b9bd4fdc2f424cbf","b969db92319355f3","ccc969cd1ed85eda","ab178e4e3109f9b0","cdb0cfd879846420"],"x":134,"y":99,"w":752,"h":202},{"id":"499ab581537f5598","type":"inject","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"milliseconds since epoch","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":310,"y":140,"wires":[["b9bd4fdc2f424cbf"]]},{"id":"b9bd4fdc2f424cbf","type":"debug","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":140,"wires":[]},{"id":"b969db92319355f3","type":"inject","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"YYYY-MM-DDTHH:mm:ss.sssZ","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"iso","payloadType":"date","x":340,"y":200,"wires":[["ccc969cd1ed85eda"]]},{"id":"ccc969cd1ed85eda","type":"debug","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":200,"wires":[]},{"id":"ab178e4e3109f9b0","type":"inject","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"JavaScript Date object","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"object","payloadType":"date","x":300,"y":260,"wires":[["cdb0cfd879846420"]]},{"id":"cdb0cfd879846420","type":"debug","z":"d7101f3a4d45deed","g":"547ec4b9d6a389c9","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":260,"wires":[]}] ``` :: > For more advanced date/time operations and formatting, see the [JSONata documentation](https://docs.jsonata.org/date-time-functions){rel=""nofollow""}. ## Formatting Dates for Display Raw timestamps like `1702310400000` or ISO strings like `2024-12-11T15:45:30.000Z` work great for machines, but people need something readable: "December 11, 2024" or "3:45 PM" or "5 minutes ago." Node-RED gives you two excellent options: the moment nodes for heavy lifting, and JSONata for quick, built-in one-offs. ### The Moment Nodes The Moment node handles formatting, timezones, relative time, and date math. It is built on the popular **Moment.js** library. #### Installation 1. Click the menu in the top-right corner (the three horizontal lines). 2. Select **Manage palette** from the dropdown. 3. Open the **Install** tab. 4. Search for `node-red-contrib-moment`. 5. Click **Install** next to the package. Once installed, you’ll see two new nodes in the palette: **Date/Time Formatter** and **Humanizer**. For this documentation, we’ll be using the **Date/Time Formatter** node. #### Your First Format 1. Drag a **Date/Time Formatter** node onto the canvas and double-click to open its configuration. 2. Look at the three key fields: **Input** (where your date lives, usually `msg.payload`), **Output Format** (your pattern), and **Output** (where the result goes, usually `msg.payload`). 3. Type this into the **Format** field: `MMMM D, YYYY`. 4. Connect an inject node (set to timestamp) to the **Date/Time Formatter** node, then connect the **Date/Time Formatter** node to a debug node. 5. Click the inject button. The debug panel will show something like `"December 11, 2024"`. #### Format Patterns The letters in your format string are placeholders that get replaced with parts of the date. You can mix them however you want. | Category | Code | Example (Dec 11, 2024 at 3:45 PM) | Description | | :--------- | :----- | :-------------------------------- | :----------------------------- | | **Years** | `YYYY` | 2024 | Full year | | | `YY` | 24 | Two-digit year | | **Months** | `MMMM` | December | Full month name | | | `MMM` | Dec | Short month name | | | `MM` | 12 | Month number (leading zero) | | **Days** | `DD` | 11 | Day of month (leading zero) | | | `D` | 11 | Day of month (no leading zero) | | | `dddd` | Wednesday | Full day name | | **Time** | `HH` | 15 | 24-hour clock (leading zero) | | | `hh` | 03 | 12-hour clock (leading zero) | | | `mm` | 45 | Minutes (leading zero) | | | `A` | PM | AM/PM marker (uppercase) | **Common Patterns:** - `YYYY-MM-DD` → 2024-12-11 (Good for logs and databases) - `MMMM D, YYYY` → December 11, 2024 (Formal style) - `h:mm A` → 3:45 PM (Standard time) - `HH:mm:ss` → 15:45:30 (24-hour time) #### Adding Custom Text You can include literal text in your format by wrapping it in square brackets. The text inside the brackets will appear exactly as you wrote it. ```text MMMM D, YYYY [at] h:mm A ``` This gives you something like **"December 11, 2024 at 3:45 PM"**. More examples: - `[Last updated:] MMM D [at] h:mm A` → Last updated: Dec 11 at 3:45 PM #### Relative Time Sometimes you want to show how long ago something happened instead of the exact time. If you want **"5 minutes ago"** instead of a specific time, put this in the **Output Format** field: ```text fromNow ``` The **Date/Time Formatter** node will calculate the time difference and give you results like: - "a few seconds ago" - "5 minutes ago" - "3 days ago" This works really well for activity feeds, notifications, or any "last updated" display. ### JSONata Formatting If you don't want to add another node to your flow, you can use JSONata instead. It's already built into the **change** node, so you don't need to install anything. 1. Open a **change** node and set it to modify `msg.payload`. 2. In the "to" dropdown, pick **JSONata expression**. 3. Use JSONata's date functions to format your timestamp. Basic syntax for a timestamp in `msg.payload`: ```text $fromMillis(payload, '[M]/[D]/[Y]') ``` This takes the timestamp in `msg.payload` and converts it to **"12/11/2024"**. #### JSONata Codes JSONata uses square brackets, but the codes are different from the **Date/Time Formatter** node. - `[Y]` or `[Y0001]` → 2024 (Year) - `[M]` or `[M01]` → 12 (Month; use [M01] to force a leading zero) - `[D]` or `[D01]` → 11 (Day of month; use [D01] to force a leading zero) - `[h]` or `[h01]` → 3 (12-hour) - `[m01]` → 45 (Minutes, with leading zero for 0–9) - `[P]` → AM or PM **Common Patterns:** - `$fromMillis(payload, '[M]/[D]/[Y]')` → 12/11/2024 - `$fromMillis(payload, '[h]:[m01] [P]')` → 3:45 PM ::render-flow ```json [{"id":"1e8a1110d36d3f33","type":"group","z":"d7101f3a4d45deed","name":"Formatting Dates for Display","style":{"label":true},"nodes":["f019c2f5ed9d8104","1699577150167eb5"],"x":108,"y":793,"w":804,"h":414},{"id":"f019c2f5ed9d8104","type":"group","z":"d7101f3a4d45deed","g":"1e8a1110d36d3f33","name":"The Moment Nodes","style":{"label":true},"nodes":["bd06565765141868","898b23fa51f95edf","f734d9e8979b20b2","f3a3a5a9974816d3","2de95300ee070276","d5f3d8aeaaeffa7d","b713b196b48c62d3"],"x":134,"y":819,"w":752,"h":202},{"id":"bd06565765141868","type":"moment","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"MMMM D, YYYY","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"MMMM D, YYYY","locale":"en-US","output":"payload","outputType":"msg","outTz":"Africa/Abidjan","x":460,"y":860,"wires":[["898b23fa51f95edf"]]},{"id":"898b23fa51f95edf","type":"debug","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":860,"wires":[]},{"id":"f734d9e8979b20b2","type":"inject","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":920,"wires":[["f3a3a5a9974816d3","d5f3d8aeaaeffa7d","bd06565765141868"]]},{"id":"f3a3a5a9974816d3","type":"moment","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"MMMM D, YYYY [at] h:mm A","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"MMMM D, YYYY [at] h:mm A","locale":"en-US","output":"payload","outputType":"msg","outTz":"Africa/Abidjan","x":510,"y":920,"wires":[["2de95300ee070276"]]},{"id":"2de95300ee070276","type":"debug","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":920,"wires":[]},{"id":"d5f3d8aeaaeffa7d","type":"moment","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"fromNow","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"fromNow","locale":"en-US","output":"payload","outputType":"msg","outTz":"Africa/Abidjan","x":430,"y":980,"wires":[["b713b196b48c62d3"]]},{"id":"b713b196b48c62d3","type":"debug","z":"d7101f3a4d45deed","g":"f019c2f5ed9d8104","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":980,"wires":[]},{"id":"1699577150167eb5","type":"group","z":"d7101f3a4d45deed","g":"1e8a1110d36d3f33","name":"JSONata Formatting","style":{"label":true},"nodes":["678f13a9fad9c397","cf810cbd301fddb0","9443cb33ea42e25b","255b135032862816","2d3b9aee8b0ebe5b"],"x":134,"y":1039,"w":752,"h":142},{"id":"678f13a9fad9c397","type":"inject","z":"d7101f3a4d45deed","g":"1699577150167eb5","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":1120,"wires":[["cf810cbd301fddb0","2d3b9aee8b0ebe5b"]]},{"id":"cf810cbd301fddb0","type":"change","z":"d7101f3a4d45deed","g":"1699577150167eb5","name":"$fromMillis(payload, '[M]/[D]/[Y]')","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis(payload, '[M]/[D]/[Y]')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":1080,"wires":[["9443cb33ea42e25b"]]},{"id":"9443cb33ea42e25b","type":"debug","z":"d7101f3a4d45deed","g":"1699577150167eb5","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1080,"wires":[]},{"id":"255b135032862816","type":"debug","z":"d7101f3a4d45deed","g":"1699577150167eb5","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1140,"wires":[]},{"id":"2d3b9aee8b0ebe5b","type":"change","z":"d7101f3a4d45deed","g":"1699577150167eb5","name":"$fromMillis(payload, '[h]:[m01] [P]')","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis(payload, '[h]:[m01] [P]')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":1140,"wires":[["255b135032862816"]]},{"id":"5e97a86213b906bf","type":"global-config","env":[],"modules":{"node-red-contrib-moment":"5.0.0"}}] ``` :: ## Handling Time Zones When you're working inside Node-RED, the timezone for any operation follows the system timezone of the machine running Node-RED. If your server is in New York, timestamps will show Eastern time. If it's in London, you'll see GMT/BST. But what if you need to display times in a different timezone? The **Date/Time Formatter** node handles all of this. ### Converting to a Different Timezone Open your **Date/Time Formatter** node and you'll see two timezone fields: - **Input Timezone** - The timezone your timestamp is currently in. - **Output Timezone** - The timezone you want to convert to. Type in the timezone you want, like `America/New_York` or `Asia/Tokyo`. #### Finding Timezone Names: The **Date/Time Formatter** node uses the IANA timezone database. These are names like: - `America/New_York` (Eastern time) - `Europe/London` (GMT/BST) - `Asia/Tokyo` (Japan time) You can find the complete list at [wikipedia.org/wiki/List\_of\_tz\_database\_time\_zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones){rel=""nofollow""}. ### Working Example Let's display the current time in three different timezones: 1. Add an **Inject** node (set to timestamp). 2. Add three **Date/Time Formatter** nodes after it. 3. Set a common **Output Format** in all three: `MMMM D, YYYY h:mm A z` 4. Set the **Output Timezone**in each: - First node: `America/New_York` - Second node: `Europe/London` - Third node: `Asia/Tokyo` 5. Connect a **debug** node to each **Date/Time Formatter** node. When you click **Inject**, you’ll see the formatted time in three different timezones. ### JSONata Timezone Handling JSONata can also handle timezones by providing the offset in the third parameter of `$fromMillis()`: ```text $fromMillis(payload, '[M]/[D]/[Y] [h]:[m01] [P]', '-0500') ``` The offset is a string like `-0500` (5 hours behind UTC). This works, but you have to know the offset and manage daylight saving time yourself. The **Date/Time Formatter** node handles all of that automatically. ::render-flow ```json [{"id":"894fb9d3ddfa14d7","type":"group","z":"d7101f3a4d45deed","name":"Handling Time Zones","style":{"label":true},"nodes":["4d735b1f71f79189","0c31bb975d166128"],"x":108,"y":1233,"w":804,"h":474},{"id":"4d735b1f71f79189","type":"group","z":"d7101f3a4d45deed","g":"894fb9d3ddfa14d7","name":"Converting to a Different Timezone Using Moment nodes","style":{"label":true},"nodes":["995cf0548ac66af1","963db1d838c1a34d","faefd25e268f68e9","9259ebfd137a16b0","32f2b829e7a6de84","6a4a272401d45e61","48bf3927e175904c"],"x":134,"y":1259,"w":752,"h":202},{"id":"995cf0548ac66af1","type":"inject","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":1360,"wires":[["963db1d838c1a34d","6a4a272401d45e61","48bf3927e175904c"]]},{"id":"963db1d838c1a34d","type":"moment","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"America/New_York","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"MMMM D, YYYY h:mm A z","locale":"en-US","output":"payload","outputType":"msg","outTz":"America/New_York","x":470,"y":1300,"wires":[["faefd25e268f68e9"]]},{"id":"faefd25e268f68e9","type":"debug","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1300,"wires":[]},{"id":"9259ebfd137a16b0","type":"debug","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1360,"wires":[]},{"id":"32f2b829e7a6de84","type":"debug","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1420,"wires":[]},{"id":"6a4a272401d45e61","type":"moment","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"Europe/London","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"MMMM D, YYYY h:mm A z","locale":"en-US","output":"payload","outputType":"msg","outTz":"Europe/London","x":460,"y":1360,"wires":[["9259ebfd137a16b0"]]},{"id":"48bf3927e175904c","type":"moment","z":"d7101f3a4d45deed","g":"4d735b1f71f79189","name":"Asia/Tokyo","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":0,"adjType":"days","adjDir":"add","format":"MMMM D, YYYY h:mm A z","locale":"en-US","output":"payload","outputType":"msg","outTz":"Asia/Tokyo","x":440,"y":1420,"wires":[["32f2b829e7a6de84"]]},{"id":"0c31bb975d166128","type":"group","z":"d7101f3a4d45deed","g":"894fb9d3ddfa14d7","name":"Converting to a Different Timezone Using JSONata","style":{"label":true},"nodes":["ee7822f24a2792cb","0a006c9e87656cfc","31122b7227db8ce7","c62c7e7b3b450e85","6dfde983c76894c3","74e37a7cc2080238","f0c786e6db30ffad"],"x":134,"y":1479,"w":752,"h":202},{"id":"ee7822f24a2792cb","type":"inject","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":1580,"wires":[["6dfde983c76894c3","74e37a7cc2080238","f0c786e6db30ffad"]]},{"id":"0a006c9e87656cfc","type":"debug","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1520,"wires":[]},{"id":"31122b7227db8ce7","type":"debug","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1580,"wires":[]},{"id":"c62c7e7b3b450e85","type":"debug","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1640,"wires":[]},{"id":"6dfde983c76894c3","type":"change","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"America/New_York ( Winter )","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis(payload, '[M]/[D]/[Y] [h]:[m01] [P]', '-0500')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":1520,"wires":[["0a006c9e87656cfc"]]},{"id":"74e37a7cc2080238","type":"change","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"Europe/London ( Winter )","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis(payload, '[M]/[D]/[Y] [h]:[m01] [P]', '+0000')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":1580,"wires":[["31122b7227db8ce7"]]},{"id":"f0c786e6db30ffad","type":"change","z":"d7101f3a4d45deed","g":"0c31bb975d166128","name":"Asia/Tokyo ( Winter )","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis(payload, '[M]/[D]/[Y] [h]:[m01] [P]', '+0900')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":480,"y":1640,"wires":[["c62c7e7b3b450e85"]]},{"id":"dcb2806bdd099173","type":"global-config","env":[],"modules":{"node-red-contrib-moment":"5.0.0"}}] ``` :: ## Doing Math with Dates You'll need date calculations for things like historical dashboards showing the last 7 days of data or checking how many days until a deadline. ### Adding and Subtracting Time Open the **Date/Time Formatter** node and you'll see the **Adjustment** field. This lets you modify the incoming date by a specific unit of time. - On the left, there's a dropdown for `+` or `-`. - On the right, there's a dropdown with units: **days, hours, minutes, weeks, months, years,** etc. #### Adjustment Examples: | Goal | Operation | Value | Unit | | :-------------- | :-------- | :---- | :---- | | **Tomorrow** | `+` | 1 | days | | **Yesterday** | `-` | 1 | days | | **2 hours ago** | `-` | 2 | hours | | **Next week** | `+` | 7 | days | **How to Set it Up:** 1. Drag an **Inject** node onto the workspace (set payload to **timestamp**). 2. Connect a **Date/Time Formatter** node and double-click to open it. 3. Configure your desired adjustment (e.g., `+ 1 days`). 4. Set the **Output Format** field, maybe to `YYYY-MM-DD`. 5. Connect the Formatter to a **Debug** node and Deploy the flow. Hit the Inject button to see the adjusted date. There’s a lot more you can do with the Moment node, including advanced formatting options and additional date/time transformations. For more information, read the node’s [README documentation](https://flows.nodered.org/node/node-red-contrib-moment){rel=""nofollow""}. ### Calculating Time Differences Sometimes you need to know the duration between two timestamps. The moment node doesn't directly calculate differences, so for this, you'll want to use a **Change** node with JSONata. JSONata can calculate differences with simple subtraction, as timestamps are in milliseconds. #### JSONata Difference Formula The basic formula is to subtract the earlier timestamp from the later one, then divide to convert the result into your desired unit. | Unit | Division Value (ms) | Example Formula | | :---------- | :------------------ | :----------------------- | | **Seconds** | `1000` | `(ts1 - ts2) / 1000` | | **Minutes** | `60000` | `(ts1 - ts2) / 60000` | | **Hours** | `3600000` | `(ts1 - ts2) / 3600000` | | **Days** | `86400000` | `(ts1 - ts2) / 86400000` | #### Working Example (Difference in Days) This example calculates the difference between a timestamp seven days ago and the current time (7 days). 1. Drag an **Inject** node onto the workspace (set payload to **timestamp**). 2. Drag a **Change** node and connect it. Use this node to set up our two reference times (`msg.start_time` and `msg.end_time`). - **Rule 1:** - **Action:** `Move` - **From:** `msg.payload` - **To:** `msg.end_time` - **Rule 2:** - **Action:** `Set` - **Property:** `msg.start_time` - **To:** `JSONata expression` - **Expression:** `msg.end_time - (7 * 86400000)` (This calculates a timestamp exactly 7 days earlier). 3. Drag a second **Change** node and connect it. This node performs the final calculation. - **Action:** `Set` - **Property:** `msg.days_difference` - **To:** `JSONata expression` - **Expression:** ```text (msg.end_time - msg.start_time) / 86400000 ``` 4. Connect this second Change node to a **Debug** node and **Deploy** the flow. Hit the **Inject** button. The **Debug** tab will show the number of days difference (**7**). ::render-flow ```json [{"id":"48b48dea5aef0701","type":"group","z":"d7101f3a4d45deed","name":"Doing Math with Dates","style":{"label":true},"nodes":["a2137766a67faf67","bb370162520e95d7"],"x":108,"y":1733,"w":964,"h":254},{"id":"a2137766a67faf67","type":"group","z":"d7101f3a4d45deed","g":"48b48dea5aef0701","name":"Using Moment nodes","style":{"label":true},"nodes":["aa71711160b1b545","f76e05557f807562","bfd33b878fefe852"],"x":134,"y":1759,"w":912,"h":82},{"id":"aa71711160b1b545","type":"inject","z":"d7101f3a4d45deed","g":"a2137766a67faf67","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":1800,"wires":[["f76e05557f807562"]]},{"id":"f76e05557f807562","type":"moment","z":"d7101f3a4d45deed","g":"a2137766a67faf67","name":"+ 1 days","topic":"","input":"payload","inputType":"msg","inTz":"Africa/Abidjan","adjAmount":"1","adjType":"days","adjDir":"add","format":"YYYY-MM-DD","locale":"en-US","output":"payload","outputType":"msg","outTz":"Africa/Abidjan","x":430,"y":1800,"wires":[["bfd33b878fefe852"]]},{"id":"bfd33b878fefe852","type":"debug","z":"d7101f3a4d45deed","g":"a2137766a67faf67","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":940,"y":1800,"wires":[]},{"id":"bb370162520e95d7","type":"group","z":"d7101f3a4d45deed","g":"48b48dea5aef0701","name":"Using JSONata","style":{"label":true},"nodes":["551b26f1bfda2d88","4881ffafeb5da64c","82c0370128a1f80d","29f632a5c68ecc55"],"x":134,"y":1879,"w":912,"h":82},{"id":"551b26f1bfda2d88","type":"inject","z":"d7101f3a4d45deed","g":"bb370162520e95d7","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":250,"y":1920,"wires":[["82c0370128a1f80d"]]},{"id":"4881ffafeb5da64c","type":"debug","z":"d7101f3a4d45deed","g":"bb370162520e95d7","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"days_difference","targetType":"msg","statusVal":"","statusType":"auto","x":940,"y":1920,"wires":[]},{"id":"82c0370128a1f80d","type":"change","z":"d7101f3a4d45deed","g":"bb370162520e95d7","name":"Set Start and End Time","rules":[{"t":"move","p":"payload","pt":"msg","to":"end_time","tot":"msg"},{"t":"set","p":"start_time","pt":"msg","to":"msg.end_time - (7 * 86400000)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":1920,"wires":[["29f632a5c68ecc55"]]},{"id":"29f632a5c68ecc55","type":"change","z":"d7101f3a4d45deed","g":"bb370162520e95d7","name":"Difference in Days","rules":[{"t":"set","p":"days_difference","pt":"msg","to":"(msg.end_time - msg.start_time) / 86400000","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":750,"y":1920,"wires":[["4881ffafeb5da64c"]]},{"id":"ccc081e4ccaba879","type":"global-config","env":[],"modules":{"node-red-contrib-moment":"5.0.0"}}] ``` :: # Node-RED Editor Header component The Node-RED editor header is a central component that facilitates navigation to Node-RED main settings, provides a deploy button for executing flows, and access to the user profile. ![Node-RED Editor Header](https://flowfuse.com/docs/node-red/getting-started/editor/images/header.png "Node-RED Editor Header"){dataZoomable=""} ## Node-RED Instance Name Clicking on the left instance name redirects you to that instance's advanced options provided by the [FlowFuse Cloud](https://flowfuse.com). These options include overview, edge devices assigned to that instance, audit logs for monitoring instance activities, settings and more. ![FlowFuse Instance Option](https://flowfuse.com/docs/node-red/getting-started/editor/images/header-flowfuse-instance.png "FlowFuse Instance Option; click to redirect to instance on platform"){dataZoomable=""} For more information, refer to the [FlowFuse Instance](https://flowfuse.com/docs/user/introduction/#working-with-instances). # Deploy Button On the right-hand side of the header, you will find a red deploy button. Clicking on it executes all flows within the instance. The red color indicates pending changes to deploy, while gray indicates no pending changes. ![Active Deploy Button](https://flowfuse.com/docs/node-red/getting-started/editor/images/deploy-button-active.png "Active Deploy Button"){dataZoomable=""} ![Inactive Deploy Button](https://flowfuse.com/docs/node-red/getting-started/editor/images/deploy-button-inactive.png "Inactive Deploy Button indicating no changes to deploy"){dataZoomable=""} To deploy only modified flows or nodes, or to stop/restart flow execution, click the deploy button's expand icon. Select your preferred option and then click the deploy button accordingly. ![Deploy Button Expand Icon and Options](https://flowfuse.com/docs/node-red/getting-started/editor/images/deploy-button-options.png "Deploy Button Expand Icon and Options"){dataZoomable=""} ## User Profile After the deploy button, you will see the profile icon. Clicking on it allows you to view your FlowFuse username and provides options to log out from that particular instance. This action redirects you to the advanced options provided by the FlowFuse platform, similar to clicking on the instance name. ## Main Menu Right after the user profile, you will see the menu icon at the right-hand corner. Clicking on it will open the list of options that make working with Node-RED. Following are the option available in the main menu ### Edit The first option in the menu allows you to perform essential editing actions. Hovering over it reveals additional options such as undo, redo, copy selected nodes, and more. ![Edit option of the menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-edit-option.png "Edit option of the menu's options"){dataZoomable=""} - **Undo**: Reverses the most recent action or series of actions performed within the editor. - **Redo**: Reapplies an action that was previously undone using the "Undo" command. - **Cut selected nodes**: Removes the selected nodes from the workspace and temporarily stores them in the clipboard. - **Copy selected nodes**: Stores a duplicate copy of the selected nodes in the clipboard without removing them from the original flow. - **Paste nodes**: Allows you to paste the copied or cut nodes from the clipboard back into the workflow. - **Copy group style**: Stores the selected group's style in the clipboard. - **Paste group style**: Applies the stored group style to another selected group. - **Select all**: Selects all flow groups on the current workspace. - **Select connected nodes**: Selects nodes that are connected to the currently selected nodes. - **Select none**: Deselects any selected nodes or groups. - **Split selection with link nodes**: Connects selected nodes using link nodes. ### View This option allows users to control the display and visibility of various interface elements within the Node-RED editor and allows them to access these elements seamlessly. ![View option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-view-option.png "View option of the main menu's options"){dataZoomable=""} - **Show Palette**: Toggles the visibility Node-RED Pallete - **Show Sidebar**: Toggles the visibility of the sidebar. - **Event Log**: Opens a log that records events and actions within Node-RED. - **Action List**: Provides a list of available actions or tasks within Node-RED, which allows to quick access to commonly used operations and functionalities. - **Flow Debugger**: Clicking on it will navigate sidebar debugging tool. - **Linter**: Clicking on it will navigate sidebar [linter tool](https://flowfuse.com/blog/2024/02/software-development-in-node-red/#linting) that provides feedback and suggestions to improve flow readablity, it Checks flows for potential issues or errors based on predefined rules. - **Debug Messages**: Displays messages generated by debug nodes. ### Arrange This option allows you to arrange and manipulate selected flow groups within the Node-RED workspace: ![Arrange option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-arrange-option.png "Arrange option of the main menu's options"){dataZoomable=""} - **Align to Left**: Aligns selected flow groups to the left edge of the workspace. *(Keyboard shortcut: `alt + a l`)* - **Align to Center**: Centers selected flow groups horizontally within the workspace. *(Keyboard shortcut: `alt + a c`)* - **Align to Right**: Aligns selected flow groups to the right edge of the workspace. *(Keyboard shortcut: `alt + a r`)* - **Align to Top**: Aligns selected flow groups to the top edge of the workspace. *(Keyboard shortcut: `alt + a t`)* - **Align to Middle**: Centers selected flow groups vertically within the workspace. *(Keyboard shortcut: `alt + a m`)* - **Align to Bottom**: Aligns selected flow groups to the bottom edge of the workspace. *(Keyboard shortcut: `alt + a b`)* - **Distribute Vertically**: Evenly distributes selected flow groups vertically across the workspace. *(Keyboard shortcut: `alt + a v`)* - **Distribute Horizontally**: Evenly distributes selected flow groups horizontally across the workspace. *(Keyboard shortcut: `alt + a h`)* - **Move Back**: Moves the selected flow groups one layer back in the stacking order. - **Move Front**: Moves the selected flow groups one layer forward in the stacking order. - **Move Backward**: Moves the selected flow groups backward by one position in the stacking order. - **Move Forward**: Moves the selected flow groups forward by one position in the stacking order. ### Import This option allows you to import the application's `flow.json`. In general, you have two main methods available for importing: you can either use the "Clipboard" field or upload the file from your local system by clicking "Select file to import". ![Import option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-import.png "Import option of the main menu's options"){dataZoomable=""} Additionally, you can choose the scope in which the flow should be imported from bottom: - **Current Flow**: Selecting "Current Flow" will import the flow into the existing workspace. - **New Flow**: Choosing "New Flow" will create a new workspace for the imported flow upon clicking the import button. On the left-hand side, you will find three options after the clipboard: - **Local Library**: Here, you can browse the local flow library, which contains flows created within the same Node-RED instance. This library is accessible only from the same instance where the flows were created. - **Examples**: This section contains the examples flows for all of the core nodes as well as the third-party nodes you have installed if they have added. - **Team Library**: This section allows you to browse flow collection that are shared across all Node-RED instances within your team. ### Export This option allows you to export the application flow that you have created. It generally provides two main options: "Copy to Clipboard," which allows you to copy the `flow.json`, and "Download," which downloads the `flow.json` file. ![Export option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-export.png "Export option of the main menu's options"){dataZoomable=""} Additionally, you can choose the scope from which the application flow is exported from the top most options: ![Option to select the scope of the flow that needs to be exported](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-export-scope.png "Option to select the scope of the flow that needs to be exported"){dataZoomable=""} - **Selected nodes**: Selecting this will only export the selected nodes from the flow. - **Current flow**: Selecting this will allow you to export the application from the current flow workspace. - **All flows**: Selecting this will export the flow from all flows within that instance. At the top, you will have two tabs: - **Export nodes**: This tab allows you to see the nodes and flows that you are going to export. - **JSON**: This will show the flow in JSON format that you are going to export. ![Option to compact and format the application flow](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-json.png "Option to compact and format the application flow"){dataZoomable=""} - **Compact**: This will compact your flow JSON into one line. - **Formatted**: This will format the flow JSON, making it easier to read or check. On the left-hand side, you will find two other options after the clipboard: ![Option to export the flow to the Local Library, with the three-dot icon for creating a new folder and an input field to rename the flow file.](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-export-local-library.png "Option to export the flow to the Local Library, with the three-dot icon for creating a new folder and an input field to rename the flow file."){dataZoomable=""} - **Local**: This option allows you to create a collection of the flows that you built into the current Node-RED instance. You can create a new folder by clicking on the three-dot icon in the top-right corner and selecting "New Folder." Additionally, at the bottom, you'll find an input field that allows you to change the flow file name. Clicking on "Export to Library" will save it in the collection. You can now access your collection within the same instance. - **Team Library**: This option also allows you to create a collection of your flows, but the difference is that this collection is shared across all Node-RED instances of your team. ### Search flows ![Search flow option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-search-flow-option.png "Search flow option of the main menu's options"){dataZoomable=""} ![Tab to search the flows of current Node-RED instance](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-search-tab.png "Tab to search the flows of current Node-RED instance"){dataZoomable=""} This option allows to search the flow groups created within that Node-RED instance. ### Configuration nodes ![Configuration option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-configuration-nodes.png "Configuration option of the main menu's options"){dataZoomable=""} Clicking on this option will open the sidebars config tab that will allow you to manage all of the configuration nodes of the current Node-RED Instance. ### Flows ![Flows option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-flows.png "Flows option of the main menu's options"){dataZoomable=""} This option allows you to manage the [flow](https://flowfuse.com/docs/node-red/terminology/#flow) tabs. - **Add**: Adds a new flow tab. - **Edit**: Edits the current flow tab. - **Delete**: Deletes the current flow tab. ### Subflow ![Subflows option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-subflows.png "Subflows option of the main menu's options"){dataZoomable=""} This option allows you to create the [subflow](https://flowfuse.com/docs/node-red/terminology/#subflow). - **Create Subflow**: Creates a new subflow tab. - **Selection to Subflow**: Converts the selected nodes into a subflow. ### Groups ![Groups option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-groups.png "Groups option of the main menu's options"){dataZoomable=""} This option allows you to manage the flow groups. - **Group Selection**: Groups the selected nodes. - **Ungroup Selection**: Ungroups the selected flow group. - **Merge Selection**: Merges the selected flow groups. - **Remove from Group**: Removes the selected nodes from the group. ### Manage Palette This allows users to manage the nodes available in their Node-RED environment. This includes installing new nodes, updating existing ones, and removing nodes that are no longer needed. It provides two main tabs: - **Nodes**: This tab shows the list of installed nodes. In the right corner of each node entry, there are options to: ![Node-RED Palette manger node's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-pallete-manger-nodes-option.png "Node-RED Palette manger node's options"){dataZoomable=""} - **Remove**: Uninstall the node. - **Disable**: Temporarily disable the node. - **Update**: Update the node if an update is available. - **Install**: This tab allows you to install third-party Node-RED nodes from the npm registry. ![Node-RED Palette manager node's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/pallete-manger-filter-and-sort-options.png "Node-RED Palette manager node's options"){dataZoomable=""} - **Filter**: This allows you to filter the nodes by showing all nodes, Node-RED community catalog, and [FlowFuse certified nodes](https://flowfuse.com/certified-nodes/). - **Sort**: These are the sorting options which allow you to sort by relevance (default), in alphabetical order, and by recently updated. - **Reload**: Refresh the list of available third party nodes. ### Settings ![Settings option of the main menu's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/pallete-manger-filter-and-sort-options.png "Settings option of the main menu's options"){dataZoomable=""} This option opens the Node-RED User Settings tab, where you can manage various aspects of your Node-RED environment: - **View**: Configure editor preferences and interface settings. - **Language**: Select the language for the Node-RED editor. - **Restore zoom level on load**: Enabling this option will restore the zoom level of the editor when Node-RED is loaded. - **Restore scroll position on load**: Enabling this option will restore the scroll position of the editor when Node-RED is loaded. - **Show grid**: Toggle to display a grid in the editor workspace. - **Snap to grid**: Toggle to enable snapping nodes to the grid. - **Grid size**: Adjust the size of the grid squares. - **Show node status**: Toggle to display the status of nodes in the editor. - **Show tips**: Toggle to display tips in the editor. - **Show guided tours for new versions**: Toggle to enable guided tours for new versions of Node-RED. - **Palette**: This [Manage palette](https://flowfuse.com/#manage-palette) option allows to Manage nodes available in your Node-RED environment. - **Keyboard**: Configure keyboard shortcuts for efficient navigation and operation in Node-RED. - **Environment**: Manage environment variables used within your Node-RED flows. This includes setting, editing, and deleting variables that can be accessed by nodes during runtime. ![Node-RED Palette manager node's options](https://flowfuse.com/docs/node-red/getting-started/editor/images/environment-variables-options.png "Node-RED Palette manager node's options"){dataZoomable=""} - **+add**: Adds a new environment variable. - **x**: Delete the corresponsing the Environment variables - **Revert**: Discards changes made to the environment variables. - **Linter**: The Linter settings allow you to configure rules for the Node-RED linter tool. The linter provides feedback and suggestions to improve the readability and quality of your flows. Here are the settings you can configure: - **Automatically lint after any change**: If enabled, the linter will automatically check your flows for issues after every change. - **Delay**: Set a delay before the linter runs after a change. - **Lint disabled flows**: When this option is enabled, the linter will also check flows that have been disabled. - **align-to-grid**: Enabling this option will Ensures that nodes are aligned to the grid. \- **gridSize**: Set the grid size to which nodes should align. - **function-eslint**: Run eslint on Function to enforce JavaScript code quality. - **config**: Customize the ESLint configuration, default setting is provided in JSON object. - **max-flow-size**: Limits the maximum size of a flow group to prevent overly large and complex flows. - **maxSize**: Set the maximum allowed flow group size. - **no-duplicate-http-in-urls**: Ensure all HTTP In nodes have a unique URL property. - **no-loops**: Checks for loops in the flow, which can cause issues in execution. - **follow link nodes**: Follow link nodes when checking for loops in the flow. - **no-overlapping-nodes**: Ensures that nodes do not overlap in the workspace. - **no-unconnected-http-nodes**: Identifies HTTP nodes that are not connected to the flow. - **no-unnamed-functions**: Enforces that all function node's have a name for better readability. - **allow default names**: Allows the use of default names for function node's if this option is enabled. - **no-unnamed-links**: Ensures that link nodes have names for better clarity. - **allow default names**: Allows the use of default names for link node's if this option is enabled. ### Keyboard Shortcuts This option directs you to the interface where you can configure keyboard shortcuts for efficient navigation and operation within the Node-RED instance. ### Node-RED Website This option takes you to the official Node-RED website. ### Node-RED Version This option displays the changelog for the current Node-RED version in the sidebar's help tab, detailing what has been changed or fixed compared to previous versions. ### About FlowFuse This option directs you to the [official FlowFuse website](https://flowfuse.com). ### FlowFuse Application This option navigates you to the [FlowFuse Cloud's](https://flowfuse.com) advance options. ### FlowFuse Launcher Version This option shows the current version of the FlowFuse launcher. # Getting Started with the Node-RED Editor The Node-RED Editor is one of the most essential components of Node-RED. As the main focus of Node-RED is to enable visual programming, the editor provides a graphical interface that allows users to create, configure, and manage flows easily. ![Node-RED Editor Window](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-editor-window.png "Node-RED Editor Window"){dataZoomable=""} ## The main components of the Node-RED editor The Node-RED Editor has four main components as follows: - Header: The [header](https://flowfuse.com/docs/node-red/getting-started/editor/header) contains the instance name, user profile menu and the main menu. - Pallete: The [palette](https://flowfuse.com/docs/node-red/getting-started/editor/palette) is a sidebar containing all of the nodes that are installed and available to use. - Workspace: The [workspace](https://flowfuse.com/docs/node-red/getting-started/editor/workspace) is where flows (groups of nodes) are developed by dragging nodes from the palette and wiring them together. Adding a new flow tab gives you a new workspace. - Sidebar: The [sidebar](https://flowfuse.com/docs/node-red/getting-started/editor/sidebar) Provides additional context-sensitive options and information depending on the selected node or workspace. # Node-RED Editor Palette The Palette is the left sidebar that contains all available nodes, including core nodes and third-party nodes that are installed. ## Search Bar ![Image showing Node-RED Palette Search bar](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-search.png "Image showing Node-RED Palette Search bar"){dataZoomable=""} Located at the top of the Palette, the search bar allows you to quickly find nodes by their name. ## Node Categories ![Image showing Node-RED Palette Node Categories](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-category.png "Image showing Node-RED Palette Node Categories"){dataZoomable=""} The palette is divided into several categories, each containing collections of nodes. When you install a third-party node, it may create a new category. Subflows are categorized under "Subflows". You can collapse or expand categories by clicking on the specific category. ## Collapse All Categories ![Image showing Node-RED Palette collapse and expand button](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-collapse-expand.png "Image showing Node-RED Palette collapse and expand button"){dataZoomable=""} At the bottom of the palette, you'll find two arrow icons. Clicking on the first up arrow icon will collapse all categories. ## Expand All Categories Clicking on the down arrow icon will expand all categories back as default. ## Toggle Palette ![Image showing Node-RED Palette toggle palette button](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-palette-toggle.png "Image showing Node-RED Palette toggle palette button"){dataZoomable=""} Clicking this button hides the palette sidebar. To show it again, click on the button once more. # Node-RED Editor Sidebar component The sidebar is located on the right side of your Node-RED Editor. It contains a collection of different tools that make Node-RED easier to use, such as managing nodes, configuration, context storage, and more. ![Image showing Node-RED Palette Search bar](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-sidebar.png "Image showing Node-RED Palette Search bar"){dataZoomable=""} The tools available in the sidebar are called panels. ## Default panels Node-RED sidebar comes with the following default panels: ### Information panel The information panel shows the information of all flows, nodes present in it, subflows and nodes present in them, and global configuration nodes in a tree structure. If you hover over any item in that tree structure, you will see some options that make work easy and fast, such as trigger (to trigger the nodes if the hovered node has a button), enable/disable (to disable and enable the hovered node), show/hide (to hide and show the hovered item in the workspace), lock/unlock (to lock and unlock the hovered item), find (to quickly find the hovered item in the workspace). These options are represented with different icons. #### Search and filtering items The information panel provides a search bar at the top which allows searching the flows, subflows, and nodes easily in the information panel. Additionally, it provides a filter that allows filtering those items by various options such as configuration nodes, unknown nodes, invalid nodes, etc. #### Item properties If you click on an item in the information panel's tree structure, you'll see the flow ID if the clicked item is a flow and Node ID and type if it is a node in the second tab of the information panel. IDs can be copied using the copy option on the right side of the ID. To see more information, click on the show more option, which will show more information on all properties of the node. At the top, you will see three options: the first help, clicking on it will navigate to the help panel and show the readme provided by the author for the selected item; the second option will copy the URL of the selected item, you can use this feature when you want to point or discuss a specific item in the workspace with your team member; and the third option allows you to locate that item in the workspace. ### Help panel This panel allows viewing the docs/readme added by the node package authors to help users. This panel's layout is kind of similar to the information panel and is divided into two tabs. The first tab provides a tree structure with two main topics: "Node-RED" and "Node help." The Node-RED includes the latest changelog and welcome tour (the quick guide for newly added features) for all Node-RED versions. The second tab shows the help docs/readme of the selected node. #### Hiding topics To hide/show the first tab of the help panel, click on the top-left option. #### Viewing Node docs in the help tab To see the readme/docs in the help panel for the nodes, single click on the nodes in the workspace or click on the node item in the tree structure of the "Node help" topic. ### Debug panel The debug panel displays the messages printed by the debug node, this panel helps to debug your application easily. When the messages are printed on the debug panel, each message includes date time, the name of the node that printed this message, the property name, its datatype, and the value. #### Filtering debug messages At the top of the debug panel, you'll see the filter option with three options that allows displaying only those messages which you want: - All nodes: Selecting this option will display messages printed by all the nodes. - Selected nodes: This option allows selecting the specific debug nodes whose messages you want to see in the debug panel. - Current flow: Selecting this option will print only messages printed by the nodes which are present in the current flow. *Note - the Debug sidebar can only show the 100 most recent messages. If the sidebar is currently showing a filtered list of messages, the hidden messages still count towards the 100 limit.* #### Clearing messages To clear the messages, click on the top-right delete option. Alternatively, click on `ctrl + alt + l`. #### Opening a separate window To open the debug panel in a separate window, click on the bottom-right option having a computer icon. Clicking on it will open the debug panel in a new browser window which will make it easy to see and manage debug messages. ### Flow Debugger This panel allows you to debug your flow step by step so that you will get to know where exactly the issue persists. By default, the Flow Debugger is disabled, you can enable it by toggling the top-left option. The panel uses a [breakpoint](https://developers.redhat.com/articles/2022/11/08/introduction-debug-events-learn-how-use-breakpoints#what_is_a_breakpoint_){rel=""nofollow""} approach to debug the flow. Debugging the flow with breakpoints will help you figure out where the message changed and which node is the cause. #### Adding breakpoints To add the breakpoints to the nodes, hover over the node's port and a blue breakpoint indicator will appear. Click it to set the breakpoint on that port. Clicking on it again will remove the breakpoint entirely or you can also use the breakpoint tab to temporarily unselect or remove breakpoints. The runtime will be paused whenever a message arrives at an active breakpoint. You can also manually pause the runtime using the pause button in the sidebar. Once paused, the flow will show how many messages are queued up at each node input and output. Those messages will also be listed in the sidebar - in the order the runtime will process them. #### Processing messages step-by-step To process the messages step-by-step, click on the step button located in the breakpoint tab. Additionally, you can step individual messages by clicking the step button that appears when you hover over the message. ### Configuration nodes panel The configuration nodes panel shows the list of all configuration nodes added to the current Node-RED instance. All the configuration nodes are organized by their scope such as by all flows, by specific flow, and subflow. Each configuration node in the configuration nodes panel displays that node's type and label along with the count of how many current nodes are using this configuration. This configuration panel also provides easy access to the config nodes edit dialog, to access it double-click on the config nodes in the panel. To access this configuration nodes panel click `Ctrl/command + g + c`. #### Filtering config nodes The configuration nodes panel provides an option to filter the nodes by all and unused. ### Linter tool panel This panel provides information on linting issues that persist in the flow. Additionally, it provides suggestions to resolve them. The panel shows the list of linting issues along with the nodes that persist in them. By clicking on the issue, it will help you locate that node in the flow. To refresh the linter panel, click on the top-right refresh option. To modify the linter rules, click on the top-right setting option, which will navigate you to the [linter settings](https://flowfuse.com/docs/node-red/getting-started/editor/header#settings). ### Context Data This panel displays the context variables by their scope. Each variable includes details such as date and time, its store (memory or persistent), and the name and value. To access this panel click `ctrl/command + g + x`. To learn more about context variables refer to the article on [Understanding Node, Flow, Global, and Environment Variables in Node-RED](https://flowfuse.com/blog/2024/05/understanding-node-flow-global-environment-variables-in-node-red/). #### Refreshing the Context variables To refresh the context variables click on the refresh button located at the top-right corner of each scope tab or to refresh a specific variable hover over it and click on the refresh button that appears on the right side of it. #### Deleting context variables To delete the context variable hover over the variable that you want to delete and click on the delete button that appears on the right side of it. #### Copying properties and values To copy the name of that variable hover over it and click on the first button that appears on the right side. To copy the value of that variable hover over it and click on the second button that appears on the right side. ## Hiding the Sidebar To hide the sidebar, click on the sidebar toggle button. To show it back, click on the toggle button again. Alternatively, you can use the `ctrl/command + space` shortcut. ## Resizing the Sidebar To resize the sidebar, hover the mouse over the sidebar's border until the cursor changes. Then, press the left mouse button and hold it while resizing. ## Switching Between Panels To switch between panels, click the expand icon in the top-right corner. This will open a menu with all the available panels. Alternatively, you can click on the small boxes with different icons at the top. # Node-RED Editor Workspace The workspace is the main area in the editor where you build application flows by dropping nodes from the palette. ![Image showing workspace in the editor](https://flowfuse.com/docs/node-red/getting-started/editor/images/editor-workspace.png "Image showing workspace in the editor"){dataZoomable=""} ## View Tools ![Image showing view tools in the editor](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-view-tool.png "Image showing view tools in the editor"){dataZoomable=""} The workspace provides view tools at the footer in the right corner. This includes zoom in (`Ctrl` + `+`) and zoom out (`Ctrl` + `-`) buttons to control the view of the workspace and reset the zoom level to its default. :video{ariaLabel="Video showing navigator tool" autoPlay="true" height="489" loop="true" muted="true" playsInline="true" preload="none" width="800"} Additionally, it provides a view navigator that allows you to see a scaled-down view of the entire workspace. In this view, you can also see the currently visible area of the workspace in the editor. To jump to a specific workspace area, click on that area in the view navigator. If the Linter tool is enabled, next to the view navigator on the left side, you will find the Linter tool option that displays the number of linting issues present in the workspace. By clicking on it, you will navigate to the Linter tool in the sidebar. ## Search Flow ![Image search option for searching the flow into the Node-RED instance](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-search-tool.png "Image search option for searching the flow into the Node-RED instance"){dataZoomable=""} ![Image showing Node-RED Palette Search bar](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-search-tab.png "Image showing Node-RED Palette Search bar"){dataZoomable=""} At the bottom-left of the workspace, you will see a search icon. Clicking on it will open a popup that allows you to quickly search the flows within your Node-RED instance by their name. You can access this dialog by pressing `Ctrl + F`. ## Flow ![Image showing flow tabs](https://flowfuse.com/docs/node-red/getting-started/editor/images/editor-flow-tabs.png "Image showing flow tabs"){dataZoomable=""} A flow is represented as a tab within the editor workspace, providing a new workspace for building applications by connecting nodes. "Flow" is also used informally to describe a single set of connected nodes. Therefore, a flow (tab) can contain multiple flows (sets of connected nodes), but formally, a flow is a parent group of multiple connected nodes. A flow can have a name and description, which will be displayed in the information sidebar. ### Adding a Flow ![Image showing 'add flow' option in the editor](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-add-flow.png "Image showing 'add flow' option in the editor"){dataZoomable=""} To create a parent flow, click on the top-right "+" icon, other you can use the [main menu's flow](https://flowfuse.com/docs/node-red/getting-started/editor/header#flows) add option. ### Editing a flow properties ![Image showing flow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-flow-edit.png "Image showing flow edit dialog"){dataZoomable=""} To edit the flow properties double-click on the flow tab to enter its name and description in the popup form that appears. ### Deleting a Flow ![Image showing option to delete the flow](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-delete-flow.png "Image showing option to delete the flow"){dataZoomable=""} To delete a flow, double-click on it. In the popup that appears, click the delete button at the top-left corner. ![Image showing option to delete the flow in the flow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-delete-flow-dialog.png "Image showing option to delete the flow in the flow edit dialog"){dataZoomable=""} Alternatively, right-click on the flow tab and select "Delete" from the menu. ### Enabling and Disabling Flows ![Image showing option to enable and disable flow in the edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-disable-enable-flow.png "Image showing option to enable and disable flow in the edit dialog"){dataZoomable=""} To enable or disable a flow, double-click on the flow tab. Click the bottom-left "Disable" button or "Enable" button if it is already disabled. ![Image showing option to enable and disable flow](https://flowfuse.com/docs/node-red/getting-started/editor/images/workspace-enable-disable-flow.png "Image showing option to enable and disable flow"){dataZoomable=""} Alternatively, right-click on the flow tab and select "Disable/Enable" from the menu. Disabled flows do not execute when deployed. ### Reordering Flows :video{ariaLabel="Video showing how to reorder flows in the editor" autoPlay="true" height="151" loop="true" muted="true" playsInline="true" preload="none" width="800"} Flows can be reordered by clicking and dragging the flow tab to the desired position. More options for the flow tab menu can be accessed by clicking on the top-right dropdown arrow icon. ## Subflow ![Image showing subflow node](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-node.png "Image showing subflow node"){dataZoomable=""} A subflow in Node-RED is a collection of nodes that are collapsed into a single node in the workspace. It allows you to group a set of nodes together into a reusable unit. This helps in organizing flows, promoting reusability, and simplifying complex flow designs by encapsulating multiple nodes into a single, higher-level node representation. *Note: a subflow cannot contain an instance of itself - either directly or indirectly.* ### Creating a Subflow ![Image showing 'create subflow' option in the main menu](https://flowfuse.com/docs/node-red/getting-started/editor/images/main-menu-subflows.png "Image showing 'create subflow' option in the main menu"){dataZoomable=""} To create the subflow, click on the subflow -> create subflow in the main menu. ![Image showing subflow tab](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-window.png "Image showing subflow tab"){dataZoomable=""} It will create the subflow window like a flow tab for you. ### Editing a Subflow :video{ariaLabel="Video showing 'How to edit the subflow properties'" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} To open the subflow edit dialog, double-click on the subflow node, then click on the "edit template properties". You can give the name for that subflow, add the description by clicking on the top-right ![Image showing description tab in the subflow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-property-description.png "Image showing description tab in the subflow edit dialog"){dataZoomable=""} "Description" option, and also set the appearance by clicking on the top-right "Appearance" option. ![Image showing custom properties tab in the subflow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-custom-properties.png "Image showing custom properties tab in the subflow edit dialog"){dataZoomable=""} In the properties tab, you can define the custom properties that will be added to the subflow's edit dialog. These properties will be then exposed as the environment variables which can be then used by nodes of that subflow. While defining those properties you can also define the data type and label for each. ![Image showing ui preview tab in the subflow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-ui-preview.png "Image showing ui preview tab in the subflow edit dialog"){dataZoomable=""} In the "UI preview" tab, you will see the preview of those properties, showing how they will appear in the subflow. ### Module Properties ![Image showing module tab in the subflow edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-module-tab.png "Image showing module tab in the subflow edit dialog"){dataZoomable=""} In the subflow property dialog, you'll see the module properties option in the top-right corner of the dialog. The Module Properties tab can be used to set additional meta-data about the Subflow, including version, license, and module name. These can be used when [packaging the Subflow as an npm module](https://nodered.org/docs/creating-nodes/subflow-modules){rel=""nofollow""}. ### Deleting a Subflow ![Image showing option to delete the subflow](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-delete-option.png "Image showing option to delete the subflow"){dataZoomable=""} To delete the subflow, click on the "delete subflow" button at the top of the subflow tab. ### Converting Nodes into a Subflow :video{ariaLabel="Video showing how to convert nodes into the subflow" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} If you have nodes on the workspace and you want to create a subflow of them, you can select them by pressing the left mouse key and drawing a rectangle around them. Then click on subflow -> selection to subflow in the main menu. *Note: Wires coming into the selection should be connected to one node - as the resulting subflow node can itself only have at most one input.* ### Inputs & Outputs ![Image showing subflow input and output along with options to add them](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-input-output.png "Image showing subflow input and output along with options to add them"){dataZoomable=""} The subflow's inputs and outputs are depicted by gray square nodes, which can be connected in the subflow workspace like regular nodes. The top toolbar offers functions for adding and removing these nodes. Similar to regular flow nodes, each subflow can have one input at most, but it can accommodate multiple outputs. ### Status Node ![Image showing status node and option to add it](https://flowfuse.com/docs/node-red/getting-started/editor/images/subflow-status-node.png "Image showing status node and option to add it"){dataZoomable=""} The Status node is used to update the status of the subflow. This status node can be edited like regular flow nodes. This node uses the input of `msg.payload` which can either be a simple string or a Status Object. ## Nodes ![Image showing Node in Node palette](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-red-node.png "Image showing Node in Node palette"){dataZoomable=""} A Node is a fundamental building block used to create flows. Each node represents a distinct piece of functionality or a specific action that can be performed within a flow. These nodes can be third-party additions using the palette manager or core nodes. ![Image showing node's input and output](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-input-ouput-port.png "Image showing node's input and output"){dataZoomable=""} Nodes can have one input and multiple output ports, connected via wires, which define the data flow within flows. Both input and output nodes can connect to multiple wires. ![Image showing node's button](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-buttons.png "Image showing node's button"){dataZoomable=""} Some nodes have buttons on either the left or right side. For example, the Inject node has its button on the left, while the Debug node has it on the right. The function of these buttons varies between nodes. ![Image showing the node status](https://flowfuse.com/docs/node-red/getting-started/editor/images/mqtt-in-node-status.png "Image showing the node status"){dataZoomable=""} Additionally, some nodes display status at the bottom with icons indicating their runtime status. For example, MQTT and WebSocket nodes show "connected" text with a green circle icon indicating a successful connection. ![Image showing the Node indicating error](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-indicating-error.png "Image showing the Node indicating error"){dataZoomable=""} ![Image showing the Node indicating undeployed changes](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-indicating-undeployed-changes.png "Image showing the Node indicating undeployed changes"){dataZoomable=""} All nodes in Node-RED indicate errors with a red triangle, undeployed changes with a blue circle, and linting issues with a yellow icon containing an info symbol. ### Adding Nodes to the Workspace There are three different ways to add nodes to the workspace: #### Dragging from the Palette Nodes can be added from the [Node-RED palette](https://flowfuse.com/docs/node-red/getting-started/editor/palette) by dragging them onto the workspace. #### Using Quick-Add Dialog Node-RED Editor provides a quick and easy way to add nodes via the palette: :video{ariaLabel="Video showing how to add nodes quickly using quick-add dialog" autoPlay="true" height="313" loop="true" muted="true" playsInline="true" preload="none" width="471"} - Press `Ctrl` or `Command` and click on the workspace. - Select the desired node from the dialog, which contains all available nodes from the main node palette. - Use the search bar to quickly search nodes. When drawing a wire from one node, leave the wire on the workspace to connect it to a node that will be added using the same quick-add dialog. #### Importing from the Library or Clipboard Nodes can also be added by [importing](https://flowfuse.com/docs/node-red/getting-started/editor/header#import) them from the team or local library or using the clipboard. ### Editing Node Properties ![Image showing node edit property dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-edit-properties-tab.png "Image showing node edit property dialog"){dataZoomable=""} To configure a node's properties, double-click on it or select the node and press Enter. A popup form will appear to configure the node. Configuration options vary depending on the node type. ![Image showing node edit property dialog's description tab](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-description-tab.png "Image showing node edit property dialog's description tab"){dataZoomable=""} - Clicking on the second option from the top-right among three opens a "Description" tab, allowing you to write a Markdown-format description displayed in the information sidebar. ![Image showing node edit property dialog's appperance tab](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-appearance-tab.png "Image showing node edit property dialog's appperance tab"){dataZoomable=""} - Clicking on the third option opens a tab to modify the node's appearance, such as changing icons, naming input and output ports, and toggling label visibility. ### Enabling and Disabling Nodes ![Image showing option to enable/disable node](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-enable-option.png "Image showing option to enable/disable node"){dataZoomable=""} To enable or disable nodes, double-click on the node and click the bottom "Enabled" button or "Disabled" button if already enabled. ### Accessing Node Help Information ![Image showing option to access the node's help document](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-help.png "Image showing option to access the node's help document"){dataZoomable=""} To access node help information, double-click on the node and click the bottom "Book" icon, which displays information related to that node in the information sidebar. ### Configuration Nodes Configuration nodes in Node-RED store configurations shared across multiple nodes within a flow. For example, HTTP Proxy nodes or TLS settings represent configurations for HTTP request nodes. **Note:** Configuration nodes do not encrypt data after configuration, potentially exposing sensitive information if shared improperly. It is recommended to use [environment variables](https://flowfuse.com/docs/user/envvar/) for configuring these nodes to prevent revealing them in the flow. Configuration nodes can be added using the edit dialog of nodes requiring configuration: ![Image showing option to add the config node](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-add-config-node.png "Image showing option to add the config node"){dataZoomable=""} - Click the "+" icon next to the pencil icon and the field showing the text "Add new \*\*\*". - Enter the necessary information in the edit dialog and click "Add". Configuration nodes are not visible on the workspace like other nodes but can be managed in the config nodes tab of the sidebar. ![Image showing note that displays how nodes are using this config node](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-indicating-how-many-nodes-using-config-node.png "Image showing note that displays how nodes are using this config node"){dataZoomable=""} To view how many nodes are using a specific configuration, check the footer information in the config node's edit dialog. Additionally, like common nodes, configuration nodes can be disabled and enabled in the same manner. ## Wires ![Image showing wires](https://flowfuse.com/docs/node-red/getting-started/editor/images/node-wire.png "Image showing wires"){dataZoomable=""} The "wires" refer to the connections that link nodes together to define the flow of data. These wires visually represent the direction and flow of information from one node to another within a Node-RED flow. ### Wiring Nodes Together :video{ariaLabel="Video showing how to wire node's together" autoPlay="true" height="159" loop="true" muted="true" playsInline="true" preload="none" width="554"} To connect the nodes using the wires, left-click on the node's output port and drag the wire to the destination input port. Additionally, if you press the `Ctrl`/`Command` and mouse left key on the input or output port you will not need to hold the left mouse key or any other button to drag the wire. To connect it to the destination port, press the left mouse key on destination port. If the `Ctrl`/`Command` key remains pressed after connecting to the destination port, and if that port's node has an output port, a new wire will be dragged. The wires can be connected from the input port to the output port, not from the input port to the input port or the output port to the output port. ### Deleting Wires :video{ariaLabel="Video showing how to delete the wires" autoPlay="true" height="112" loop="true" muted="true" playsInline="true" preload="none" width="516"} To delete wires, click the left mouse button to select the first wire. To select multiple wires, press and hold the `Ctrl\Command` key while clicking each wire with the left mouse button. If you use only the left mouse button, you can select only one wire at a time. After selecting the wires, press the 'delete' or 'backspace' key to delete them. ### Moving Wires :video{ariaLabel="Video showing how to move a single wire" autoPlay="true" height="207" loop="true" muted="true" playsInline="true" preload="none" width="626"} To disconnect the wire from the port, select the wire by clicking on it. Then press and hold the `Shift` key while the left mouse key is pressed on the port. When the mouse is dragged you'll see the wire disconnects from the port and can be connected to another port. :video{ariaLabel="Video showing how to move multiple wires" autoPlay="true" height="249" loop="true" muted="true" playsInline="true" preload="none" width="635"} If a port has multiple wires connected to it, if none of them are selected when the button is pressed with the Shift key held, all of the wires will move. ### Slicing Wires :video{ariaLabel="Video showing how to slice wires quickly" autoPlay="true" height="173" loop="true" muted="true" playsInline="true" preload="none" width="689"} Wires can also be removed by slicing through them. You can do this by holding the `Alt`/`Option` key and then drawing the line for slicing by holding the left mouse key. ### Detaching Nodes #### Keeping Wire While Deleting Node :video{ariaLabel="Video showing how to delete nodes while keeping wires" autoPlay="true" height="169" loop="true" muted="true" playsInline="true" preload="none" width="710"} To do that, press and hold the `Ctrl`/`Command` key, select the node by clicking the left mouse key, and then press the "delete" or "backspace" button. #### Detaching Node from Wires :video{ariaLabel="Video showing how to detach nodes while keeping wires" autoPlay="true" height="168" loop="true" muted="true" playsInline="true" preload="none" width="664"} To use this option, you have to set the [keyboard shortcut](https://flowfuse.com/docs/node-red/getting-started/editor/header#keyboard-shortcuts) for the "detach-selected-nodes" action. ## Groups ![Image showing flow's group](https://flowfuse.com/docs/node-red/getting-started/editor/images/group.png "Image showing flow's group"){dataZoomable=""} In Node-RED, groups can be created for better organization, containing a single object with included node configurations within the editor. ### Creating a Flow Group :video{ariaLabel="Video showing how to create the group" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} To create a flow group, select nodes (by holding the Ctrl key or drawing a rectangle around them). Navigate to `Groups -> Group selection` in the main menu or press `Ctrl + Shift + G`. ### Editing Group Properties ![Image showing group edit dialog](https://flowfuse.com/docs/node-red/getting-started/editor/images/group-properties.png "Image showing group edit dialog"){dataZoomable=""} A flow group can have a name, background color, and border label visible in the workspace. By default, it has a gray border with no background or name. To style and name a group, double-click on it. Enter the name, select outline and background colors under the fill property, adjust label position and color, then click "Done" from the top-right corner. ### Adding Group-Level Environment Variables ![Image showing Node-RED Palette Search bar](https://flowfuse.com/docs/node-red/getting-started/editor/images/group-level-env.png "Image showing Node-RED Palette Search bar"){dataZoomable=""} To add environment variables at the group level, double-click on it. Select the second option from the top-right in the popup, located under the "Done" button. Click the bottom-most "Add" button to add variables. ### Adding Description ![Image showing group edit dialog's description tab](https://flowfuse.com/docs/node-red/getting-started/editor/images/group-description.png "Image showing group edit dialog's description tab"){dataZoomable=""} To add a description to the group, double-click on it. Select the third option from the top-right in the popup, under the "Done" button. Enter the description in Markdown format. ### Adding Nodes to a Group :video{ariaLabel="Video showing how to add nodes to existing group" autoPlay="true" height="302" loop="true" muted="true" playsInline="true" preload="none" width="750"} To add nodes to an existing group, drag and drop them into the group. This can be done one node at a time. Groups can also be nested within each other in the same manner. ### Removing Nodes from a Group :video{ariaLabel="Video showing how to remove nodes from existing group" autoPlay="true" height="259" loop="true" muted="true" playsInline="true" preload="none" width="681"} To remove nodes from a group, select the nodes and navigate to `Groups -> Remove selection` in the main menu. Similarly, remove a group from another group. Alternatively, click on a node, hold the 'Alt' key, and drag it outside of the group. ### Merging Groups :video{ariaLabel="Video showing how to merge multple groups into single group" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} To merge multiple groups into a single group, select the groups. Go to `Main Menu -> Groups -> Merge selection`. ### Ungrouping Selected Nodes :video{ariaLabel="Video showing how to ungroup selected nodes" autoPlay="true" height="317" loop="true" muted="true" playsInline="true" preload="none" width="800"} To ungroup nodes from a group, select the nodes. Go to `Main Menu -> Groups -> Ungroup selection`. ## Selection Node-RED Editor provides an easy interface for selecting the nodes and the wires on the workspace. A node can be selected or deselected by clicking on it. To select multiple nodes, press the `Ctrl/Command` key and select the nodes you want to select. To select all of the nodes in the workspace, click `Ctrl+A`. ### Lasso Tool :video{ariaLabel="Video showing the lasso tool selection" autoPlay="true" height="288" loop="true" muted="true" playsInline="true" preload="none" width="497"} Node-RED provides a lasso tool to make selection faster. To use the lasso tool, press the left mouse key and drag the cursor, then you can select multiple nodes by drawing a rectangle around them. ### Selecting Connected Nodes :video{ariaLabel="Video showing how to select connected nodes" autoPlay="true" height="189" loop="true" muted="true" playsInline="true" preload="none" width="625"} To select all connected nodes to a specific node, press the `Shift` button and click on the middle of that node. ### Selecting All Upstream Nodes :video{ariaLabel="Video showing how to select upstream connected nodes" autoPlay="true" height="184" loop="true" muted="true" playsInline="true" preload="none" width="648"} To select all of the connected nodes that are before that specific node, press the `Shift` button and while holding it click on the left part of that node. ### Selecting All Downstream Nodes :video{ariaLabel="Video showing how to select downstream connected nodes" autoPlay="true" height="148" loop="true" muted="true" playsInline="true" preload="none" width="590"} To select all of the connected nodes that are after that specific node, press the `Shift` button and while holding it click on the right part of that node. ### Selecting Flows :video{ariaLabel="Video showing how to select multiple flows at a time" autoPlay="true" height="135" loop="true" muted="true" playsInline="true" preload="none" width="800"} To select the flow tabs, press the `Ctrl/Command` key and while holding it click on the flow tab you want to select. Now you can then delete, export, or copy them collectively. # Getting Started with Node-RED This section provides an overview of Node-RED, a robust visual tool designed for integrating IoT devices and automating workflows without needing extensive programming knowledge. - [Getting Started with the Node-RED Editor](https://flowfuse.com/docs/node-red/getting-started/editor/): Learn about the powerful features of Node-RED Editor. - [Node-RED Port (localhost:1880)](https://flowfuse.com/docs/node-red/getting-started/node-red-port/): Learn how to configure Node-RED ports, change default settings, secure your installation, and set up remote access with FlowFuse. - [How to Update Node-RED](https://flowfuse.com/docs/node-red/getting-started/update-node-red/): Learn how to update Node-RED across different installation methods including npm, Raspberry Pi, Docker, and FlowFuse - [Node-RED Library – A Curated and Actively Maintained List of Nodes](https://flowfuse.com/docs/node-red/getting-started/library/): Browse the Node-RED Library for community-built nodes and integrations. FlowFuse's curated catalog offers tested, documented, enterprise-ready solutions with professional support for critical deployments. - [Installing Node-RED on Android](https://flowfuse.com/docs/node-red/getting-started/node-red-android/): Learn how to install and run Node-RED on Android devices using Termux - [Understanding Node-RED Messages](https://flowfuse.com/docs/node-red/getting-started/node-red-messages/): A comprehensive guide to working with Node-RED messages, ensuring error-free flows and optimized data handling. - [Strings in Node-RED: Convert String to Number, Split, Concatenate, Trim, and More](https://flowfuse.com/docs/node-red/getting-started/string/): Learn essential string operations in Node-RED including converting between strings and numbers, splitting and concatenating text, parsing JSON, extracting substrings, trimming whitespace, and more. Step-by-step guide with practical examples. - [Node-RED Programming](https://flowfuse.com/docs/node-red/getting-started/programming/): Master Node-RED programming fundamentals including flows, nodes, messages, conditional logic, and data manipulation. Learn essential concepts for building sophisticated visual programming solutions. - [Working with Dates and Times in Node-RED](https://flowfuse.com/docs/node-red/getting-started/date-and-time/): Learn how to handle dates and times in Node-RED without coding. Master timestamps, formatting, timezones, calculations, and time-based automation with visual nodes. # Node-RED Library The Node-RED library contains thousands of community-contributed nodes that extend Node-RED's core functionality. These pre-built nodes enable connections to hardware devices, cloud services, databases, APIs, and industrial protocols without requiring custom development. The library operates as a central repository where developers share installable packages. Each package adds new nodes to your Node-RED palette, from basic utility functions to sophisticated enterprise integrations. If you need to connect to a specific system or protocol, there's likely an existing node available. ## FlowFuse Integration Catalog While the community library offers extensive options, finding production-ready integrations can be challenging. Documentation quality varies significantly, maintenance is inconsistent, and enterprise scalability isn't always guaranteed. The **[FlowFuse Integration Catalog](https://flowfuse.com/integrations/)** provides vetted, production-grade integrations designed for industrial and enterprise deployments. Created and curated by FlowFuse and the Node-RED community, the catalog includes: - **Tested integrations** verified for enterprise-scale applications - **Complete documentation** with implementation examples and troubleshooting guides - **Security-focused** configurations following industry best practices - **Active maintenance** with regular updates and version compatibility - **Professional support** available for critical implementations Our catalog covers databases, communication protocols, IoT devices, and automation workflows. Each integration includes detailed setup instructions, real-world use cases, and configuration recommendations. New nodes are continuously being added to expand the catalog's capabilities. To strengthen the ecosystem and ensure high-quality nodes, FlowFuse launched the **Certified Nodes 2.0** program, which partners with expert node developers and provides financial rewards for maintaining enterprise-grade nodes. FlowFuse takes full responsibility for maintaining and supporting these integrations. If you need a specific integration or feature added to the catalog, [reach out to us](https://flowfuse.com/contact-us/), we're here to help ensure you have the tools needed for your production deployments. ## Installing Nodes from the Library ### Using the Palette Manager To install nodes through the Node-RED editor: 1. Open your Node-RED editor 2. Click the menu icon (☰) in the top-right corner 3. Select **Manage palette** 4. Navigate to the **Install** tab 5. Search for the required node 6. Click **Install** next to the package :video{ariaLabel="Installing a Node-RED node through the Palette Manager in the Node-RED editor" autoPlay="true" height="720" loop="true" muted="true" playsInline="true" preload="none" width="1326"} Installed nodes appear in your palette immediately and are ready for use in your flows. ### Using Command Line (npm) For automated deployments or when working directly on the server, you can install nodes using npm. Navigate to your Node-RED user directory (typically `~/.node-red` or `/opt/flowfuse-device` if using [flowfuse agent](https://flowfuse.com/platform/device-agent/)) and run: ```bash npm install node-red-contrib-example ``` After installation, restart Node-RED to load the new nodes into your palette. # Installing Node-RED on Android You can run Node-RED on Android devices using Termux, a terminal emulator and Linux environment app. This guide walks you through the installation process and helps you get Node-RED running on your Android phone or tablet. ## Prerequisites - Android device running Android 7.0 or later - At least 1GB of free storage space - Stable internet connection for downloading packages ## Installation Steps ### 1. Install Termux Download and install Termux from [F-Droid](https://f-droid.org/packages/com.termux/){rel=""nofollow""}. Note that the Google Play Store version is outdated and no longer maintained. **Note:** Node-RED and FlowFuse are not affiliated with Termux or F-Droid. These are independent third-party applications that enable running Node-RED on Android devices. Use them at your own discretion. ### 2. Update Termux Packages Open Termux and update the package repository: ```bash pkg update && pkg upgrade ``` Press `Y` when prompted to confirm the updates. ### 3. Install Node.js Install Node.js and npm: ```bash pkg install nodejs ``` Verify the installation: ```bash node --version npm --version ``` ### 4. Install Node-RED Install Node-RED globally using npm: ```bash npm install -g --unsafe-perm node-red ``` The `--unsafe-perm` flag is necessary for the installation to complete successfully on Termux. ### 5. Start Node-RED Launch Node-RED: ```bash node-red ``` You should see output indicating that Node-RED has started. Look for a line similar to: ```text [info] Server now running at http://127.0.0.1:1880/ ``` ### 6. Access the Editor Open a web browser on your Android device and navigate to: ```text http://127.0.0.1:1880 ``` You should see the Node-RED editor interface. ## Accessing Node-RED from Other Devices To access Node-RED from other devices on your local network: 1. Find your Android device's IP address: ```bash ifconfig ``` Look for your IP address (typically starting with 192.168.x.x or 10.x.x.x) 2. Access Node-RED from another device using: ```text http://YOUR_ANDROID_IP:1880 ``` ## Security Considerations **Important:** By default, Node-RED runs without any authentication or encryption. This means anyone who can access the editor URL can view and modify your flows. Before accessing Node-RED from other devices on your network, make sure to secure your installation by enabling authentication and following security best practices. For detailed instructions on securing Node-RED, including enabling authentication, HTTPS, and other security features, refer to the official [Node-RED Security documentation](https://nodered.org/docs/user-guide/runtime/securing-node-red){rel=""nofollow""}. ## Device Access You can get direct access to various hardware on the device by using the extra Termux device plugins - which can then be accessed via Node-RED using the `exec` node. Note: you need to install both the add-on app, and also the add-on API in Termux. 1. Install add-on app - Termux\:API from the same source you got Termux 2. Install add-on access into Termux: ```bash pkg install termux-api ``` 3. Use the [node-red-contrib-termux-api](https://flows.nodered.org/node/node-red-contrib-termux-api){rel=""nofollow""} node to access device features like camera, GPS, sensors, and more Learn more about [how to use Termux API](https://wiki.termux.com/wiki/Termux\:API){rel=""nofollow""}. ## Limitations Running Node-RED on Android has some limitations: - Performance depends on your device's hardware - Some nodes may not work due to Android/Termux limitations - Battery consumption can be significant for long-running instances - Background execution may be restricted by Android's power management # Understanding Node-RED Messages Node-RED operates by passing messages between nodes to create dynamic IoT, automation, and data-processing workflows. Each message transports data that nodes read, modify, process, or analyze. Understanding message structure and handling is essential for building reliable flows. Poor message management can cause subtle bugs like data overwrites, infinite loops, or system crashes. This guide explores Node-RED message mechanics, common pitfalls, and best practices for maintaining smooth, error-free data flow. ## What Are Node-RED Messages? Messages in Node-RED are data packets that flow between nodes in your workflow. Node-RED follows an event-driven architecture where nodes act as both event emitters and listeners, with messages serving as the communication medium between them. :video{ariaLabel="Node-RED message passing animation" autoPlay="true" height="133" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Node-RED message passing visualization* Messages carry the data that powers your workflows, sensor readings, user inputs, API responses, and more. Fundamentally, Node-RED messages are JavaScript objects, providing a flexible structure for managing and transferring data throughout your flows. ### JavaScript Objects Primer A [JavaScript object](https://www.youtube.com/watch?v=BRSg22VacUA){rel=""nofollow""} is a data structure that stores multiple values in key-value pairs. Each key (property) associates with a specific value, allowing organized access to related data. Example: ```javascript { name: "Bob", age: 24, married: true } ``` Here, `name`, `age`, and `married` are object properties. Node-RED messages use this same structure to organize and transport data within flows. ## Anatomy of Node-RED Messages Node-RED messages are referenced as `msg` by default, and nodes are designed around this convention. Key message properties include: - **`msg._msgid`**: A unique identifier automatically assigned by Node-RED for tracking and debugging messages within flows. - **`msg.payload`**: The primary data container. This holds the main information that nodes process, sensor readings, user input, computed results, etc. - **`msg.topic`**: An optional property for categorizing or identifying messages, useful for routing or filtering based on context. These are the most frequently used properties, though additional custom properties can be added as needed. The `_msgid` property appears automatically, even when sending an empty object between nodes. However, `payload` and `topic` are not always present, their inclusion depends on whether nodes append them. Most Node-RED nodes, including community-contributed ones, use `payload` as the standard communication property. ## Data Types in Node-RED Messages Understanding data types is crucial when working with Node-RED messages. Messages themselves must always be JavaScript objects, or Node-RED will throw an error. One exception: you can send `null` as a message. This effectively stops message propagation, preventing data from flowing to subsequent nodes. Message property values can be any JavaScript-supported data type: - [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String){rel=""nofollow""} - [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number){rel=""nofollow""} - [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array){rel=""nofollow""} - [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean){rel=""nofollow""} - [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object){rel=""nofollow""} - [Buffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer){rel=""nofollow""} - Other complex data types :video{ariaLabel="Inject node sending different data types" autoPlay="true" height="301" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Various data types supported by Node-RED* ## How to Clone Messages and Properties Cloning creates an independent copy of a message or its properties, allowing modifications without affecting the original. This is essential when sending different versions of data to multiple flow branches or when preserving original data for comparison. ### Using the Change Node The **Change** node provides a visual interface for modifying and cloning message properties. Note that you cannot clone the entire `msg` object in change node at once, properties must be copied individually. The Change node can clone properties to other `msg` properties, or to flow/global context. Steps to clone using the Change node: 1. Double-click on the **Change** node to open its configuration dialogue. 2. You will see an interface with an existing item added by default. 3. On the left side of the field, you will see options like **"Set"**, **"Change"**, **"Delete"**, and **"Move"**. You can use these options to perform the corresponding operations on the message. 4. To clone the property `msg.payload` to `flow.data`, select the **"Set"** action. In the first **"Property"** field, enter `payload`, and in the **"to the value"** field, select **flow** and enter `data`. For cloning `msg` properties to new `msg` properties, select **msg** in the second field and specify the new property name. For comprehensive information on Change node capabilities, including Delete, Move, and Change actions, refer to the [Change Node documentation](https://nodered.org/docs/user-guide/nodes){rel=""nofollow""}. ### Using the Function Node The Function node offers programmatic control over message cloning using JavaScript: ```js // Clone the entire message var newMsg = RED.util.cloneMessage(msg); // Modify the clone safely (original remains unchanged) newMsg.payload = "Modified data"; // Return the original message return msg; ``` **Critical Note:** Direct assignment like `let newMsg = msg;` does **not** create a true clone. It creates a reference to the same object, meaning changes to `newMsg` will affect `msg` as both point to the same data. ```js // This creates a reference, NOT a clone var newMsg = msg; // Modifying newMsg also modifies the original msg newMsg.payload = "Modified data"; return msg; // The original is now changed! ``` To clone specific properties only: ```js // Clone selective properties var newMsg = {}; newMsg.payload = msg.payload; // Copy payload newMsg.topic = msg.topic; // Copy topic return newMsg; ``` ## Adding New Properties to Messages Node-RED messages are JavaScript objects, making them highly flexible for customization. You can add unlimited properties to carry additional data through your flow, metadata, tags, timestamps, configuration details, and more. ### Using the Change Node The Change node allows property addition without coding: 1. Drag a **Change** node into your flow and open configuration. 2. Select the **Set** action. 3. In the **Property** field, enter the new property name (e.g., `msg.customData`). 4. In the **To** field, enter the value, this can be a string, number, boolean, JSONata expression, or reference to another property. ### Using the Function Node For programmatic control, add properties in a Function node: ```js // Add custom properties to the message msg.customData = { description: "Sensor reading from device A", timestamp: new Date().toISOString(), location: "Building 3, Floor 2" }; // Return the enhanced message return msg; ``` ## Deleting and Moving Message Properties In addition to adding and cloning properties, you may need to remove or relocate properties within messages. ### Deleting Properties #### Using the Change Node Use the **Delete** action to remove unwanted properties from messages: 1. Double-click on the **Change** node to open its configuration dialogue. 2. Select the **"Delete"** action from the dropdown. 3. In the **"Property"** field, specify the property to remove (e.g., `msg.tempData`). #### Using the Function Node To delete properties programmatically in a Function node, use the `delete` operator: ```js // Delete a single property delete msg.tempData; // Delete multiple properties delete msg.tempData; delete msg.oldPayload; delete msg.metadata; return msg; ``` ### Moving Properties #### Using the Change Node Use the **Move** action to relocate a property to a new location while removing it from the original location: 1. Double-click on the **Change** node to open its configuration dialogue. 2. Select the **"Move"** action from the dropdown. 3. In the first **"Property"** field, specify the source property (e.g., `msg.payload`). 4. In the **"to"** field, specify the destination property (e.g., `msg.oldPayload`). #### Using the Function Node To move a property programmatically in a Function node, copy the property to its new location and then delete it from the original: ```js // Move msg.payload to msg.oldPayload msg.oldPayload = msg.payload; delete msg.payload; // Or move a nested property msg.backup = { data: msg.tempData }; delete msg.tempData; return msg; ``` **Important Note:** Just like with cloning, if you need to move a property that contains an object or array and want to ensure the original is completely removed from memory, you should clone it first: ```js // Move with cloning (for objects/arrays) msg.oldPayload = RED.util.cloneMessage(msg.payload); delete msg.payload; return msg; ``` This ensures that the moved property is independent and modifications to the new location won't affect any lingering references to the old location. ## Handling JSON Messages Working with [JSON](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON){rel=""nofollow""} is common in Node-RED, especially with APIs and IoT data. JSON (JavaScript Object Notation) is a lightweight data-exchange format. Two forms of JSON exist in Node-RED: 1. **JSON Object**: A structured JavaScript object that can be directly manipulated, access properties, modify values, pass through nodes seamlessly. 2. **JSON String**: A serialized JSON representation, commonly used when transmitting data between systems. Unlike objects, JSON strings cannot be directly manipulated as structured data. ### Converting JSON String to JSON Object To work with a JSON string as a JavaScript object, convert it using the **JSON** node. This node parses the string into a usable object structure. Steps: 1. Drag the **JSON** node onto the canvas. 2. Double-click to configure. 3. Set the action to "Always convert to JavaScript Object" and click Done. 4. Connect the JSON node between the source node (sending the JSON string) and the destination node (requiring the parsed object). The JSON node automatically converts incoming JSON strings into JavaScript objects. For more details, see the [JSON node documentation](https://flowfuse.com/docs/node-red/core-nodes/parsers/json/). ## Common Mistakes to Avoid Avoiding these pitfalls ensures smooth flow operation: ### 1. Adding Properties to Non-Object Types Attempting to add properties to primitive types (strings, numbers) causes errors. **Incorrect:** ```javascript msg.payload = 'stringValue'; msg.payload.newProperty = 'value'; // Error: Cannot add property to string return msg; ``` **Correct:** ```javascript msg.payload = {}; // Initialize as object msg.payload.newProperty = 'value'; return msg; ``` This commonly occurs when an Inject node sends `msg.payload` as a string or number, then a Change node attempts to add properties to it. :video{ariaLabel="Showing the common mistake: adding property to non-object" autoPlay="true" height="351" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Incorrect: Adding properties to a non-object type* :video{ariaLabel="Correct approach to prevent the error" autoPlay="true" height="348" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Correct: Initialize as object before adding properties* ### 2. Overwriting the Entire Message Object Accidentally replacing the entire `msg` object loses important properties like `_msgid`, `topic`, and custom metadata. **Incorrect:** ```javascript msg = { newProperty: 'value' }; // Destroys existing msg structure return msg; ``` **Correct:** ```javascript msg.newProperty = 'value'; // Preserves existing properties return msg; ``` ### 3. Returning Incorrect Data Types Node-RED expects function nodes to return message objects. Returning primitives breaks the flow. **Incorrect:** ```javascript return "some string"; // Error: Not a valid message ``` **Correct:** ```javascript msg.payload = "some string"; return msg; ``` ### 4. Forgetting to Return the Message In Function nodes, forgetting the return statement halts the flow at that node. **Incorrect:** ```javascript msg.payload = msg.payload * 2; // Missing return statement - flow stops here ``` **Correct:** ```javascript msg.payload = msg.payload * 2; return msg; // Flow continues ``` By mastering Node-RED message handling and avoiding common mistakes, you can build robust, efficient workflows. Understanding JSON conversion, message cloning, and proper property management ensures smooth data flow between nodes and maintainable, error-free applications. # Node-RED Port (localhost:1880) Node-RED runs on **port 1880** by default. This is the network port where Node-RED listens for connections. When you start Node-RED, you access the editor by opening `http://localhost:1880` in your browser. All your HTTP endpoints and the Node-RED interface are served through this port. ## Changing the Default Port There are times when you need to change the default port - perhaps 1880 is already in use by another application, or you're running multiple Node-RED instances, or you simply prefer a different port number. ### Temporary Port Change For a one-time port change, start Node-RED with the `--port` flag: ```bash node-red --port 8080 ``` Now access Node-RED at `http://localhost:8080`. This change only lasts for the current session. ### Permanent Port Change To make the port change permanent, edit your Node-RED settings file (`settings.js`) located in the Node-RED user directory (commonly `~/.node-red/settings.js` on Linux/macOS; on Windows it’s typically under your user profile at `.node-red\settings.js`). ```javascript uiPort: process.env.PORT || 8080, ``` Save the file and restart Node-RED. The new port setting will persist across all future sessions. ## Securing Your Node-RED Installation **Critical Security Note:** By default, Node-RED has no authentication or authorization. Anyone with network access to this port can view and modify your flows, access your data, and control your connected devices. Always secure Node-RED before exposing it to any network beyond your local development machine. ### Enable Authentication Add user authentication by editing your `settings.js` file: ```javascript adminAuth: { type: "credentials", users: [{ username: "admin", password: "$2b$08$...", // Generate with: node-red admin hash-pw permissions: "*" }] } ``` Generate a secure password hash by running: ```bash node-red admin hash-pw ``` Enter your desired password when prompted, then copy the generated hash into your settings file. > **Note**: adminAuth secures the editor/admin UI (and admin API), but it doesn’t protect http in endpoints by default. ### Firewall Protection Restrict network access using a firewall. For Linux users, this example allows only devices on your local network (192.168.1.x) to access Node-RED: ```bash sudo ufw allow from 192.168.1.0/24 to any port 1880 ``` Adjust the IP range to match your network configuration. ## Accessing Node-RED Remotely When you want to access your Node-RED instance from outside your local network. The manual approach is complex and requires ongoing maintenance. You would need - Configure port forwarding on your router - Set up and maintain SSL/TLS certificates for HTTPS - Configure proper authentication and authorization - Implement rate limiting and DDoS protection - Keep security patches up to date - Monitor suspicious access attempts - Manage firewall rules and security patches - And much more ### The Easy Way FlowFuse makes Node-RED production-ready with secure remote access built in. Access your instances from anywhere through HTTPS without router configuration or certificate management. The platform handles SSL certificates automatically, provides role-based access control, and maintains enterprise-grade security through encrypted communications and comprehensive audit logging. Device Agents connect your Node-RED instances to FlowFuse, enabling secure remote access immediately after registration. Your team connects from any location while FlowFuse manages infrastructure, security updates, and system reliability. Remote access works without port forwarding, certificate renewals, or manual security configuration. FlowFuse handles the complexity so you can focus on building flows. Learn more about [setting up FlowFuse for production Node-RED deployments](https://flowfuse.com/blog/2025/09/installing-node-red/). ## Troubleshooting Port Issues ### Port Already in Use If Node-RED fails to start with an error message like: ```text Error: listen EADDRINUSE: address already in use :::1880 ``` This means another application is already using port 1880. Either stop the application that's using the port, or run Node-RED on a different port. # How to Filter, Map, Sort, and Reduce Data in Node-RED Data transformation is at the heart of most Node-RED applications, whether you're processing IoT sensor readings, cleaning API responses, or preparing data for visualization. While you could write JavaScript functions to handle these operations, Node-RED's visual, low-code approach offers a more maintainable and accessible alternative that anyone on your team can understand and modify. This guide demonstrates how to perform four fundamental data operations, filtering, mapping, sorting, and reducing, using Node-RED's built-in nodes instead of custom code. Through a practical example of processing temperature sensor data, you'll learn to: - Transform data values (converting Kelvin to Celsius) - Filter datasets by specific criteria (selecting date ranges) - Sort data chronologically or by any field - Aggregate values to calculate metrics like averages The low-code techniques covered here not only accelerate development but also make your Node-RED flows more transparent and easier to maintain. When your entire team can read and modify data transformations visually, collaboration becomes simpler and debugging becomes faster. Let's explore why mastering these data operations matters and how Node-RED makes them straightforward. ## What is Low-Code Low-code is a software development approach that requires little to no coding to build applications and processes. Instead of using complex programming languages, you use visual interfaces with basic logic and drag-and-drop capabilities. > Low-code is not just about accelerating development; it’s about democratizing it. It’s about giving more people the ability to create solutions to business problems. > > *Charles Lamanna, Corporate Vice President of Business Applications & Platforms at Microsoft* For more details refer to the following articles: - [Why Low-Code is Better](https://flowfuse.com/blog/2024/03/low-code-is-better/). - [Why you need a low-code platform](https://flowfuse.com/blog/2024/05/why-you-need-a-low-code-platform/). ## Why do you need to learn to filter, map, sort, and reduce the data? Filter, map, sort, and reduce are essential functions in data processing because they efficiently transform, extract, organize, and aggregate data, that makes it easier to analyze and derive insights from datasets. For example, consider the scenario where you have an array of sensor data retrieved from an database. The data looks something like this: ```json [ { "timestamp": "2024-06-17T10:00:00Z", "temperature": 298.15 }, { "timestamp": "2024-06-17T11:00:00Z", "temperature": 299.15 }, { "timestamp": "2024-06-17T10:30:00Z", "temperature": 300.15 }, { "timestamp": "2024-06-17T10:15:00Z", "temperature": 301.15 }, { "timestamp": "2024-06-17T10:45:00Z", "temperature": 303.15 }, { "timestamp": "2024-06-18T09:00:00Z", "temperature": 297.15 }, { "timestamp": "2024-06-18T10:00:00Z", "temperature": 300.15 }, { "timestamp": "2024-06-18T11:00:00Z", "temperature": 301.15 }, { "timestamp": "2024-06-18T12:00:00Z", "temperature": 302.15 }, { "timestamp": "2024-06-19T10:00:00Z", "temperature": 298.15 }, { "timestamp": "2024-06-19T11:00:00Z", "temperature": 299.15 } ] ``` However, you've noticed that the temperature data is in Kelvin, but you need it in Celsius. Additionally, the data is not correctly ordered by timestamp, and you only need the data of June 17th. Finally, you want to calculate the average temperature for that day. Users who are not familiar with Node-RED basics can use a JavaScript function node to achieve this, as shown below: ::render-flow ```json [{"id":"306d455509a3747e","type":"inject","z":"977143edb097b685","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15},{\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15},{\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15},{\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15},{\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15}]","payloadType":"json","x":260,"y":200,"wires":[["c58e1653fe5511eb"]]},{"id":"c58e1653fe5511eb","type":"function","z":"977143edb097b685","name":"Filtering, mapping, reducing and sorting data with traditional coding","func":"let sensorData = msg.payload;\n\nconst filteredData = sensorData\n .filter(item => item.timestamp.startsWith(\"2024-06-17\"))\n .map(item => ({\n timestamp: item.timestamp,\n temperature: item.temperature - 273.15\n }));\n\nfilteredData.sort((a, b) => (a.timestamp > b.timestamp) ? 1 : ((b.timestamp > a.timestamp) ? -1 : 0));\n\nconst totalTemperature = filteredData.reduce((acc, entry) => acc + entry.temperature, 0);\nconst averageTemperature = totalTemperature / filteredData.length;\n\nmsg.payload = {\n sensorData: filteredData,\n averageTemperature: averageTemperature\n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":700,"y":200,"wires":[["827c7d2009eeb046"]]},{"id":"827c7d2009eeb046","type":"debug","z":"977143edb097b685","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1060,"y":200,"wires":[]}] ``` :: Using function nodes isn't wrong, but it adds complexity to your applications, for more information refer to the [Drawbacks of using Fuction nodes](https://flowfuse.com/blog/2023/03/why-should-you-use-node-red-function-nodes/#_5-benefits-of-avoiding-function-nodes) Article. Since not everyone on the team may be familiar with JavaScript, it can limit who can solve business problems. To keep the application flow simple, using a low-code approach to perform these operations is crucial. In the following sections, we'll explore how to perform these operations using a low-code approach. ## Mapping Mapping often refers to the process of applying a function to each item in a list, array, or other collection to produce a new collection of transformed items. here in our context, we need to covert the temperature data of each object from kelvin to celsius. To perform mapping we will use the Split, Change, and Join nodes. 1. Drag a Split node onto the canvas, the Split node will Split a message into a sequence of messages which will allow us to operate on each message, additioanlly split node bind the metadata to each of the object splitted, this metadata will helps join node to merge the all of message sequence back to an array. 2. Drag a Change node onto the canvas, set the `msg.payload.temperature` to `payload.temperature - 273.15` as JSONata expression. !["Screenshot of the change node converting temperature kelvin data from celsius"](https://flowfuse.com/docs/node-red/getting-started/images/filtering-mapping-sorting-reducing-data-with-node-red-change-node.png "Screenshot of the change node converting temperature kelvin data from celsius"){dataZoomable=""} 3. Now drag the Join node onto the canvas and set the Mode to "Automatic". This will automatically join all the messages originating from the Split node into an array. !["Screenshot join node creating new array by combining message sequnce"](https://flowfuse.com/docs/node-red/getting-started/images/filtering-mapping-sorting-reducing-data-with-node-red-join-node-combining-node.png "Screenshot join node creating new array by combining message sequnce"){dataZoomable=""} ::render-flow ```json [{"id":"306d455509a3747e","type":"inject","z":"977143edb097b685","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[ {\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15}, {\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15}, {\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15}, {\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15}, {\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15}, {\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15}, {\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15}, {\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15}, {\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15}, {\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15}, {\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15} ]","payloadType":"json","x":500,"y":540,"wires":[["9d9c0688468e1aae"]]},{"id":"9d9c0688468e1aae","type":"split","z":"977143edb097b685","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":860,"y":540,"wires":[["785125a70fbdc554"]]},{"id":"785125a70fbdc554","type":"change","z":"977143edb097b685","name":"Converting the temperature data from kelvin to celsius","rules":[{"t":"set","p":"payload.temperature","pt":"msg","to":"payload.temperature - 273.15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1340,"y":540,"wires":[["9cd6e05b88ec26fd"]]},{"id":"9cd6e05b88ec26fd","type":"join","z":"977143edb097b685","name":"Creating new array by combining message sequence","mode":"auto","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1820,"y":540,"wires":[["244660d81bf5e5b2"]]},{"id":"244660d81bf5e5b2","type":"debug","z":"977143edb097b685","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2140,"y":540,"wires":[]}] ``` :: ## Filtering Filtering is the process of selecting specific items from an array to create a new array. In Node-RED, filtering is achieved using mapping and condition-based routing. Now we are familiar with mapping and have done it above, so we need to use only one more extra node which is the switch node for condition-based routing. 1. Drag a switch node and place it after the Change node and before the Join node. 2. Set the condition to check whether `msg.payload.timestamp` includes '2024-06-17' This condition ensures that only messages containing the specified date in their timestamp are sent further. 3. Next, In the switch node checked the option "recreate message sequences" that will repair the `msg.parts` metadata added by Split node if any messages are dropped by the switch node. !["Screenshot of switch node filtering data bases on timestamp"](https://flowfuse.com/docs/node-red/getting-started/images/filtering-mapping-sorting-reducing-switch-node.png "Screenshot switch node filtering data bases on timestamp"){dataZoomable=""} ::render-flow ```json [{"id":"4e76a2328451b4c3","type":"inject","z":"977143edb097b685","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15},{\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15},{\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15},{\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15},{\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15}]","payloadType":"json","x":380,"y":480,"wires":[["f4e07a31f505a50c"]]},{"id":"f4e07a31f505a50c","type":"split","z":"977143edb097b685","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":680,"y":480,"wires":[["fcd6a0a1497203a9"]]},{"id":"bed1a7d861fa9e3d","type":"join","z":"977143edb097b685","name":"Creating new array by combining message sequence","mode":"auto","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"0","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":1940,"y":480,"wires":[["2449811ec79bc220"]]},{"id":"2449811ec79bc220","type":"debug","z":"977143edb097b685","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2240,"y":480,"wires":[]},{"id":"7797594a508cfb46","type":"switch","z":"977143edb097b685","name":"Routing message sequence based on condition","property":"payload.timestamp","propertyType":"msg","rules":[{"t":"cont","v":"2024-06-17","vt":"str"}],"checkall":"true","repair":true,"outputs":1,"x":1520,"y":480,"wires":[["bed1a7d861fa9e3d"]]},{"id":"fcd6a0a1497203a9","type":"change","z":"977143edb097b685","name":"Converting the temperature data from kelvin to celsius","rules":[{"t":"set","p":"payload.temperature","pt":"msg","to":"payload.temperature - 273.15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1100,"y":480,"wires":[["7797594a508cfb46"]]}] ``` :: ## Sorting Sorting, as the name suggests, means arranging items in a specific order. This order can be ascending (smallest to largest), descending (largest to smallest), or based on any defined criteria. In the Node-RED you can sort the numbers, alphabets, arrays, strings, and more. To perform sorting, we have to use the Node-RED Sort Node. 1. Drag the Sort node on the canvas. 2. Set the key to `timestamp` as the JSONata expression and then set the order to 'ascending'. We set the key to timestamp because we want to sort the data based on the timestamp. You can set it to temperature if you want to sort based on that instead. !["Screenshot of sort node sorting data in ascending order based on timestamp"](https://flowfuse.com/docs/node-red/getting-started/images/filtering-mapping-sorting-reducing-data-with-node-red-sort-node.png "Screenshot of sort node sorting data in ascending order based on timestamp"){dataZoomable=""} ::render-flow ```json [{"id":"8b66990baca45f2d","type":"inject","z":"977143edb097b685","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15},{\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15},{\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15},{\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15},{\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15}]","payloadType":"json","x":400,"y":540,"wires":[["f0d48b57cbd10fdd"]]},{"id":"f0d48b57cbd10fdd","type":"split","z":"977143edb097b685","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":740,"y":540,"wires":[["fdc21a267f7583e7"]]},{"id":"fdc21a267f7583e7","type":"change","z":"977143edb097b685","name":"Correcting the temperature property","rules":[{"t":"set","p":"payload.temperature","pt":"msg","to":"payload.temperature - 273.15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1080,"y":540,"wires":[["7d4ce2d4e21cd914"]]},{"id":"c273215c8c9cebee","type":"join","z":"977143edb097b685","name":"Creating new array by combining message sequence","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":1860,"y":540,"wires":[["c423efbd0581367a"]]},{"id":"e70d15ced7405755","type":"debug","z":"977143edb097b685","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2480,"y":540,"wires":[]},{"id":"7d4ce2d4e21cd914","type":"switch","z":"977143edb097b685","name":"Routing message sequence based on condition","property":"payload.timestamp","propertyType":"msg","rules":[{"t":"cont","v":"2024-06-17","vt":"str"}],"checkall":"true","repair":true,"outputs":1,"x":1440,"y":540,"wires":[["c273215c8c9cebee"]]},{"id":"c423efbd0581367a","type":"sort","z":"977143edb097b685","name":"Sorting data based on timestamp","order":"ascending","as_num":false,"target":"payload","targetType":"msg","msgKey":"timestamp","msgKeyType":"jsonata","seqKey":"payload.timestamp","seqKeyType":"jsonata","x":2240,"y":540,"wires":[["e70d15ced7405755"]]}] ``` :: ## Reducing Reducing refers to the process of combining elements of a data structure (such as an array) into a single value. It involves iterating over the elements of the data structure and applying a combining function repeatedly until all elements have been processed. 1. Drag another Split node onto the canvas 2. Drag Join another node onto the canvas. 3. Select the mode to "reduce sequence", set Reduce exp to `$A+ payload.temperature`, initial value to 0 and the Fix-up exp to `$A/$N` !["Screenshot of join node calculating average of the temperature"](https://flowfuse.com/docs/node-red/getting-started/images/filtering-mapping-sorting-reducing-data-with-node-red-join-node-calculating-avg.png "Screenshot of join node calculating average of the temperature"){dataZoomable=""} In this configuration, the Join node is set to reduce sequence mode. The initial value of the accumulator ( [[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[A]{.mord.mathnormal}[)]{.mclose}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ini]{.mord.mathnormal}[t]{.mord.mathnormal}[ia]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[i]{.mord.mathnormal}[z]{.mord.mathnormal style="margin-right:0.044em;"}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[0.]{.mord}[A]{.mord.mathnormal}[se]{.mord.mathnormal}[a]{.mord.mathnormal}[c]{.mord.mathnormal}[hm]{.mord.mathnormal}[ess]{.mord.mathnormal}[a]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[e]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[ocesse]{.mord.mathnormal}[d]{.mord.mathnormal}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[t]{.mord.mathnormal}[h]{.mord.mathnormal}[ec]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[tt]{.mord.mathnormal}[e]{.mord.mathnormal}[m]{.mord.mathnormal}[p]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[(]{.mopen}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[y]{.mord.mathnormal style="margin-right:0.0359em;"}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[o]{.mord.mathnormal}[a]{.mord.mathnormal}[d]{.mord.mathnormal}[.]{.mord}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[m]{.mord.mathnormal}[p]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[)]{.mclose}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[a]{.mord.mathnormal}[dd]{.mord.mathnormal}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[t]{.mord.mathnormal}[o]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex} A. Once all messages have been processed, the accumulated sum [[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[A]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[v]{.mord.mathnormal style="margin-right:0.0359em;"}[i]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[b]{.mord.mathnormal}[y]{.mord.mathnormal style="margin-right:0.0359em;"}[t]{.mord.mathnormal}[h]{.mord.mathnormal}[e]{.mord.mathnormal}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[t]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[n]{.mord.mathnormal}[u]{.mord.mathnormal}[mb]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[o]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[m]{.mord.mathnormal}[ess]{.mord.mathnormal}[a]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[es]{.mord.mathnormal}[(]{.mopen}]{.base}]{.katex-html ariaHidden="true"}]{.katex} N) to compute the average temperature. ::render-flow ```json [{"id":"5ff0902202c21e85","type":"inject","z":"977143edb097b685","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15},{\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15},{\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15},{\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15},{\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15}]","payloadType":"json","x":580,"y":620,"wires":[["993ffc096c3e8089"]]},{"id":"993ffc096c3e8089","type":"split","z":"977143edb097b685","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":900,"y":620,"wires":[["3bbb68c2dc2a0f5c"]]},{"id":"3bbb68c2dc2a0f5c","type":"change","z":"977143edb097b685","name":"Correcting the temperature property","rules":[{"t":"set","p":"payload.temperature","pt":"msg","to":"payload.temperature - 273.15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1280,"y":620,"wires":[["580210c585730f97"]]},{"id":"01e7066b3ff012e7","type":"join","z":"977143edb097b685","name":"Creating new array by combining message sequence","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":2060,"y":620,"wires":[["27cc5d5e90f7facd","6116c1efc3f7f682"]]},{"id":"580210c585730f97","type":"switch","z":"977143edb097b685","name":"Routing message sequence based on condition","property":"payload.timestamp","propertyType":"msg","rules":[{"t":"cont","v":"2024-06-17","vt":"str"}],"checkall":"true","repair":true,"outputs":1,"x":1640,"y":620,"wires":[["01e7066b3ff012e7"]]},{"id":"27cc5d5e90f7facd","type":"sort","z":"977143edb097b685","name":"Sorting data based on timestamp","order":"ascending","as_num":false,"target":"payload","targetType":"msg","msgKey":"timestamp","msgKeyType":"jsonata","seqKey":"payload.timestamp","seqKeyType":"jsonata","x":2440,"y":620,"wires":[["f1f93a7b4575daf1"]]},{"id":"362ec9c482688cf6","type":"debug","z":"977143edb097b685","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":3140,"y":740,"wires":[]},{"id":"b9f2f83a330140ca","type":"join","z":"977143edb097b685","name":"Calculating the the average of temperature","mode":"reduce","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"","reduceRight":false,"reduceExp":"$A+ payload.temperature","reduceInit":"0","reduceInitType":"num","reduceFixup":"$A/$N","x":2890,"y":740,"wires":[["362ec9c482688cf6"]]},{"id":"f1f93a7b4575daf1","type":"debug","z":"977143edb097b685","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2720,"y":620,"wires":[]},{"id":"6116c1efc3f7f682","type":"split","z":"977143edb097b685","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":2460,"y":740,"wires":[["b9f2f83a330140ca"]]}] ``` :: # How to Debug Node-RED Flows Using Debugger When it comes to debugging application flows in Node-RED, the tool most Node-RED developers often reach for is the [Debug](https://flowfuse.com/docs/node-red/core-nodes/common/debug/) node. It provides a simple way to output message payloads or other data to the debug sidebar, helping you gain insights into how your flow is working. But what if you needed more control and visibility over the flow’s execution? What if you wanted to step through each node in detail, inspect variables, or pause the flow at specific points to understand what’s happening? In these cases, using the **Node-RED Debugger** becomes invaluable. The debugger allows you to trace the execution of your flows interactively, set breakpoints, and gain deeper insights beyond what the Debug node offers. This Documentation will show you how to effectively use the Node-RED Debugger to pinpoint issues and fine-tune your applications. > **Tip:** If your flows aren’t named or formatted clearly, making them hard to understand, you can use **[FlowFuse Expert](https://flowfuse.com/docs/user/expert/)** to analyze, debug, and build your flows faster with AI-powered features, including code completion, Function builders, templates, and more. ## What is Debugging, and Why is it crucial in Node-RED Flows? Debugging is finding and fixing issues in your code or workflow. In Node-RED, debugging helps you understand how your flows function by providing insights into the data being processed and identifying where things might go wrong. Typically, developers use the **Debug** node to output message payloads and view them in the sidebar. While this is useful for simple debugging, it can be limiting when you need to troubleshoot more complex scenarios. As flows become larger and more interconnected, pinpointing the exact source of an issue using just a Debug node can be like searching for a needle in a haystack. That’s where the **Node-RED Debugger** steps in, offering a more granular approach to debugging. The debugger allows you to: - **Manual stop**: to manually stop the runtime and execution of the flow - **Step through** the execution of nodes one by one. - **Set breakpoints** to pause the flow at critical points. - **Inspect messages** and data in real-time, including message payloads, context, and more. ## Installing and Enabling Node-RED Debugger To install the Node-RED Debugger: 1. Click the menu icon in the top-right corner. 2. Select **Manage palette** and switch to the **Install** tab. 3. Search for [node-red-debugger](https://flows.nodered.org/node/node-red-debugger){rel=""nofollow""}. 4. Click **Install** to add the package. ### Enabling the Debugger ![Image showing the option to turn the debugger on and off in the sidebar](https://flowfuse.com/docs/node-red/getting-started/images/disable-enable-button.png){dataZoomable=""}*Image showing the option to turn the debugger on and off in the sidebar* Once installed, open the debugger tab in the sidebar by clicking the collapsible arrow icon in the right sidebar and selecting **Flow Debugger**. In the new Debugger tab, toggle the switch at the sidebar's top-left corner to enable the debugger. By default, it is disabled, so enable it before proceeding further. ## Using the Debugger for Debugging Flows To illustrate how to use the Node-RED Debugger effectively, let’s consider a flow that simulates sensor data processing. The flow consists of an Inject node that sends a set of simulated sensor data, including temperature readings in Kelvin and their corresponding dates. The subsequent nodes perform the following operations: 1. Convert the temperature from Kelvin to Celsius. 2. Filter the data to forward specific date entries. 3. Create a new array from the filtered results. 4. Split the array and calculate the average temperature. ::render-flow ```json [{"id":"3c012808d6b397e2","type":"group","z":"9cf82b68bb89e8ce","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["5ff0902202c21e85","993ffc096c3e8089","3bbb68c2dc2a0f5c","580210c585730f97","362ec9c482688cf6","b9f2f83a330140ca","6116c1efc3f7f682","01e7066b3ff012e7"],"x":394,"y":1899,"w":532,"h":642},{"id":"5ff0902202c21e85","type":"inject","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Inject the sample data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"timestamp\":\"2024-06-17T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-17T11:00:00Z\",\"temperature\":299.15},{\"timestamp\":\"2024-06-17T10:30:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-17T10:15:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-17T10:45:00Z\",\"temperature\":303.15},{\"timestamp\":\"2024-06-18T09:00:00Z\",\"temperature\":297.15},{\"timestamp\":\"2024-06-18T10:00:00Z\",\"temperature\":300.15},{\"timestamp\":\"2024-06-18T11:00:00Z\",\"temperature\":301.15},{\"timestamp\":\"2024-06-18T12:00:00Z\",\"temperature\":302.15},{\"timestamp\":\"2024-06-19T10:00:00Z\",\"temperature\":298.15},{\"timestamp\":\"2024-06-19T11:00:00Z\",\"temperature\":299.15}]","payloadType":"json","x":540,"y":1940,"wires":[["993ffc096c3e8089"]]},{"id":"993ffc096c3e8089","type":"split","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":640,"y":2020,"wires":[["3bbb68c2dc2a0f5c"]]},{"id":"3bbb68c2dc2a0f5c","type":"change","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Kelvin to celcius","rules":[{"t":"set","p":"payload.temperature","pt":"msg","to":"payload.temperature - 273.15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":2100,"wires":[["580210c585730f97"]]},{"id":"580210c585730f97","type":"switch","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Routing message sequence based on condition","property":"payload.timestamp","propertyType":"msg","rules":[{"t":"cont","v":"2024-06-17","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":660,"y":2180,"wires":[["01e7066b3ff012e7"]]},{"id":"362ec9c482688cf6","type":"debug","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":2500,"wires":[]},{"id":"b9f2f83a330140ca","type":"join","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Calculating the  the average of temperature","mode":"reduce","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":true,"accumulate":true,"timeout":"","count":"","reduceRight":false,"reduceExp":"$A+ payload.temperature","reduceInit":"0","reduceInitType":"num","reduceFixup":"$A/$N","x":690,"y":2400,"wires":[["362ec9c482688cf6"]]},{"id":"6116c1efc3f7f682","type":"split","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Splits a message into a sequence of messages.","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":700,"y":2340,"wires":[["b9f2f83a330140ca"]]},{"id":"01e7066b3ff012e7","type":"join","z":"9cf82b68bb89e8ce","g":"3c012808d6b397e2","name":"Creating new array by combining message sequence","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":true,"accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":700,"y":2260,"wires":[["6116c1efc3f7f682"]]}] ``` :: However, clicking the **Inject** node once does not produce the expected results; instead, it requires clicking again to get the output. This indicates that there might be a timing issue or a logic flaw in the flow that prevents it from processing correctly on the first click. Let's debug the flow with a debugger now. ### Understanding the Debugger sidebar tab Before proceeding further, let's first understand the Debugger tab and its different sections. The Debugger tab contains two main areas: **Breakpoints** and **Messages**. ![Image showing the breakpoint section in the sidebar](https://flowfuse.com/docs/node-red/getting-started/images/breakpoints-section.png){dataZoomable=""}*Image showing the breakpoint section in the sidebar* 1. **Breakpoints**: This section lists all the breakpoints you have set within your flow. It allows you to manage and navigate through the breakpoints effectively. ![Image showing the messages section in the sidebar](https://flowfuse.com/docs/node-red/getting-started/images/message-section.png){dataZoomable=""}*Image showing the messages section in the sidebar* 2. **Messages**: This section shows any messages currently queued up in the runtime, giving you visibility into the data being processed at various stages of your flow. ![Image showing the controls in the sidebar](https://flowfuse.com/docs/node-red/getting-started/images/debugger-controls.png){dataZoomable=""}*Image showing the controls in the sidebar* At the top of the Debugger tab, you will find controls to stop the runtime manually and buttons to resume execution and step through the flow one input or output at a time when it is paused. ### Pausing the Runtime Manually and Navigating Through Each Step Now, let's diagnose the flow. We’ll manually pause the runtime, then step through each part of the flow using the debugger controls, observing the changes at each step. :video{ariaLabel="Video shows the execution of flow while debugger enabled and how to proceed to subsequent execution" autoPlay="true" height="351" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Video showing the execution of flow while debugger enabled and how to proceed to subsequent execution* Follow these steps: 1. Go to the **Debugger** tab in the sidebar. 2. Click the **Pause** button in the top-right corner to halt the runtime. 3. Next, click the **Inject** button to start the execution of the flow. 4. Once paused, you'll notice that the flow executes step by step, depending on the total inputs, outputs, and number of messages they produce and the message length. Each message will be printed in the **Messages** section of the debugger tab. At the top of each message, the name of the node that generated it will be displayed. 5. To proceed, click the **step forward** button (represented as an array icon next to the pause button). As you move forward, the **Messages** field will update with the message sent by each node, and the execution will also resume at the next step. Additionally, the input/output of the node sending the message will be highlighted in the flow with a light-bordered rectangle. 6. As we progress through the execution, everything works fine up to the **Switch** node, where the message passes through correctly. However, when you reach the **Join** node, the highlighted box does not move forward, and no message is printed in the debugger tab. This indicates the issue lies between the **Switch** node and the **Join** node. Manually stepping through the flow is useful for understanding how the flow operates, making it easier to identify where breakpoints should be placed effectively. ## Adding Breakpoints for Debugging Flows Now that we've pinpointed the problem to be somewhere between the[Switch](https://flowfuse.com/docs/node-red/core-nodes/function/switch/) node and the [Join](https://flowfuse.com/docs/node-red/core-nodes/sequence/join/) node, it’s time to leverage breakpoints for a more efficient debugging experience. These breakpoints allow you to pause the flow automatically allowing you to inspect messages and context without having to step through each node manually. This is especially useful for larger or more intricate flows. First, let’s discuss where exactly we should add breakpoints. Our previous debugging shows that all 11 messages are correctly reaching the input of the Switch node. However, we need to check how many messages pass through the Switch node's condition and whether they contain the required part object for the Join node to create a single value (array). To do this, we should add breakpoints at the output of the Switch node to monitor how many messages pass through, as well as at the input and output of the Join node. This will help us determine how many messages are reaching the input of the Join node and whether they contain the part object necessary for the Join node to automatically convert them into an array of those objects. :video{ariaLabel="Video showing how to add breakpoints" autoPlay="true" height="469" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Video showing how to add breakpoints* To add a breakpoint: 1. In the flow, find the node where you want to add the breakpoint. 2. Hover over the input or output of the desired node; a dotted rectangle will appear. 3. Click within that rectangle to add the breakpoint. It will turn solid blue, indicating that your breakpoint has been added. 4. The breakpoint will appear in the debugger sidebar tab list once added. ### Debugging: Pinpointing the Exact Problem and Solving the Issue in the Flow :video{ariaLabel="Video showing the execution of the flow with added breakpoints, indicating the number of each input/output being sent and received for debugging." autoPlay="true" height="458" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Video showing the execution of the flow with added breakpoints, indicating the number of each input/output being sent and received for debugging.* Start by clicking the inject node to trigger execution, which will pause at the output of the switch node. Check the blue rectangle to see how many messages have passed through; it shows only a few, not 11, indicating that only those messages met the condition. As you proceed, you will see those messages also reaching the input of the join node correctly. Next, look in the debugger tab's messages section to verify if these messages have the `parts` property, noting the value of `count`. You will see that the count value is 11, which means the join node is waiting for all 11 messages to create a single message; otherwise, it will not send anything. Click the arrow button to see how many messages reach the output of the join node; you’ll notice that nothing reaches the output, indicating that the join node is still waiting for the remaining messages. This is likely due to an issue with the `parts.count` property. While the split node previously set the count to 11 automatically, which is correct, the switch node filtered some messages, resulting in only a few passing through. Therefore, the count should be corrected to reflect the correct number of messages that passed through the switch node instead of 11. ## Disabling and Removing Breakpoints Now that you’ve learned how to add breakpoints and pinpoint problems, lets look at how to manage them. Sometimes you may need to disable specific breakpoints to allow the flow to run without interruption, or you may want to remove them once you’ve finished debugging. ### Disabling Breakpoints :video{ariaLabel="Video showing two ways of disabling breakpoints" autoPlay="true" height="415" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Video showing two ways of disabling breakpoints* To disable a breakpoint without removing it: 1. Go to the **Debugger** tab in the sidebar. 2. Locate the list of active breakpoints; you will see a checkbox on the left side for each breakpoint. 3. Click the checkbox for the breakpoint you wish to disable. This will toggle its state and no longer pause execution when reached. 4. Alternatively, locate the breakpoints you have added to the flow. Click on the breakpoint once, and it will turn into a transparent blue rectangle with a border, indicating it is disabled. 5. To enable them again, click on the checkbox or the breakpoints again. ### Removing Breakpoints :video{ariaLabel="Video showing two ways of removing breakpoints" autoPlay="true" height="474" loop="true" muted="true" playsInline="true" preload="none" width="800"}*Video showing two ways of removing breakpoints* To remove a breakpoint: 1. In the **Debugger** tab, find the breakpoint you want to remove. 2. Click the **x** button located to the right of the breakpoint. 3. Alternatively, locate the breakpoint on the flow and click it twice until it is a transparent rectangle with a dotted border, indicating it is removed. In conclusion, debugging in Node-RED is a great way to verify and improve your flows. While the Debug node is excellent for quick insights, the Node-RED Debugger adds another level of insight. Setting breakpoints can significantly streamline your troubleshooting process and help you identify issues more effectively. ## Up Next - [Monitoring and Optimizing Node-RED Flows with Open Telemetry](https://flowfuse.com/blog/2024/08/opentelemetry-with-node-red/): Learn how to Monitor and Optimize Node-RED Flows using Open Telemetry that will help you spot and fix delays in your flows quickly. - [Format your Node-RED flows for better team collaboration](https://flowfuse.com/blog/2022/12/node-red-flow-best-practice/):  Learn how to format your flows for readability to providing explicit comments on nodes and groups, a little bit of effort upfront can save your team many headaches down the road. # How to Use If-Else Logic in Node-RED Human decision-making is often guided by a series of "if this, then that" choices, whether it's deciding what to wear based on the weather or determining the quickest route to work depending on traffic. This kind of logic is equally crucial in systems, especially those built in Node-RED. Just as we make decisions based on various factors, systems must evaluate conditions and choose the appropriate course of action. When developing automated solutions in Node-RED, the ability to replicate this human-like decision-making process is essential. By implementing If-Else logic, your system can intelligently navigate different scenarios, adapting its behavior based on the inputs it receives. This guide will show you how to effectively incorporate If-Else logic into your Node-RED flows, ensuring your system can make smart, context-aware decisions, just like you would. ## Understanding If-Else Logic The concept of If-Else logic emerged from the need for computers to make decisions. As programming languages developed, guiding a computer through different actions based on varying conditions became essential. This led to the creation of conditional statements, which allow programs to choose different paths depending on specific criteria. ### What is If-Else Logic? If-Else logic is a way for programs to make decisions. It works like this: - **If** a particular condition is true (e.g., "Is the temperature above 30°C?"), then execute a set of actions (e.g., "Turn on the air conditioner"). - **Else** (if the condition is not true), execute a different set of actions (e.g., "Turn off the air conditioner"). This approach allows systems to respond appropriately to different situations. ## Implementing Conditional Flows in Node-RED: A Practical Walkthrough In Node-RED, implementing If-Else logic allows you to create dynamic and responsive flows that react to different inputs and conditions. Whether you're automating a smart home, managing IoT devices, or developing complex workflows, mastering conditional logic is essential for creating intelligent systems. To implement If-Else logic in Node-RED, you can use the Switch node, which aligns perfectly with Node-RED's low-code approach. However, another way to achieve this is using the Function node, which offers more flexibility and control when writing custom JavaScript logic. ### Using Switch Node The [Switch](https://flowfuse.com/docs/node-red/core-nodes/function/switch/) node in Node-RED is used for routing messages based on specific conditions, offering a straightforward, low-code approach to implementing conditional logic in your flows. The Switch node allows you to set up rules using a visual interface, making it ideal for users who prefer a more intuitive method for handling conditions. However, it’s important to note that the Switch node represents a different, independent concept known as the "[switch statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch){rel=""nofollow""}." While it serves a similar purpose to If-Else logic by building conditional flows, it operates under its own programming paradigm. To demonstrate the Switch node, we'll set up a flow to make decisions based on the temperature value. We will route messages through different outputs based on temperature thresholds. 1. Drag the inject node onto the canvas and set the `msg.payload` to `$random() * 100` as JSONata expression; this inject node will simulate a temperature sensor by generating a random number. 2. Drag a Switch node onto the canvas. Double-click on it and set Property to `msg.payload`. 3. To add rules, click the + Add button at the bottom left of the configuration panel. You will see a prompt to select the condition and a prompt to enter the value to compare with. Add the following four rules and set it for "checking all rules": - Rule 1: `msg.payload > 30` - Rule 2: `msg.payload <= 30` - Rule 3: `msg.payload <= 20` - Rule 4: `msg.payload <= 10` 4. Now drag another Switch node and connect its input to the output of Switch nodes 2 and 3. We are adding a second switch node because we need to route messages based on ranges. A single Switch node doesn’t allow multiple checks in one rule, so we need to use another Switch node to route the temperature based on ranges. Add the following rules and set it for "stopping after the first match": - Rule 1: `msg.payload > 20` - Rule 2: `msg.payload > 10` 5. Now drag the Debug nodes and connect them to the Switch nodes' outputs according to our example. For messages greater than 30, connect the Debug node to the first output of the first Switch node. For the range between 30 to 20, connect the Debug node to the first output of the second Switch node. For the range between 20 to 10, connect the Debug node to the second output of the second Switch node. Finally, for messages less than 10, connect the Debug node to the fourth output of the first Switch node. 6. Deploy the flow by clicking the "Deploy" button in the top-right corner of the Node-RED editor. 7. Once deployed, click the button on the Inject node to trigger it. The Debug nodes will show the routed messages based on the temperature value. Notice how messages are routed through different outputs based on the temperature value. Now, you may ask how to update the message payload based on a condition. For that, you will need to use the Change node or the Function node. ```mermaid graph TD A[Inject Node: Random Temperature] --> B[Switch Node 1] B -->|msg.payload > 30| C[Output: Temperature > 30] B -->|msg.payload <= 30| D[Switch Node 2] B -->|msg.payload <= 20| D[Switch Node 2] B -->|msg.payload <= 10| E[Output: Temperature <= 10] D -->|msg.payload > 20| F[Output: 30 > Temperature > 20] D -->|msg.payload <= 20| G[Output: 20 > Temperature > 10] ``` *Node-RED flow using the Switch node to route messages based on temperature thresholds.* ::render-flow ```json [{"id":"b90722a28f81c014","type":"group","z":"9cf82b68bb89e8ce","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["dd0d3432c348e1f2","21bd53568877f3b5","5277e184faad375a","7fbf37916236a960","aac1f46169e431ab","3eb24a6129d8fa8e","36e51d42d34c9587"],"x":194,"y":1219,"w":892,"h":322},{"id":"dd0d3432c348e1f2","type":"debug","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"high temperature","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":1260,"wires":[]},{"id":"21bd53568877f3b5","type":"inject","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"Temperature","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random()*100","payloadType":"jsonata","x":310,"y":1380,"wires":[["5277e184faad375a"]]},{"id":"5277e184faad375a","type":"switch","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"","property":"payload","propertyType":"msg","rules":[{"t":"gt","v":"30","vt":"num"},{"t":"lte","v":"30","vt":"num"},{"t":"lte","v":"20","vt":"num"},{"t":"lte","v":"10","vt":"num"}],"checkall":"true","repair":false,"outputs":4,"x":530,"y":1380,"wires":[["dd0d3432c348e1f2"],["7fbf37916236a960"],["7fbf37916236a960"],["3eb24a6129d8fa8e"]]},{"id":"7fbf37916236a960","type":"switch","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"","property":"payload","propertyType":"msg","rules":[{"t":"gt","v":"20","vt":"num"},{"t":"gt","v":"10","vt":"num"}],"checkall":"false","repair":false,"outputs":2,"x":670,"y":1380,"wires":[["aac1f46169e431ab"],["36e51d42d34c9587"]]},{"id":"aac1f46169e431ab","type":"debug","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"for medium temperature","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":1340,"wires":[]},{"id":"3eb24a6129d8fa8e","type":"debug","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"for very low temperature","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":1500,"wires":[]},{"id":"36e51d42d34c9587","type":"debug","z":"9cf82b68bb89e8ce","g":"b90722a28f81c014","name":"for low temperature","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":910,"y":1420,"wires":[]}] ``` :: ### Using Function Node The [Function](https://flowfuse.com/docs/node-red/core-nodes/function/function/) node allows for more complex logic by writing JavaScript. It's suitable when you need more control, or multiple values must be checked together. For demonstration purposes, let's use the temperature example where we determine whether to turn the air conditioner on or off based on the temperature: 1. Drag the inject node onto the canvas and set the `msg.payload` to `$random() * 100` as JSONata expression; this inject node will simulate a temperature sensor by generating a random number. 2. Drag the function node onto the canvas, double-click on it, and paste the following code into it: ```javascript let Temperature = msg.payload; if (Temperature > 30) { msg.payload = "Turn on the air conditioner"; } else { msg.payload = "No action required"; } return msg; ``` Before moving further, let's pause and understand what’s happening in the code and how `msg.payload` is being used. In Node-RED, `msg.payload` is used to carry data through the flow. Initially, it holds the temperature value injected by the Inject node. The Function node then processes this value using If-Else logic. If the temperature exceeds 30°C, `msg.payload` is set to `"Turn on the air conditioner"`, indicating that the air conditioner should be turned on. If the temperature is 30°C or lower, `msg.payload` is set to `"No action required"`, signaling that the air conditioner should remain off. This updated `msg.payload` is then passed on to the next node, ensuring the system responds appropriately based on the temperature input. Many people need clarification on the messaging system in Node-RED. For a deeper understanding of how messaging works in Node-RED, I recommend going through this document: [Node-RED Messaging Guide](https://flowfuse.com/docs/node-red/getting-started/node-red-messages/). 3. Next, drag the Debug node onto the canvas and connect it to the output of the Function node. This will allow you to see the results of your conditional logic in the Node-RED debug window. 4. Deploy the flow by clicking the "Deploy" button in the top-right corner of the Node-RED editor. 5. Once deployed, click the button on the Inject node to trigger it. You should see the output of the Function node in the debug window, which will show true or false depending on the temperature value. ```mermaid flowchart TD A[Start] --> B{Is Temperature > 30?} B -- Yes --> C[Turn on the air conditioner] B -- No --> D[No action required] C --> E[End] D --> E[End] ``` ::render-flow ```json [{"id":"51ffa77e55eb7f63","type":"group","z":"9cf82b68bb89e8ce","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["19fa09374c1be7c4","ff41ef215d860c1a","773175cbe8014372"],"x":154,"y":2059,"w":672,"h":82},{"id":"19fa09374c1be7c4","type":"inject","z":"9cf82b68bb89e8ce","g":"51ffa77e55eb7f63","name":"Temperature","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random()*100","payloadType":"jsonata","x":270,"y":2100,"wires":[["ff41ef215d860c1a"]]},{"id":"ff41ef215d860c1a","type":"function","z":"9cf82b68bb89e8ce","g":"51ffa77e55eb7f63","name":"Temperature Threshold Check","func":"let Temperature = msg.payload;\n if (Temperature > 30) {\n msg.payload = \"Turn on the air conditioner\";\n } else {\n msg.payload = \"No action required\";\n }\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":2100,"wires":[["773175cbe8014372"]]},{"id":"773175cbe8014372","type":"debug","z":"9cf82b68bb89e8ce","g":"51ffa77e55eb7f63","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":2100,"wires":[]}] ``` :: *Node-RED flow using the Function node to implement simple If-Else logic for temperature control.* ### Handling Multiple Flows with Node-RED's Function Node We’ve seen how to handle a simple one-way flow using If-Else logic with a function node, but what if you need to direct messages along different paths based on various conditions or evaluate multiple values while using a function node? In such cases, the Function node in Node-RED provides the flexibility to write complete JavaScript code, enabling more complex decision-making. Additionally, the Function node supports setting it for multiple output ports, which allows you to route messages to different destinations based on various conditions. Let’s update our example to handle multiple values. In this scenario, we will incorporate both temperature and humidity into our decision-making process. We will use multiple output ports in the Function node to route messages based on different conditions. 1. Drag another inject node onto the canvas, set `msg.payload.temperature` to `$random() * 100` as the JSONata expression and `msg.payload.humidity` to `$random() * 100`. 2. Drag another function node onto the canvas, double-click on it, switch to the "Setup" tab, and increase the number of output ports to match the number of conditions you will handle. For our example, increase the number of outputs to 4 and click Done. ```javascript let Temperature = msg.payload.temperature; let Humidity = msg.payload.humidity; // Initialize output array let outputs = [null, null, null, null]; if (Temperature > 30 && Humidity < 40) { // High temperature and low humidity outputs[0] = { payload: "High temperature and low humidity: Turn on the air conditioner and use a humidifier" }; } else if (Temperature > 30 && Humidity >= 40) { // High temperature and high humidity outputs[1] = { payload: "High temperature and high humidity: Turn on the air conditioner" }; } else if (Temperature < 15 && Humidity < 40) { // Low temperature and low humidity outputs[2] = { payload: "Low temperature and low humidity: Turn on the heater and use a humidifier" }; } else if (Temperature < 15 && Humidity >= 40) { // Low temperature and high humidity outputs[3] = { payload: "Low temperature and high humidity: Turn on the heater" }; } return outputs; ``` Now, you will see that the Function node has four outputs, each corresponding to the sequence of conditions we have written. For example, the message for the first condition will appear at the first output of the Function node, the message for the second condition will appear at the second output, and so on. Regarding the outputs being sent, the Function node initializes an array with `null` values to ensure all outputs are accounted for. When a specific condition is met, the corresponding index in this array is updated with the desired message. For example, if the temperature is high and the humidity is low, the message will be set at `outputs[0]`, which is the first output. If no condition is met, the output remains `null`, meaning nothing is sent for that output, ensuring only the relevant outputs are populated with messages. 3. Next, drag four Debug nodes onto the canvas. Connect each Debug node to one of the outputs from the Function node. This setup will allow you to see the messages routed through each output in the Debug panel. 4. Deploy the flow by clicking the "Deploy" button in the top-right corner of the Node-RED editor. 5. Once deployed, click the button on the Inject node to trigger it. ```mermaid flowchart TD A[Start] --> B{Is Temperature > 30?} B -- Yes --> C{Is Humidity < 40?} C -- Yes --> D[Turn on the air conditioner and use a humidifier] C -- No --> E[Turn on the air conditioner] B -- No --> F{Is Temperature < 15?} F -- Yes --> G{Is Humidity < 40?} G -- Yes --> H[Turn on the heater and use a humidifier] G -- No --> I[Turn on the heater] F -- No --> J[No specific action required] D --> K[End] E --> K H --> K I --> K J --> K ``` *Node-RED flow using the Function node with multiple outputs for handling various conditions like temperature and humidity.* ::render-flow ```json [{"id":"301a31b0972b0b20","type":"group","z":"9cf82b68bb89e8ce","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["dbd085ed607e41de","31d2e8f0c871143d","b169bf385ca85f6c","aa7ab6452f4a7791","cc3e6e2c70643f6f","800508e428d74f5f"],"x":114,"y":719,"w":832,"h":202},{"id":"dbd085ed607e41de","type":"inject","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Temperature && Humidity","props":[{"p":"payload.temperature","v":"$random() * 100","vt":"jsonata"},{"p":"payload.humidity","v":"$random() * 100","vt":"jsonata"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":820,"wires":[["31d2e8f0c871143d"]]},{"id":"31d2e8f0c871143d","type":"function","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Temp/Humidity Decision Engine","func":"let Temperature = msg.payload.temperature;\nlet Humidity = msg.payload.humidity;\n\n// Initialize output array\nlet outputs = [null, null, null, null];\n\nif (Temperature > 30 && Humidity < 40) {\n // High temperature and low humidity\n outputs[0] = { payload: \"High temperature and low humidity: Turn on the air conditioner and use a humidifier\" };\n} else if (Temperature > 30 && Humidity >= 40) {\n // High temperature and high humidity\n outputs[1] = { payload: \"High temperature and high humidity: Turn on the air conditioner\" };\n} else if (Temperature < 15 && Humidity < 40) {\n // Low temperature and low humidity\n outputs[2] = { payload: \"Low temperature and low humidity: Turn on the heater and use a humidifier\" };\n} else if (Temperature < 15 && Humidity >= 40) {\n // Low temperature and high humidity\n outputs[3] = { payload: \"Low temperature and high humidity: Turn on the heater\" };\n}\n\nreturn outputs;","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":820,"wires":[["b169bf385ca85f6c"],["aa7ab6452f4a7791"],["cc3e6e2c70643f6f"],["800508e428d74f5f"]]},{"id":"b169bf385ca85f6c","type":"debug","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Output 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":760,"wires":[]},{"id":"aa7ab6452f4a7791","type":"debug","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Output 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":800,"wires":[]},{"id":"cc3e6e2c70643f6f","type":"debug","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Output 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":840,"wires":[]},{"id":"800508e428d74f5f","type":"debug","z":"9cf82b68bb89e8ce","g":"301a31b0972b0b20","name":"Output 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":880,"wires":[]}] ``` :: ## Choosing Between the Function Node and Switch Node When deciding between the Function node and the Switch node in Node-RED, it is essential to consider the complexity of your logic and the nature of the message routing you require. The Function node excels in scenarios where complex logic and detailed message processing are necessary. It allows for writing custom JavaScript code, which can handle sophisticated conditions and perform calculations. This node is particularly useful when you need to make intricate decisions based on multiple values or when you need to perform detailed updates to the `msg` object. For instance, if your flow requires combining data from different sources, applying complex rules, or modifying multiple properties of `msg.payload`, the Function node offers the flexibility and power to accomplish these tasks. In contrast, the Switch node is designed for simpler, value-based routing. It is ideal for straightforward scenarios where you need to route messages based on a single value with multiple possible outputs. This node enables you to create rules based on specific values or conditions without the need for complex logic or extensive message modifications. If your routing logic involves basic comparisons and does not require advanced processing or calculations, the Switch node provides a more streamlined and intuitive approach. In summary, choose the Function node for intricate decision-making and detailed message processing, while the Switch node is better suited for scenarios where simple value-based routing is sufficient. # Node-RED Programming Learn the core programming concepts you'll use every day in Node-RED. This section covers the fundamental building blocks that will help you create more sophisticated and reliable flows. ## What's Covered Programming in Node-RED means working with flows, nodes, and messages. Even though you're working visually, you'll still need to understand key programming concepts like conditional logic, loops, and data manipulation. This section teaches you how to implement these concepts using Node-RED's visual approach. - [How to Debug Node-RED Flows Using Debugger](https://flowfuse.com/docs/node-red/getting-started/programming/debugging-flows/): Debug Node-RED flows using the Debugger. Learn to set breakpoints, step through execution, and inspect messages for efficient troubleshooting. - [How to Filter, Map, Sort, and Reduce Data in Node-RED](https://flowfuse.com/docs/node-red/getting-started/programming/data-tranformation/): Learn how to perform data transformation in Node-RED with a low-code approach. - [How to implement loops in Node-RED flows](https://flowfuse.com/docs/node-red/getting-started/programming/loop/): Learn how to implement while, for, and for...of loops in Node-RED with core and custom nodes for efficient data processing and automation. - [How to Use If-Else Logic in Node-RED](https://flowfuse.com/docs/node-red/getting-started/programming/if-else/): Learn how to implement If-Else logic in Node-RED with our step-by-step guide. Use Function and Switch nodes for dynamic, conditional flows. # How to implement loops in Node-RED flows Handling repetitive tasks is a common challenge in automation and data processing. Whether you need to iterate over large datasets, perform calculations, or execute operations based on conditions multiple times, using loops can significantly enhance efficiency and scalability. In this document, we’ll explore how to replicate different types of loops that are essential in various contexts. We’ll discuss their applications and provide examples to help you effectively implement them in Node-RED. ## What is a Loop? A [loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Loops_and_iteration){rel=""nofollow""} is a programming construct that allows you to execute a block of code repeatedly until a certain condition is met. Different types of loops are suited to different scenarios, depending on how and when you want the code to repeat: - **For Loop**: Executes a block of code a specific number of times. This is useful when you know in advance how many times you need to iterate, such as iterating through a range of numbers or items in a list. It's also known as a fixed number loop. - **While Loop**: Repeats a block of code as long as a specified condition is true. This type of loop is useful when you don’t know how many times the loop will need to run beforehand. The loop continues executing until the condition becomes false. - **For...of / ForEach Loop**: These loops are used to iterate over iterable objects such as arrays or maps. They allow you to access each element in a collection. The **for...of** loop is used specifically for iterables, while **forEach** is a method available on arrays that applies a function to each element. Each type of loop serves a different purpose and can be chosen based on the requirements of the task at hand. ## Implementing Loops in Node-RED In this section, we’ll explore how to implement loops in Node-RED. First, we’ll demonstrate how to achieve looping with core nodes. Then, we’ll show how to accomplish similar tasks using custom nodes. We’ll also cover some essential operations typically performed using loops, providing practical examples to enhance your Node-RED flows. ### Implementing Loops in Node-RED with Core Nodes #### While Loop To demonstrate a while loop in Node-RED, we’ll create a flow that appends random characters to a string until it contains the character "Z". This example will help you understand how to simulate a while loop using Node-RED's core nodes. 1. Drag an **Inject** node onto the canvas. This node will trigger the start of the loop. 2. Add a **Change** node to initialize the string variable. Configure it to set `msg.i` to an empty string (`""`). Connect the **Inject** node to this **Change** node. 3. Place a **Switch** node on the canvas. Set it to check if `msg.i` contains the character "Z" (`msg.i.includes('Z')`). Add an additional rule for when this condition is not met (`otherwise`). Connect the output of the **Change** node to the input of the **Switch** node. 4. Add a **Function**node to append a random uppercase letter to the string. Use the following JavaScript code: ```javascript msg.i += String.fromCharCode(65 + Math.floor(Math.random() * 26)); // Append a random uppercase letter return msg; ``` Connect this **Function** node to the second output of the **Switch** node (where the condition is `msg.i` does not contain "Z"). Then, connect the output of this **Function** node back to the input of the **Switch** node to repeat the process. 5. Drag a **Debug** node onto the canvas and connect it to the second output of the **Switch** node. This node will display the current value of `msg.i` in the debug panel. 6. Add another **Change** node to signal completion. Configure it to set `msg.payload` to `"completed"`. Connect this **Change** node to the first output of the **Switch** node (where `msg.i` contains "Z"), and then link it to another **Debug** node to show the completion message. The flow will continuously append random letters to the string and print the value in the debug panel until the string contains "Z". Once the condition is met, the flow will print a "completed" message and terminate the loop. ```mermaid graph TD A["Inject Node
Triggers the start of the loop"] --> B["Change Node
Initialize String
Sets msg.i = ' '"] B --> C["Switch Node
Check if msg.i contains 'Z'"] C -->|"msg.i contains 'Z'"| D["Change Node
Set msg.payload to 'completed'"] D --> E["Debug Node
Display Completion Message"] C -->|"msg.i does not contain 'Z'"| F["Function Node
Append Random Character
Sets msg.i += random uppercase letter"] F --> C F --> G["Debug Node
Display Current String"] ``` ::render-flow ```json [{"id":"e90dc2e50e40896c","type":"group","z":"a3aa840957f658c6","name":"While Loop","style":{"label":true},"nodes":["cf6ebf02d00eaca6","696d89aa4050cd13","0e2d9f0447cf6226","bb6074cdf4398cb6","e19304ab31199315","c467509c666ee400","06bb08e3f0d7888a"],"x":34,"y":139,"w":1092,"h":242},{"id":"cf6ebf02d00eaca6","type":"switch","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"Does the msg.i contains Z","property":"i","propertyType":"msg","rules":[{"t":"cont","v":"Z","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":590,"y":240,"wires":[["c467509c666ee400"],["0e2d9f0447cf6226","06bb08e3f0d7888a"]]},{"id":"696d89aa4050cd13","type":"debug","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"End","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1030,"y":180,"wires":[]},{"id":"0e2d9f0447cf6226","type":"debug","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"Output \"i\"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"i","targetType":"msg","statusVal":"","statusType":"auto","x":900,"y":260,"wires":[]},{"id":"bb6074cdf4398cb6","type":"inject","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"Start","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"[\"hello\",\"run\",\"why\"]","payloadType":"json","x":130,"y":240,"wires":[["e19304ab31199315"]]},{"id":"e19304ab31199315","type":"change","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"initilized i with empty string","rules":[{"t":"set","p":"i","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":240,"wires":[["cf6ebf02d00eaca6"]]},{"id":"c467509c666ee400","type":"change","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"if not Set payload to \"completed\"","rules":[{"t":"set","p":"payload","pt":"msg","to":"Completed","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":180,"wires":[["696d89aa4050cd13"]]},{"id":"06bb08e3f0d7888a","type":"function","z":"a3aa840957f658c6","g":"e90dc2e50e40896c","name":"Append Random Letter","func":"// add ASCII char from 32 to 126\nmsg.i += String.fromCharCode(Math.random()*26 + 65);\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":340,"wires":[["cf6ebf02d00eaca6"]]}] ``` :: #### For Loop In traditional programming, `for` loops iterate a set number of times based on an index or range, while `while` loops execute as long as a condition is `true`. In Node-RED, you can simulate a `for` loop by managing a counter with nodes to iterate through array elements. By incrementing an index variable, you can access each element and perform operations, effectively mimicking the behavior of a traditional `for` loop. Here’s how you can set up a `for` loop in Node-RED: 1. Drag an **Inject** node onto the canvas and set the `msg.payload` to `["foo","bar","foobar"]`. This node triggers the start of the loop and provides the array to process. 2. Add a **Change** node to initialize the loop counter. Configure it to set `msg.i` to `0`, and connect the **Inject** node to this **Change** node. 3. Next, drag a **Switch** node onto the canvas. Configure it to check if `msg.i` is equal to the array length (`msg.i == msg.payload.length`). Add an additional rule for when this condition is not met (`otherwise`). Connect the **Change** node to the **Switch** node. 4. Add another **Change** node to increment the counter. Configure it to set `msg.i` to `i + 1` using a JSONata expression. Connect this **Change** node to the output of the **Switch** node where the condition (`msg.i < msg.payload.length`) is true (second output). 5. To access and display the array elements, drag a **Change** node onto the canvas. Configure it to set `msg.payload` to `msg.payload[msg.i]`, accessing the array element at the current index. Connect this **Change** node to the **Switch** node's second output (`otherwise`). 6. Attach a **Debug** node to the output of this **Change** node to print the current array element. 7. For the final step, add another **Change** node to signal when the loop has completed. Configure it to set `msg.payload` to `completed`, and connect it to the first output of the **Switch** node where the loop condition is `msg.i == msg.payload.length`. Finally, attach a **Debug** node to display the completion message. This flow will iterate through the array, printing each element until all elements have been processed. Once the loop reaches the end of the array, it prints a "completed" message and terminates. ```mermaid graph TD A["Inject Node
Sets msg.payload to array"] --> B["Change Node
Initialize Counter
Sets msg.i = 0"] B --> C["Switch Node
Check if msg.i == msg.payload.length"] C -->|"msg.i < msg.payload.length"| D["Change Node
Increment Counter
Sets msg.i = msg.i + 1"] D --> E["Change Node
Access Element
Sets msg.payload = msg.payload[msg.i]"] E --> F["Debug Node
Display Current Element"] C -->|"msg.i == msg.payload.length"| G["Change Node
Loop Completion
Sets msg.payload = 'completed'"] G --> H["Debug Node
Display Completion Message"] ``` ::render-flow ```json [{"id":"3b546e0612673478","type":"group","z":"a3aa840957f658c6","name":"ForLoop","style":{"label":true},"nodes":["8ee16c1d06fca9fe","64a4a7329631d7ce","e67a34285a7e6979","e2074cf01ed451fd","811cedbe7cb89415","8bbccc2d10c36bfe","826d819a06810184","e7ebf7f66cda1370"],"x":14,"y":159,"w":1292,"h":242},{"id":"8ee16c1d06fca9fe","type":"switch","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"if msg.i == msg.payload.length","property":"i","propertyType":"msg","rules":[{"t":"eq","v":"payload.length","vt":"msg"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":550,"y":280,"wires":[["8bbccc2d10c36bfe"],["e7ebf7f66cda1370","826d819a06810184"]]},{"id":"64a4a7329631d7ce","type":"debug","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"End","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1210,"y":200,"wires":[]},{"id":"e67a34285a7e6979","type":"debug","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"Output \"i\"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1080,"y":340,"wires":[]},{"id":"e2074cf01ed451fd","type":"inject","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"Start","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"[\"hello\",\"run\",\"why\"]","payloadType":"json","x":110,"y":280,"wires":[["811cedbe7cb89415"]]},{"id":"811cedbe7cb89415","type":"change","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"initilized i with 0","rules":[{"t":"set","p":"i","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":280,"wires":[["8ee16c1d06fca9fe"]]},{"id":"8bbccc2d10c36bfe","type":"change","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"If 'i' equals array length, set payload to \"completed\".","rules":[{"t":"set","p":"payload","pt":"msg","to":"Completed","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":200,"wires":[["64a4a7329631d7ce"]]},{"id":"826d819a06810184","type":"change","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"Access array element with i","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[msg.i]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":340,"wires":[["e67a34285a7e6979"]]},{"id":"e7ebf7f66cda1370","type":"change","z":"a3aa840957f658c6","g":"3b546e0612673478","name":"increment i by 1","rules":[{"t":"set","p":"i","pt":"msg","to":"i+1","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":360,"wires":[["8ee16c1d06fca9fe"]]}] ``` :: #### For...of / ForEach Loop In traditional programming, `for...of` and `forEach` loops are commonly used to iterate through arrays or object properties, allowing for individual element processing. Since Node-RED doesn’t include these specific constructs, you can replicate their functionality by using a combination of nodes, particularly the **[Split](https://flowfuse.com/docs/node-red/core-nodes/sequence/split/)** and **[Join](https://flowfuse.com/docs/node-red/core-nodes/sequence/join/)** nodes. Here’s how you can replicate this functionality in Node-RED: 1. Drag an **Inject** node onto the canvas and set the `msg.payload` to `["foo", "bar", "foobar"]`. 2. Drag a **Split** node onto the canvas. Keep the default settings. If you are working with a string, ensure you select the appropriate delimiter for splitting. 3. Optionally, use a **Change** or **Function** node to perform operations on each element if needed. 4. Drag a **Debug** node onto the canvas and connect it to the **Split** node’s output. When you click the **Inject** button, the **Split** node will process each element of the array individually, and the **Debug** node will display each one in the debug window. To explore how you can map, sort, filter, and reduce data using this approach, check out our guide: [How to Filter, Map, Sort, and Reduce Data in Node-RED](https://flowfuse.com/docs/node-red/getting-started/programming/data-tranformation/). ```mermaid graph TD A["Inject Node"] --> B["Split Node"] B -->|Outputs Each Element Individually| C["Debug Node"] A["Inject Node\nSets msg.payload to array"] B["Split Node\nSplits array into individual elements"] C["Debug Node\nDisplays each element"] ``` ::render-flow ```json [{"id":"2e67b2739f364a71","type":"group","z":"a3aa840957f658c6","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["2add2705d262010d","40333c736844a0c1","3337ba024a363a15","2f7d2aab12b3b4e9"],"x":254,"y":219,"w":532,"h":162},{"id":"2add2705d262010d","type":"inject","z":"a3aa840957f658c6","g":"2e67b2739f364a71","name":"Array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[\"foo\",\"bar\",\"foobar\"]","payloadType":"json","x":350,"y":340,"wires":[["40333c736844a0c1"]]},{"id":"40333c736844a0c1","type":"split","z":"a3aa840957f658c6","g":"2e67b2739f364a71","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":530,"y":320,"wires":[["3337ba024a363a15"]]},{"id":"3337ba024a363a15","type":"debug","z":"a3aa840957f658c6","g":"2e67b2739f364a71","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":690,"y":320,"wires":[]},{"id":"2f7d2aab12b3b4e9","type":"inject","z":"a3aa840957f658c6","g":"2e67b2739f364a71","name":"Object","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"first\":\"Hello World\",\"second\":8,\"third\":true}","payloadType":"json","x":350,"y":260,"wires":[["40333c736844a0c1"]]}] ``` :: #### Implementing Loops with the Function Node Implementing loops with the **[Function](https://flowfuse.com/docs/node-red/core-nodes/function/function/)** node is straightforward if you're familiar with JavaScript, as it allows you to write custom code. However, a common issue is figuring out how to send a message on each iteration without ending the loop after the first iteration. In this section, we’ll show you how to implement loops in the `Function` node correctly, ensuring that each iteration is processed and sent out properly without prematurely breaking the loop. For demonstration purposes, we will implement a `for` loop. 1. Drag the **Inject** node onto the canvas and set the `msg.payload` to `[1, "hello", "%", true]`. 2. Drag the **Function**node onto the canvas and add the following JavaScript code: ```javascript for (let i = 0; i < msg.payload.length; i++) { // Create a new message for each item let newMsg = { ...msg }; // Copy the original msg object newMsg.payload = msg.payload[i]; // Set payload to the current item node.send(newMsg); // Send the message } ``` 3. Drag a **Debug** node onto the canvas and connect it to the output of the **Function** node. When you deploy the flow and click the **Inject** button, each item in the array will be sent as a separate message and printed in the debug panel. This works because the `node.send()` method allows you to send messages asynchronously. Unlike `return`, which ends the execution of the **Function** node immediately, `node.send()` continues to process and send each message without halting the loop. By using `node.send()` inside the loop, you ensure that each iteration produces a separate message, and the Function node can handle multiple messages efficiently. For more information on on this, refer to [Documentation on Sending messages asynchronously](https://nodered.org/docs/user-guide/writing-functions#sending-messages-asynchronously){rel=""nofollow""}. ::render-flow ```json [{"id":"50be2bac3b058be5","type":"group","z":"a3aa840957f658c6","name":"","style":{"label":true},"nodes":["97c288f32955c6ab","7a8404c44ac0749b","0d7b9cb51669ad9b"],"x":414,"y":499,"w":612,"h":82},{"id":"97c288f32955c6ab","type":"inject","z":"a3aa840957f658c6","g":"50be2bac3b058be5","name":"Array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1, \"hello\", \"%\", true]","payloadType":"json","x":510,"y":540,"wires":[["7a8404c44ac0749b"]]},{"id":"7a8404c44ac0749b","type":"function","z":"a3aa840957f658c6","g":"50be2bac3b058be5","name":"For Loop","func":"for (let i = 0; i < msg.payload.length; i++) {\n // Create a new message for each item\n let newMsg = { ...msg }; // Copy the original msg object\n newMsg.payload = msg.payload[i]; // Set payload to the current item\n node.send(newMsg)\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":700,"y":540,"wires":[["0d7b9cb51669ad9b"]]},{"id":"0d7b9cb51669ad9b","type":"debug","z":"a3aa840957f658c6","g":"50be2bac3b058be5","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":540,"wires":[]}] ``` :: ### Implementing Loops in Node-RED with Custom node Throughtout this section we will show you how you can implement loops in Node-RED with custom nodes easily, there are plenty of custom nodes that can be used to achieve the loop but we will going use the popular one [node-red-contrib-loop](https://flows.nodered.org/node/node-red-contrib-loop){rel=""nofollow""}, before moving further make sure to install it by palette manager also for demostration purpose we will use same example we used in the above sections with loops. #### While Loop 1. Drag the **Inject** node onto the canvas. 2. Drag the **Loop** node onto the canvas, double-click it, and set the kind to **Condition**. Set the condition to `msg.payload.includes("Z") != true` as JavaScript. The condition kind offers a lot of flexibility as it allows adding conditions in JavaScript, regex, and JSONata. 3. Set the "When test" option: - Choose "after" if you want the loop to execute at least once before checking the condition, similar to how a while loop operates when it checks the condition after the first iteration. - Choose "before" if you want the loop to check the condition before executing, functioning like a traditional while loop that only runs if the condition is true at the start. 4. Drag the **Function** node onto the canvas. Add the following JavaScript code to it and connect its input to the second output of the **Loop** node: ```javascript msg.i += String.fromCharCode(65 + Math.floor(Math.random() * 26)); // Append a random uppercase letter return msg; ``` 5. Drag the **Delay** node onto the canvas, set the delay to 0.5 milliseconds. When using the condition kind of loop, it is important to use the **Delay** node with this loop custom node to avoid creating an infinite loop. Connect the **Delay** node's input to the output of the **Function** node, and connect its output to the input of the **Loop** node. 6. Drag a **Debug** node onto the canvas and connect its input to the output of the **Function** node. This will print the current `msg.payload` after each iteration. 7. Drag another **Debug** node onto the canvas and connect its input to the first output of the **Loop** node. This will print when the loop exits, indicating that the condition has been met. ::render-flow ```json [{"id":"7ef4d41cf74f75c3","type":"group","z":"a3aa840957f658c6","name":"While Loop","style":{"label":true},"nodes":["3ba931b1.fb48d6","2d297dfa.6e660a","5aef28e.0f9e7d8","90c49008.053a58","9a725668.330148","e88b0e8c.d39858","9c48f336.471ea","66144af4.d1ec9c","75c6b326.e934d4","77ac6763.8e54b8","99b265b4.e2d3c8"],"x":174,"y":139,"w":672,"h":322},{"id":"3ba931b1.fb48d6","type":"loop","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","kind":"cond","count":"10","initial":"1","step":"1","condition":"msg.payload.includes(\"Z\") != true","conditionType":"js","when":"after","enumeration":"enum","enumerationType":"msg","limit":"","loopPayload":"loop-keep","finalPayload":"final-last","x":500,"y":280,"wires":[["5aef28e.0f9e7d8"],["e88b0e8c.d39858"]]},{"id":"2d297dfa.6e660a","type":"inject","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":270,"y":280,"wires":[["3ba931b1.fb48d6"]]},{"id":"5aef28e.0f9e7d8","type":"debug","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"loop","targetType":"msg","statusVal":"","statusType":"auto","x":720,"y":280,"wires":[]},{"id":"90c49008.053a58","type":"comment","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"Example: Conditional loop","info":"","x":310,"y":180,"wires":[]},{"id":"9a725668.330148","type":"comment","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"Show final status","info":"","x":740,"y":240,"wires":[]},{"id":"e88b0e8c.d39858","type":"function","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","func":"// add ASCII char from 32 to 126\nmsg.payload += String.fromCharCode(Math.random()*26 + 65);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":380,"wires":[["66144af4.d1ec9c","9c48f336.471ea"]]},{"id":"9c48f336.471ea","type":"debug","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":730,"y":380,"wires":[]},{"id":"66144af4.d1ec9c","type":"delay","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"","pauseType":"delay","timeout":"0.5","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":530,"y":355,"wires":[["3ba931b1.fb48d6"]]},{"id":"75c6b326.e934d4","type":"comment","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"Show string","info":"","x":730,"y":420,"wires":[]},{"id":"77ac6763.8e54b8","type":"comment","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"Add char to string","info":"","x":370,"y":420,"wires":[]},{"id":"99b265b4.e2d3c8","type":"comment","z":"a3aa840957f658c6","g":"7ef4d41cf74f75c3","name":"Repeat until string doesn't finish with \"Z\"","info":"","x":480,"y":240,"wires":[]}] ``` :: #### For Loop 1. Drag an **Inject** node onto the canvas and set the `msg.payload` to `["foo", "bar", "foobar"]`. This node will trigger the start of the loop and provide the array we want to process. 2. Drag a **Change** node onto the canvas. Configure it to set `msg.count` to `msg.payload.length`, which will store the length of the array. Connect the **Inject** node to this **Change** node. 3. Drag a **Loop** node onto the canvas, double-click on it, and set the kind to **Fixed**. Leave the "count" field empty as we are setting it dynamically using `msg.count`. Set the initial value to `0` and the step value to `1`. Set the loop payload to the original `msg.payload`. 4. Next, drag a **Change** node onto the canvas and configure it to either clear or set `msg.payload` to itself. This ensures that the payload remains unchanged. Connect its input to the output of the **Loop** node and then connect its output back to the input of the **Loop** node. This setup allows the loop to repeat. 5. Then, drag another **Change** node onto the canvas. Configure this node to set `msg.payload` to `msg.payload[msg.loop.value]`, which extracts the current array element using the loop’s counter (`msg.loop.value`). The **Loop** node generates properties like `value`, which is the counter we are incrementing. 6. Finally, drag a **Debug** node onto the canvas and connect it to the output of the previous **Change** node to print the current array element in each iteration. ::render-flow ```json [{"id":"ac4bfead30be7380","type":"group","z":"a3aa840957f658c6","name":"For Loop","style":{"label":true},"nodes":["2ffc3d07.23e6fa","6e1118b1.449db8","ea852667d724cfb1","880a2501f7d153ba","bd58a956.c036b","60a8f138.0909a","44f1a29403157bba"],"x":74,"y":199,"w":1132,"h":202},{"id":"2ffc3d07.23e6fa","type":"inject","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[\"foo\",\"bar\",\"foobar\"]","payloadType":"json","x":210,"y":260,"wires":[["880a2501f7d153ba"]]},{"id":"6e1118b1.449db8","type":"loop","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"","kind":"fcnt","count":"","initial":"0","step":"1","condition":"","conditionType":"js","when":"before","enumeration":"enum","enumerationType":"msg","limit":"","loopPayload":"loop-orig","finalPayload":"final-last","x":710,"y":260,"wires":[["44f1a29403157bba"],["bd58a956.c036b"]]},{"id":"ea852667d724cfb1","type":"change","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"Access array element with loop counter","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[msg.loop.value]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":360,"wires":[["60a8f138.0909a"]]},{"id":"880a2501f7d153ba","type":"change","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"Set Pass Count","rules":[{"t":"set","p":"count","pt":"msg","to":"payload.length","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":260,"wires":[["6e1118b1.449db8"]]},{"id":"bd58a956.c036b","type":"change","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"Repeat","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":360,"wires":[["6e1118b1.449db8","ea852667d724cfb1"]]},{"id":"60a8f138.0909a","type":"debug","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":360,"wires":[]},{"id":"44f1a29403157bba","type":"debug","z":"a3aa840957f658c6","g":"ac4bfead30be7380","name":"End Loop","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1000,"y":240,"wires":[]}] ``` :: #### For...of / ForEach Loop 1. Drag an **Inject** node onto the canvas and set the `msg.payload` to `[6, 14, 36, -8, 100]`. This node will trigger the loop and provide the array of numbers to process. 2. Drag a **Loop** node onto the canvas. Set the **Kind** to **Enumeration** and choose `msg.payload` as the enumeration source. This configuration will loop through each value in the array. Set the "loop payload" to the "value". 3. Drag a **Change** node onto the canvas and configure it to either clear or set `msg.payload` to itself. This ensures that the payload remains unchanged during each iteration. Connect its input to the second output of the **Loop** node, then connect its output back to the input of the **Loop** node to create the loop. 4. Finally, drag a **Debug** node onto the canvas and connect it to the second output of the **Loop** node. This will display the current value being processed in the loop. With the **Enumeration** kind, you can iterate through different types of data such as arrays, strings, objects, and more, making this loop versatile for handling various data structures. ::render-flow ```json [{"id":"65d854c9098393e8","type":"group","z":"a3aa840957f658c6","name":"For of/ for each","style":{"label":true},"nodes":["38630ff1.21721","f009e0e9.24576","cf52b6dd.5febb8","33031c0e.1b115c","b28b06f1.38eb48","6e593ff2.dc94c"],"x":334,"y":219,"w":722,"h":222},{"id":"38630ff1.21721","type":"inject","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"Object","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"first\":\"Hello World\",\"second\":8,\"third\":true}","payloadType":"json","x":430,"y":260,"wires":[["f009e0e9.24576"]]},{"id":"f009e0e9.24576","type":"loop","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"","kind":"enum","count":"","initial":"","step":"","condition":"","conditionType":"js","when":"before","enumeration":"payload","enumerationType":"msg","limit":"","loopPayload":"loop-val","finalPayload":"final-orig","x":740,"y":300,"wires":[["cf52b6dd.5febb8"],["33031c0e.1b115c","6e593ff2.dc94c"]]},{"id":"cf52b6dd.5febb8","type":"debug","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"Loop End","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"loop","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":280,"wires":[]},{"id":"33031c0e.1b115c","type":"change","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"Repeat","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":400,"wires":[["f009e0e9.24576"]]},{"id":"b28b06f1.38eb48","type":"inject","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"Array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[\"foo\",\"bar\",\"foobar\"]","payloadType":"json","x":430,"y":320,"wires":[["f009e0e9.24576"]]},{"id":"6e593ff2.dc94c","type":"debug","z":"a3aa840957f658c6","g":"65d854c9098393e8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":940,"y":400,"wires":[]}] ``` :: # Strings in Node-RED: Convert String to Number, Split, Concatenate, Trim, and More Strings are one of the most common data types in Node-RED. Whether you're converting sensor values, parsing API responses, or building dynamic messages, understanding string operations is essential for building reliable flows. ## Converting String to Number One of the most frequent operations is converting string values to numbers for mathematical calculations or comparisons. 1. Connect your data source to a **Change** node 2. In the **Change** node, set the rule to **"Set"** `msg.payload` 3. Select **"to the value of"** and choose **JSONata expression** 4. Enter: `$number(payload)` 5. Connect to where you need the processed data If your payload contains the string `"42"`, it becomes the number `42`. You can now use this in calculations or comparisons. ::render-flow ```json [{"id":"2ac68ed1d9a7b380","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"42","payloadType":"str","x":530,"y":200,"wires":[["a14a67ac6574ae82"]]},{"id":"a14a67ac6574ae82","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"String to Number","rules":[{"t":"set","p":"payload","pt":"msg","to":"$number(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":200,"wires":[["225f4c6a47df2d3c"]]},{"id":"225f4c6a47df2d3c","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":200,"wires":[]}] ``` :: ## Converting Number to String Converting numbers to strings is useful for displaying values, building messages, or formatting output. 1. Connect your data source to a **Change** node 2. Set the rule to **"Set"** `msg.payload` 3. Select **JSONata expression** 4. Enter: `$string(payload)` 5. Connect to where you need the processed data A number like `42` becomes the string `"42"`, ready for text operations. ::render-flow ```json [{"id":"38b9110190ddba53","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"42","payloadType":"num","x":530,"y":260,"wires":[["06a4b6591a791f8d"]]},{"id":"06a4b6591a791f8d","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Number to String","rules":[{"t":"set","p":"payload","pt":"msg","to":"$string(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":260,"wires":[["c560426627a56137"]]},{"id":"c560426627a56137","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":260,"wires":[]}] ``` :: ## Splitting Strings Splitting strings is essential when parsing CSV data, breaking apart delimited values, or extracting specific parts of text. 1. Connect your string data source to a **Split** node 2. Double-click the Split node to open its configuration 3. In the **"Split using"**field, enter your delimiter character: - `,` for comma-separated values (CSV) - ``(space) for splitting words - `\n` for splitting by lines - `\t` for tab-separated values (TSV) - `;` for semicolon-separated data - `|` for pipe-delimited data 4. Click **Done** 5. Connect to where you need the processed data The Split node creates separate messages for each segment. For example, if you receive a log entry `"2024-12-15 14:30:45 ERROR Database connection failed"` and you split using spaces, it produces separate messages: first `"2024-12-15"`, then `"14:30:45"`, then `"ERROR"`, then `"Database"`, and so on. Each piece flows through your subsequent nodes one at a time, allowing you to extract the timestamp, severity level, and message separately. ::render-flow ```json [{"id":"6a5c13d2e5bf152f","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2024-12-15 14:30:45 ERROR Database connection failed","payloadType":"str","x":530,"y":320,"wires":[["4d3e4c715e94572b"]]},{"id":"9e90b663c08067fc","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":320,"wires":[]},{"id":"4d3e4c715e94572b","type":"split","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Split String","splt":" ","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":690,"y":320,"wires":[["9e90b663c08067fc"]]}] ``` :: ## Concatenating Strings Combining strings is common when building messages, URLs, or formatted output. 1. Add a **Template** node after the nodes containing your data 2. Double-click to open the Template configuration 3. Write your text and insert variables using `{{variableName}}` syntax 4. Click **Done** 5. Connect to where you need the processed data Each `{{variableName}}` is replaced with actual data. For example, the template `Hello {{payload.name}}, your order #{{payload.orderId}} has shipped to {{payload.city}}.` with data containing name "Sarah", orderId "12345", and city "Portland" produces: `Hello Sarah, your order #12345 has shipped to Portland.` ::render-flow ```json [{"id":"60cb0a1d79b095a3","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"name\":\"Sarah\",\"orderId\":\"12345\",\"city\":\"Portland\"}","payloadType":"json","x":530,"y":380,"wires":[["88d113db79d20417"]]},{"id":"9ed0bd64daa0cf35","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":380,"wires":[]},{"id":"88d113db79d20417","type":"template","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Concatenating Strings","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Hello {{payload.name}}, your order #{{payload.orderId}} has shipped to {{payload.city}}.","output":"str","x":720,"y":380,"wires":[["9ed0bd64daa0cf35"]]}] ``` :: ## Parsing JSON Strings API responses and stored data often arrive as JSON strings, text that looks like JSON but isn't yet usable as an object. 1. Place a **JSON** node after your data source (like an HTTP request or file read) 2. Double-click to open its configuration 3. Set the **Action** to **"Convert between JSON String & Object"** 4. Click **Done** 5. Connect to where you need the processed data The JSON node detects your data type automatically. String `'{"temperature":22,"humidity":65}'` becomes an object `{temperature: 22, humidity: 65}` so you can access `msg.payload.temperature`. ::render-flow ```json [{"id":"987d6b5ce96f863e","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"temperature\":22,\"humidity\":65}","payloadType":"str","x":530,"y":440,"wires":[["b92eb17c8dac49d8"]]},{"id":"b92eb17c8dac49d8","type":"json","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Parsing JSON Strings","property":"payload","action":"","pretty":false,"x":720,"y":440,"wires":[["a34030e2d9a085fe"]]},{"id":"a34030e2d9a085fe","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":440,"wires":[]}] ``` :: ## Extracting Substrings Getting specific parts of a string is useful for parsing fixed-format data, extracting codes, or isolating values. 1. Add a **Change** node after your string source 2. Set the rule to **"Set"** `msg.payload` 3. Select **JSONata expression** 4. Enter: `$substring(payload, start, length)`where: - `start` is the position (0 is first character) - `length` is how many characters to take 5. Connect to where you need the processed data **Examples:** - `$substring(payload, 0, 5)` on `"Hello World"` gives `"Hello"` - `$substring(payload, 6, 5)` on `"Hello World"` gives `"World"` - `$substring(payload, 6)` (no length) on `"Hello World"` gives `"World"` (all remaining characters) ::render-flow ```json [{"id":"1505bbcdda1ef70f","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello World","payloadType":"str","x":510,"y":500,"wires":[["7e13b90c9b8e48de"]]},{"id":"7b985161f48aaf74","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":500,"wires":[]},{"id":"7e13b90c9b8e48de","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Extracting Substrings","rules":[{"t":"set","p":"payload","pt":"msg","to":"$substring(payload, 0, 5)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":500,"wires":[["7b985161f48aaf74"]]}] ``` :: ## Trimming Whitespace Removing unwanted spaces, tabs, or line breaks from strings prevents comparison errors and formatting issues. 1. Place a **Change** node before your comparison or processing logic 2. Set the rule to **"Set"** `msg.payload` 3. Select **JSONata expression** 4. Enter: `$trim(payload)` 5. Connect to where you need the processed data Whitespace from both ends is removed. `" Hello World "` becomes `"Hello World"`. The space between words stays, only edge spaces are removed. ::render-flow ```json [{"id":"f43fff4b7a32185d","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":" Hello World ","payloadType":"str","x":510,"y":560,"wires":[["9ed2f66a591cefab"]]},{"id":"603d7024f06979bc","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":560,"wires":[]},{"id":"9ed2f66a591cefab","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Trimming Whitespace","rules":[{"t":"set","p":"payload","pt":"msg","to":"$trim(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":560,"wires":[["603d7024f06979bc"]]}] ``` :: ## Changing Case Converting string case helps with standardization and comparison since computers treat uppercase and lowercase as different. 1. Add a **Change** node before your comparison or output 2. Set the rule to **"Set"** `msg.payload` 3. Select **JSONata expression** 4. If you want to convert to uppercase, enter: `$uppercase(payload)`. If you want to convert to lowercase, enter: `$lowercase(payload)` 5. Connect to where you need the processed data Using `$uppercase(payload)`, the string `"hello world"` becomes `"HELLO WORLD"`. Using `$lowercase(payload)`, the string `"Hello World"` becomes `"hello world"`. ::render-flow ```json [{"id":"9b39fb9d0a75e5c2","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello World","payloadType":"str","x":510,"y":620,"wires":[["04389bee4b520eb3"]]},{"id":"3390eac6826821a7","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":620,"wires":[]},{"id":"04389bee4b520eb3","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Changing Case : Lowercase","rules":[{"t":"set","p":"payload","pt":"msg","to":"$lowercase(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":740,"y":620,"wires":[["3390eac6826821a7"]]}] ``` :: ::render-flow ```json [{"id":"2bee50ed8273a7d7","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":680,"wires":[]},{"id":"b2d16a456b083325","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Changing Case : Uppercase","rules":[{"t":"set","p":"payload","pt":"msg","to":"$uppercase(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":740,"y":680,"wires":[["2bee50ed8273a7d7"]]},{"id":"2f20f09b8146b397","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello World","payloadType":"str","x":510,"y":680,"wires":[["b2d16a456b083325"]]}] ``` :: ## Replacing Text Finding and replacing text within strings lets you correct values, standardize formats, or update content. 1. Add a **Change** node after your text source 2. Set the rule to **"Set"** `msg.payload` 3. Select **JSONata expression** 4. Enter: `$replace(payload, "old", "new")` where "old" is text to find and "new" is the replacement 5. Connect to where you need the processed data All occurrences are replaced. `"I love apples and apples are great"` with `$replace(payload, "apples", "oranges")` gives `"I love oranges and oranges are great"`. ::render-flow ```json [{"id":"dfbb9d1ce41c8672","type":"debug","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":740,"wires":[]},{"id":"33d81d6a0d2d0f0c","type":"change","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"Replacing Text","rules":[{"t":"set","p":"payload","pt":"msg","to":"$replace(payload, \"apples\", \"oranges\")","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":740,"wires":[["dfbb9d1ce41c8672"]]},{"id":"a8e1a932d619fa33","type":"inject","z":"c16e1fb8932e7e73","g":"09a33e651efa47a8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"I love apples and apples are great","payloadType":"str","x":530,"y":740,"wires":[["33d81d6a0d2d0f0c"]]}] ``` :: ## Checking String Length Determining string length helps with validation or conditional processing. 1. Add a **Change** node that will store the length 2. Set the rule to **"Set"** `msg.length` (or another property) 3. Select **JSONata expression** 4. Enter: `$length(payload)` 5. Connect to where you need the processed data The string `"Hello"` returns `5`. Use this value in conditions or validation logic. ::render-flow ```json [{"id":"3791a3fac7be4833","type":"debug","z":"b446dfa04d79d359","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"length","targetType":"msg","statusVal":"","statusType":"auto","x":1110,"y":1260,"wires":[]},{"id":"fca2213c932b1fea","type":"change","z":"b446dfa04d79d359","name":"Checking String Length","rules":[{"t":"set","p":"length","pt":"msg","to":"$length(payload)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":1260,"wires":[["3791a3fac7be4833"]]},{"id":"706d864345a0c62c","type":"inject","z":"b446dfa04d79d359","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello","payloadType":"str","x":710,"y":1260,"wires":[["fca2213c932b1fea"]]}] ``` :: ## Checking if String Contains Text Testing whether a string contains specific text helps with filtering and conditional logic. 1. Add a **Change** node to create a test result 2. Set the rule to **"Set"** `msg.contains` (or another property) 3. Select **JSONata expression** 4. Enter: `$contains(payload, "search term")` 5. Connect to where you need the processed data Returns `true` if found, `false` if not. `"The quick brown fox"` with `$contains(payload, "quick")` returns `true`. Use in Switch nodes to route messages differently based on content. ::render-flow ```json [{"id":"eb5fe8fe6e00e603","type":"debug","z":"b446dfa04d79d359","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":710,"y":1740,"wires":[]},{"id":"daba355f3ca9693f","type":"change","z":"b446dfa04d79d359","name":"Checking if String Contains Text","rules":[{"t":"set","p":"contains","pt":"msg","to":"$contains(payload, \"quick\")","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":1740,"wires":[["eb5fe8fe6e00e603"]]},{"id":"ca2ab085f8edec8c","type":"inject","z":"b446dfa04d79d359","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"The quick brown fox","payloadType":"str","x":270,"y":1740,"wires":[["daba355f3ca9693f"]]}] ``` :: ## Complex String Operations For complex string operations that combine multiple steps or require custom logic, you can use a Function node with JavaScript. If you're not familiar with JavaScript, but you're using FlowFuse, you can use the [FlowFuse Expert's](https://flowfuse.com/blog/2025/07/flowfuse-ai-assistant-better-node-red-manufacturing/) function node generator. Simply describe what you want to accomplish, and the assistant will generate the function node code for you. # How to Update Node-RED Regular updates keep your Node-RED installation running smoothly with the latest features, improvements, and bug fixes. Each new release brings enhancements that expand what you can build and improve your development experience. Whether you installed Node-RED through npm, used the Raspberry Pi script, or are running it in Docker, this guide walks you through the update process step by step. We'll also cover how to check your version, update your installation, and handle updates in FlowFuse. ::cta-image --- alt: Update hundreds of Node-RED instances in one click, not one at a time cta: sign-up reference: "Node-RED: How to Update Node-RED" src: https://flowfuse.com/docs/node-red/getting-started/images/update-node-red-cta-1.png --- :: ## Checking Your Current Version Before updating, check which version you're currently running: ```bash node-red --version ``` You can also check the version from the Node-RED editor by clicking the menu icon (three horizontal lines) in the top right corner and selecting "About." ## Updating Node-RED Installed with npm If you installed Node-RED globally using npm, updating is straightforward. ### Standard Update Stop Node-RED if it's running, then update using npm: ```bash npm install -g --unsafe-perm node-red ``` The `--unsafe-perm` flag is required on some systems, particularly Linux and macOS, to ensure proper permissions during installation. ### Updating to a Specific Version To install a specific version rather than the latest: ```bash npm install -g --unsafe-perm node-red@2.2.0 ``` Replace `2.2.0` with your desired version number. ## Updating on Raspberry Pi If you used the recommended install script on Raspberry Pi, use the same script to update: ```bash bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered) ``` This script will: - Check if updates are available - Update Node-RED to the latest version - Update Node.js if needed - Preserve your existing flows and configuration ## Updating Docker Installations For Docker installations, updating involves pulling the latest image and recreating your container. **Step 1: Pull the Latest Image** ```bash docker pull nodered/node-red:latest ``` **Step 2: Stop and Remove the Old Container** ```bash docker stop mynodered docker rm mynodered ``` Replace `mynodered` with your actual container name. If you're not sure what your container is named, run `docker ps -a` to see a list of all your containers. **Step 3: Start a New Container** ```bash docker run -d --name mynodered -p 1880:1880 -v node_red_data:/data nodered/node-red:latest ``` Make sure to use the same volume mapping (`-v`) to preserve your flows and settings. ## Updating FlowFuse Node-RED Instance Updating a FlowFuse-managed Node-RED instance is quick and straightforward: 1. Navigate to your FlowFuse instance 2. Go to the **Overview** tab 3. In the **Specs** section, you'll see all details including your current Node-RED version 4. If an update is available, you'll see an **Update** button in the top right corner of the specs section 5. Click the **Update** button ![FlowFuse Node-RED instance update interface](https://flowfuse.com/docs/node-red/getting-started/images/node-red-instance-update.png){dataZoomable=""} You'll be redirected to the instance settings where you have two options: ### Update to Latest Version Click **Update Node-RED version** to install the latest available version. ### Change to Specific Version 1. Click **Change Node-RED version** 2. Select your desired Node-RED version from the dropdown 3. Click **Change Node-RED version** to apply FlowFuse handles the update process automatically while preserving your flows and configuration. ## Scheduling Automatic Updates Instead of updating instances manually, you can schedule automatic updates in flowfuse: 1. Go to **Settings** 2. Open the **Maintenance** section 3. Configure the update schedule by selecting: - Days of the week - Preferred time ranges ![FlowFuse scheduled update configuration](https://flowfuse.com/docs/node-red/getting-started/images/schedule-update.png){dataZoomable=""} FlowFuse will then automatically update your Node-RED instances within the defined maintenance window, ensuring they stay up to date without manual intervention. # Setting Node-RED on BLIIOT ARMxy BL340 ## Specifications | | | | -------------- | ------------------------------------------------------------------------ | | Model | BLIIoT ARMxy BL340 Series | | RAM MB | 2048 | | Processor | ARM Cortex-A53 | | GPU | G31 MP2 | | IO Points | Optional (GPIO, RS485,CAN,RS232,DI/DO/AI/AO etc.,) | | Connectivity | Dual-band Wi-Fi 6, Bluetooth 5.2, Gigabit Ethernet, 4x USB 3.0, 1x USB-C | | Clock Speed | Up to 1.4 GHz | | Storage | SD, SDHC and SDXC(UHS-I) card | | Display Output | HDMI 2.0, DisplayPort | | Power Supply | 9\~36V DC | The BLIIOT ARMxy BL340 is a high-performance single-board computer designed for demanding applications, including edge computing, automation, and embedded systems. Featuring an octa-core ARM processor, advanced connectivity options, and support for high-speed storage, it provides a powerful platform for developers and engineers. Integrating this powerful hardware with FlowFuse not only enhances its capabilities but also simplifies the management and deployment process. ::div{.ff-callout.ff-callout--note} Note :::div{.ff-callout__content} Exciting Update! FlowFuse is now available as three different products: [FlowFuse Edge for OT teams](https://flowfuse.com/product/edge/), [FlowFuse Hub for IT teams](https://flowfuse.com/product/hub/), and [FlowFuse Fleet](https://flowfuse.com/product/fleet/) for managing devices at scale. Visit the [FlowFuse Product Page](https://flowfuse.com/product/) to learn how the platform enables you to build and manage industrial apps at scale. ::: :: ## Prerequisites Before proceeding with the installation, ensure you have the following: - **BLIIOT ARMxy BL340** – A functioning device with internet access. - **FlowFuse Account** - Ensure you have a FlowFuse account. If not, you can create a free account that allows you to manage up to two edge devices for free. For more information, refer to [FlowFuse Free Tier](https://flowfuse.com/blog/2024/12/flowfuse-release-2-12/) - **Sudo Privileges** – Administrator access to install required packages. ## Getting Started This guide explores how to install and run Node-RED through the FlowFuse Device Agent on the BLIIOT ARMxy BL340, enabling you to build, manage, and scale Node-RED flows efficiently from a remote location. ### Installing FlowFuse Device Agent Before we start, it is recommended to update and upgrade your system to ensure all your packages are up to date: ```bash sudo apt update && sudo apt upgrade -y ``` Next, let's install the FlowFuse device agent with the following script. ```bash bash <(curl -sL https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/raspbian-install-device-agent.sh) ``` This script installs the Node.js runtime (if not already installed), sets up the FlowFuse device agent, and configures the device to automatically run the FlowFuse agent on boot and restart it in case of a crash. To verify that the service is running, use the following command: ```bash sudo systemctl status flowfuse-device-agent.service ``` If running, you should see a result similar to the one shown in the image below: !["Status of the FlowFuse Device Agent systemd service"](https://flowfuse.com/docs/node-red/hardware/images/systemctl-status.png "Status of the FlowFuse Device Agent systemd service"){dataZoomable=""} ### Registering the Device to Connect to FlowFuse Once you have installed the FlowFuse Device Agent, you need to register the hardware to connect it to your FlowFuse team. For instructions on how to register the hardware with your FlowFuse team, follow the documentation: [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register/). When registering your hardware, you will be presented with a dialog containing a one-time passcode command that the Device Agent uses to retrieve its configuration. **Make sure to copy it.** !["Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"](https://flowfuse.com/docs/node-red/hardware/images/configuration-dailog-with-one-time-code.png "Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"){dataZoomable=""} ### Connecting Device Execute the command you have copied with sudo as shown below ```bash sudo flowfuse-device-agent -o https://app.flowfuse.com ``` Once executed, you should see an output similar to the one below, indicating that the FlowFuse Device Agent has been successfully configured: ```bash [AGENT] 3/21/2025 7:09:25 PM [info] Entering Device setup... [AGENT] 3/21/2025 7:09:27 PM [info] Device setup was successful [AGENT] 3/21/2025 7:09:27 PM [info] To start the Device Agent with the new configuration, run the following command: [AGENT] 3/21/2025 7:09:27 PM [info] flowfuse-device-agent ``` Now, you can check the remote instance in the FlowFuse platform, where its status should be displayed as **"running."**. !["Status of the BLIIOT ARMxy BL340 remote instance in FlowFuse, showing its connection and operational state"](https://flowfuse.com/docs/node-red/hardware/images/status-flowfuse.png "Status of the BLIIOT ARMxy BL340 remote instance in FlowFuse, showing its connection and operational state"){dataZoomable=""} Now, when your device reboots, the FlowFuse Device Agent will automatically start, ensuring that your BLIIOT ARMxy BL340 remains connected to the FlowFuse platform. ## Accessing Node-RED Editor. 1. Login into your FlowFuse account. 2. Click on the remote instances option in the left sidebar. 3. Click on the device and enable the developer mode by clicking on the top right-corner switch. 4. Once Developer Mode is enabled, click on the Open Editor option located next to the that switch. For more information refer to [FlowFuse documentation](https://flowfuse.com/docs/user/introduction/#working-with-devices) # Setting Up Node-RED on Different Hardware Node-RED is highly versatile and can be set up on a wide range of hardware devices, including popular choices like Raspberry Pi, Ardiuno, Siemens 2050, and more. This flexibility allows you to connect Node-RED to diverse devices and sensors, which enables the creation of interactive and automated systems. By setting up Node-RED on different hardware, you can easily integrate physical inputs with digital systems. This capability is essential for IoT (Internet of Things) applications, where data from sensors and devices can trigger actions or responses in real-time. ## Resources Here are some resources to help you get started with Node-RED on diffrent hardware devices: - [Run Node-RED on Siemens IoT2050](https://flowfuse.com/docs/node-red/hardware/siemens-iot-2050/): In this guide, we will discuss how to install FlowFuse Device agent on Siemens IoT2050. - [Setting Node-RED on BLIIOT ARMxy BL340](https://flowfuse.com/docs/node-red/hardware/armxy-bl340/): Guide to setting up Node-RED on BLIIOT ARMxy BL340, including installation and configuration steps. - [Setting Node-RED on Raspberry Pi 4](https://flowfuse.com/docs/node-red/hardware/raspberry-pi-4/): Learn how to install the FlowFuse Edge Agent on the Raspberry Pi 4 effortlessly. Manage your device with Node-RED through FlowFuse with ease. - [Setting Node-RED on Robustel EG5120](https://flowfuse.com/docs/node-red/hardware/robustel-eg5120/): In this guide, we will discuss how to install FlowFuse Device agent on Robustel EG5120. - [Setting up Node-RED on Opto-22 Groov Rio R7](https://flowfuse.com/docs/node-red/hardware/opto-22-groove-rio-7-mm2001-10/): Learn how to install and configure Node-RED on the Opto-22 Groov Rio R7, a rugged edge I/O module for industrial applications. - [Setting Up Node-RED on Raspberry Pi 5](https://flowfuse.com/docs/node-red/hardware/raspberry-pi-5/): Learn how to install the FlowFuse Edge Agent on the Raspberry Pi 5 effortlessly. Manage your device with Node-RED through FlowFuse with ease. # Setting up Node-RED on Opto-22 Groov Rio R7 ## Specifications | | | | ------------ | -------------------------------------------------------------------------------------------------- | | Model | GRV-R7-MM2001-10 | | RAM | 1024 MB | | Processor | ARM Cortex-A8, 1 GHz | | I/O Channels | 10 multi-signal, multifunction channels (analog I/O, temperature, discrete I/O, mechanical relays) | | Connectivity | Dual switched Gigabit Ethernet,USB 2.0 (host),Power over Ethernet (PoE),10–32 VDC power input | | Clock Speed | 1 GHz | | Storage | 4 GB eMMC (internal),USB memory stick support (up to 32 GB) | The Opto-22 Groov Rio R7 is a rugged edge I/O module designed for industrial applications. Equipped with a powerful ARM Cortex-A8 processor, versatile I/O channels, and various connectivity options, it’s an ideal solution for edge computing and industrial IoT. ## Prerequisites Before proceeding with the installation, ensure you have the following: - **Opto-22 Groov Rio R7** – A functioning device with internet access. - **FlowFuse Account** - You need an active FlowFuse account to access the platform and configure your instance. If you do not have one, please visit the FlowFuse website and [sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} for a new account before proceeding. - **Sudo Privileges** – Administrator access to install required packages. ## Getting Started This guide will walk you through setting up Node-RED on the Groov Rio R7 using the FlowFuse Device Agent, allowing you to manage, scale, and secure your remote instances effectively. ### Installing FlowFuse Device Agent Before starting the installation, it is recommended to update your system to ensure that all your packages are up to date. You can use groov manage, which acts as the command central for your groov RIO devices. For detailed instructions on how to update the system, [watch this video](https://www.opto22.com/support/resources-tools/videos/playlist-what-is-groov-epic?wchannelid=61lkudfc8c&wmediaid=mxzzp2kudx){rel=""nofollow""}. This guide is written for the firmware version of: `4.0.2-b.194`. Node.JS 20 is available on the device, and you should be good to go to register the edge device on FlowFuse. ### Registering the Device to Connect to FlowFuse Once you have installed the FlowFuse Device Agent, you need to register the hardware to connect it to your FlowFuse team. For instructions on how to register the hardware with your FlowFuse team, follow the documentation: [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register/). When registering your hardware, you will be presented with a dialog containing a one-time passcode command that the Device Agent uses to retrieve its configuration. **Make sure to copy it.** !["Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"](https://flowfuse.com/docs/node-red/hardware/images/configuration-dailog-with-one-time-code.png "Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"){dataZoomable=""} ### Connecting Device Execute the command you have copied with sudo as shown below ```bash sudo flowfuse-device-agent -o https://app.flowfuse.com --port 1881 ``` > **Important:** Be sure to include the --port 1881 flag when running the command. By default, the Opto-22 firewall only allows access to port 1880 (default Node-RED port) by a very restricted list of users, so the Groov Rio R7 requires specifying port 1881 for the Device Agent to start correctly. Once executed, you should see an output similar to the one below, indicating that the FlowFuse Device Agent has been successfully configured: ```bash [AGENT] 3/21/2025 7:09:25 PM [info] Entering Device setup... [AGENT] 3/21/2025 7:09:27 PM [info] Device setup was successful [AGENT] 3/21/2025 7:09:27 PM [info] To start the Device Agent with the new configuration, run the following command: [AGENT] 3/21/2025 7:09:27 PM [info] flowfuse-device-agent ``` Now, you can check the remote instance in the FlowFuse platform, where its status should be displayed as **"running."**. # Setting Node-RED on Raspberry Pi 4 ## Specifications | | | | ------------ | ------------------------------------------------------------------------------- | | Model | Raspberry Pi 4 B 8GB | | RAM MB | 8192 | | Processor | Broadcom BCM2711, ARM Cortex-A72 (ARMv8-A), 4 (Quad-core) | | GPIO | (Fully backwar ds-compatible with previous boards), Standard 40-pin GPIO Header | | Connectivity | Dual-band Wi-Fi, Bluetooth 5.0, Gigabit Ethernet, 2x USB 3.0, 2x USB 2.0 | | Clock Speed | 1.5 GHz | | Storage | microSD | ## Raspberry Pi OS Installation To set up your Raspberry Pi 4 for use with Node-RED and FlowFuse, follow these steps: ### Flashing Raspberry Pi OS 1. Use the [official Raspberry Pi Imager](https://www.raspberrypi.com/software/){rel=""nofollow""} to flash the 64-bit version of Raspberry Pi OS to an SD card. ![Flash Raspberry Pi OS on an SD-card](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-5-flash-os.png) 2. Before writing to the SD card, configure the OS for headless mode, including Wi-Fi, SSH, and authentication settings. ![Configure RPi OS before flashing](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-5-config-before-flash.png) 3. Write the OS and configuration to the SD card. This process takes about 10 minutes. 4. Insert the SD card into the Raspberry Pi 4 and power it on. The device should appear on your network after a minute or so. 5. Connect to the Raspberry Pi using SSH: ```sh ssh pi@raspberrypi.local ``` ## Getting Started This guide explores how to install and run Node-RED through the FlowFuse Device Agent on the Raspberry Pi 4, enabling you to build, manage, and scale Node-RED flows efficiently from a remote location. ### Installing FlowFuse Device Agent Before we start, it is recommended to update and upgrade your system to ensure all your packages are up to date: ```bash sudo apt update && sudo apt upgrade -y ``` Next, let's install the FlowFuse device agent with the following script. ```bash bash <(curl -sL https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/raspbian-install-device-agent.sh) ``` This script installs the Node.js runtime (if not already installed), sets up the FlowFuse device agent, and configures the device to automatically run the FlowFuse agent on boot and restart it in case of a crash. To verify that the service is running, use the following command: ```bash sudo systemctl status flowfuse-device-agent.service ``` If running, you should see a result similar to the one shown in the image below: !["Status of the FlowFuse Device Agent systemd service"](https://flowfuse.com/docs/node-red/hardware/images/systemctl-status.png "Status of the FlowFuse Device Agent systemd service"){dataZoomable=""} ### Registering the Device to Connect to FlowFuse Once you have installed the FlowFuse Device Agent, you need to register the hardware to connect it to your FlowFuse team. For instructions on how to register the hardware with your FlowFuse team, follow the documentation: [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register/). When registering your hardware, you will be presented with a dialog containing a one-time passcode command that the Device Agent uses to retrieve its configuration. **Make sure to copy it.** !["Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"](https://flowfuse.com/docs/node-red/hardware/images/configuration-dailog-with-one-time-code.png "Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"){dataZoomable=""} ### Connecting Device Execute the command you have copied with sudo as shown below ```bash sudo flowfuse-device-agent -o https://app.flowfuse.com ``` Once executed, you should see an output similar to the one below, indicating that the FlowFuse Device Agent has been successfully configured: ```bash [AGENT] 3/21/2025 7:09:25 PM [info] Entering Device setup... [AGENT] 3/21/2025 7:09:27 PM [info] Device setup was successful [AGENT] 3/21/2025 7:09:27 PM [info] To start the Device Agent with the new configuration, run the following command: [AGENT] 3/21/2025 7:09:27 PM [info] flowfuse-device-agent ``` Now, you can check the remote instance in the FlowFuse platform, where its status should be displayed as **"running."**. ![Status of the remote instance in FlowFuse, showing its connection and operational state](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-4.png "Status of the remote instance in FlowFuse, showing its connection and operational state"){dataZoomable=""} ## Accessing Node-RED Editor. 1. Login into your FlowFuse account. 2. Click on the remote instances option in the left sidebar. 3. Click on the device and enable the developer mode by clicking on the top right-corner switch. 4. Once Developer Mode is enabled, click on the Open Editor option located next to the that switch. For more information refer to [FlowFuse documentation](https://flowfuse.com/docs/user/introduction/#working-with-devices) # Setting Up Node-RED on Raspberry Pi 5 ## Specifications | | | | ------------ | ----------------------------------------------------------------------------------------- | | Model | Raspberry Pi 5 Model 8GB | | RAM MB | 8192 | | Processor | Broadcom BCM2712, ARM Cortex-A76 (ARMv8.2-A), 4 (Quad-core) | | GPIO | Standard 40-pin GPIO Header | | Connectivity | 2 × USB 2.0 Ports, 2 × USB 3.0 Ports, Bluetooth 5.0, USB-C, Wi-Fi + Bluetooth® Low Energy | | Clock Speed | 2.4 GHz | | Storage | microSD | ## Raspberry Pi OS Installation To set up your Raspberry Pi 5 for use with Node-RED and FlowFuse, follow these steps: ### Flashing Raspberry Pi OS 1. Use the [official Raspberry Pi Imager](https://www.raspberrypi.com/software/){rel=""nofollow""} to flash the 64-bit version of Raspberry Pi OS to an SD card. ![Flash Raspberry Pi OS on an SD-card](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-5-flash-os.png) 2. Before writing to the SD card, configure the OS for headless mode, including Wi-Fi, SSH, and authentication settings. ![Configure RPi OS before flashing](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-5-config-before-flash.png) 3. Write the OS and configuration to the SD card. This process takes about 10 minutes. 4. Insert the SD card into the Raspberry Pi 5 and power it on. The device should appear on your network after a minute or so. 5. Connect to the Raspberry Pi using SSH: ```sh ssh pi@raspberrypi.local ``` ## Getting Started This guide explores how to install and run Node-RED through the FlowFuse Device Agent on the Raspberry Pi 5, enabling you to build, manage, and scale Node-RED flows efficiently from a remote location. ### Installing FlowFuse Device Agent Before we start, it is recommended to update and upgrade your system to ensure all your packages are up to date: ```bash sudo apt update && sudo apt upgrade -y ``` Next, let's install the FlowFuse device agent with the following script. ```bash bash <(curl -sL https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/raspbian-install-device-agent.sh) ``` This script installs the Node.js runtime (if not already installed), sets up the FlowFuse device agent, and configures the device to automatically run the FlowFuse agent on boot and restart it in case of a crash. To verify that the service is running, use the following command: ```bash sudo systemctl status flowfuse-device-agent.service ``` If running, you should see a result similar to the one shown in the image below: !["Status of the FlowFuse Device Agent systemd service"](https://flowfuse.com/docs/node-red/hardware/images/systemctl-status.png "Status of the FlowFuse Device Agent systemd service"){dataZoomable=""} ### Registering the Device to Connect to FlowFuse Once you have installed the FlowFuse Device Agent, you need to register the hardware to connect it to your FlowFuse team. For instructions on how to register the hardware with your FlowFuse team, follow the documentation: [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register/). When registering your hardware, you will be presented with a dialog containing a one-time passcode command that the Device Agent uses to retrieve its configuration. **Make sure to copy it.** !["Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"](https://flowfuse.com/docs/node-red/hardware/images/configuration-dailog-with-one-time-code.png "Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"){dataZoomable=""} ### Connecting Device Execute the command you have copied with sudo as shown below ```bash sudo flowfuse-device-agent -o https://app.flowfuse.com ``` Once executed, you should see an output similar to the one below, indicating that the FlowFuse Device Agent has been successfully configured: ```bash [AGENT] 3/21/2025 7:09:25 PM [info] Entering Device setup... [AGENT] 3/21/2025 7:09:27 PM [info] Device setup was successful [AGENT] 3/21/2025 7:09:27 PM [info] To start the Device Agent with the new configuration, run the following command: [AGENT] 3/21/2025 7:09:27 PM [info] flowfuse-device-agent ``` Now, you can check the remote instance in the FlowFuse platform, where its status should be displayed as **"running."**. ![Status of the remote instance in FlowFuse, showing its connection and operational state](https://flowfuse.com/docs/node-red/hardware/images/raspberry-pi-5.png "Status of the remote instance in FlowFuse, showing its connection and operational state."){dataZoomable=""} ## Accessing Node-RED Editor. 1. Login into your FlowFuse account. 2. Click on the remote instances option in the left sidebar. 3. Click on the device and enable the developer mode by clicking on the top right-corner switch. 4. Once Developer Mode is enabled, click on the Open Editor option located next to the that switch. For more information refer to [FlowFuse documentation](https://flowfuse.com/docs/user/introduction/#working-with-devices) # Setting Node-RED on Robustel EG5120 ## Specifications | | | | ------------ | ------------------------------------------------------------------------ | | Model | Robustel EG5120 | | RAM MB | 2048 | | Processor | Broadcom BCM2711, ARM Cortex-A72 (ARMv8-A), 4 (Quad-core) | | GPIO | Standard 40-pin GPIO Header | | Connectivity | Dual-band Wi-Fi, Bluetooth 5.0, Gigabit Ethernet, 2x USB 3.0, 2x USB 2.0 | | Clock Speed | 1.5 GHz | | Storage | microSD | The [Robustel EG5120](https://www.robustel.com/product/eg5120-industrial-edge-computing-gateway/){rel=""nofollow""} is a versatile gateway that facilitates robust connectivity for industrial IoT applications. Integrating this powerful hardware with FlowFuse not only enhances its capabilities but also simplifies the management and deployment process. In this documentation, we’ll walk through the steps to integrate the Robustel EG5120 with FlowFuse. The [Robustel EG5120](https://www.robustel.com/product/eg5120-industrial-edge-computing-gateway/){rel=""nofollow""}, equipped with Linux-based Debian 11 supporting a wide variety of programming languages including Node.js, offers robust connectivity options. When combined with FlowFuse, this gateway becomes even more powerful, enabling seamless device management and deployment. The Robustel EG5120 supports multiple connectivity options including Ethernet, Wi-Fi, and cellular networks, which are essential for flexible deployments in various industrial scenarios. Its built-in support for Bluetooth, cellular connectivity, RS232, RS485, and Modbus facilitates seamless integration with a wide array of IoT devices and services. This blog will guide you through using FlowFuse to effectively manage your Node-RED instance, enhancing both the security and scalability of your IoT applications. ## Getting Started This guide explores how to install and run Node-RED through the FlowFuse Device Agent on the Robustel EG5120, enabling you to build, manage, and scale Node-RED flows efficiently from a remote location. ### Installing FlowFuse Device Agent Before we start, it is recommended to update and upgrade your system to ensure all your packages are up to date: ```bash sudo apt update && sudo apt upgrade -y ``` Next, let's install the FlowFuse device agent with the following script. ```bash bash <(curl -sL https://raw.githubusercontent.com/FlowFuse/device-agent/main/service/raspbian-install-device-agent.sh) ``` This script installs the Node.js runtime (if not already installed), sets up the FlowFuse device agent, and configures the device to automatically run the FlowFuse agent on boot and restart it in case of a crash. To verify that the service is running, use the following command: ```bash sudo systemctl status flowfuse-device-agent.service ``` If running, you should see a result similar to the one shown in the image below: !["Status of the FlowFuse Device Agent systemd service"](https://flowfuse.com/docs/node-red/hardware/images/systemctl-status.png "Status of the FlowFuse Device Agent systemd service"){dataZoomable=""} ### Registering the Device to Connect to FlowFuse Once you have installed the FlowFuse Device Agent, you need to register the hardware to connect it to your FlowFuse team. For instructions on how to register the hardware with your FlowFuse team, follow the documentation: [Register your Remote Instance](https://flowfuse.com/docs/device-agent/register/). When registering your hardware, you will be presented with a dialog containing a one-time passcode command that the Device Agent uses to retrieve its configuration. **Make sure to copy it.** !["Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"](https://flowfuse.com/docs/node-red/hardware/images/configuration-dailog-with-one-time-code.png "Dialog containing a one-time passcode command that the Device Agent can use to retrieve its configuration"){dataZoomable=""} ### Connecting Device Execute the command you have copied with sudo as shown below ```bash sudo flowfuse-device-agent -o https://app.flowfuse.com ``` Once executed, you should see an output similar to the one below, indicating that the FlowFuse Device Agent has been successfully configured: ```bash [AGENT] 3/21/2025 7:09:25 PM [info] Entering Device setup... [AGENT] 3/21/2025 7:09:27 PM [info] Device setup was successful [AGENT] 3/21/2025 7:09:27 PM [info] To start the Device Agent with the new configuration, run the following command: [AGENT] 3/21/2025 7:09:27 PM [info] flowfuse-device-agent ``` Now, you can check the remote instance in the FlowFuse platform, where its status should be displayed as **"running."**. ## Accessing Node-RED Editor. 1. Login into your FlowFuse account. 2. Click on the remote instances option in the left sidebar. 3. Click on the device and enable the developer mode by clicking on the top right-corner switch. 4. Once Developer Mode is enabled, click on the Open Editor option located next to the that switch. For more information refer to [FlowFuse documentation](https://flowfuse.com/docs/user/introduction/#working-with-devices) # Run Node-RED on Siemens IoT2050 ## Specifications | | | | ------------ | -------------------------------------------------------- | | Model | IOT2050 Basic | | RAM MB | 1024 | | Processor | ARM TI AM6528 GP | | GPIO | x20 Digital I/O | | Connectivity | 1x RS 232 / 422 / 485, Ethernet, USB 2.0, Arduino, mPCIe | | Clock Speed | 1 GHz | | Storage | SD Card | Siemens [announced](https://press.siemens.com/global/en/pressrelease/new-siemens-gateway-between-cloud-company-it-and-production){rel=""nofollow""} the IoT2000 series in March of 2020. With this tool many have been using it to function as a gateway between their plant operations and cloud infrastructure. Onboard it came with Node-RED pre-installed. To manage Node-RED as an organization the FlowFuse agent is recommended, this documentation shows you how to do so. ::div --- style: "background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;" --- Warning: Later in the documentation we will be updating Node.js. This will break [MRAA](https://www.npmjs.com/package/mraa) library. This will prevent communication to the GPIO of the device. :: ## Goal The goal of this documentation is to guide the user through the installation process of getting FlowFuse Device agent installed on an IoT2050. The IoT2050 comes pre-installed with version 12.22.x Node.js on the [IOT2050\_Example\_Image\_V1.3.1](https://support.industry.siemens.com/cs/document/109741799/downloads-for-simatic-iot20x0?dti=0&lc=en-GB){rel=""nofollow""} image. A requirement to install FlowFuse Device Agent, Node.js needs to be upgraded to version 18 minimum. We will be going through that process. ## Prerequisites We will be working with the IoT2050 Advanced, *6ES7 647-0BA00-1YA2*. The device has been [upgraded](https://support.industry.siemens.com/cs/attachments/109741799/IOT2050_How_To_Firmware_Update_V1.3.pdf){rel=""nofollow""} to the latest firmware at the time of writing this article of v1.3.1. We will be leveraging the IOT2050\_Example\_Image\_V1.3.1.zip image which is a Debian base OS. To complete this guide, knowledge of Linux-based cli is necessary. Documentation to complete these requirements can be found [here](https://support.industry.siemens.com/cs/document/109741799/downloads-for-simatic-iot20x0?dti=0&lc=en-GB){rel=""nofollow""}. ## Step by Step Guide 1. First we need to run the standard updates. ```shell apt-get update apt-get upgrade ``` 2. If you need to migrate your existing Node-RED follow these [instructions](https://flowfuse.com/docs/migration) to backup your existing progress. From there we will need to remove the existing service that autostarts Node-RED by running the following command and rebooting: ```shell systemctl disable node-red.service reboot -h now ``` 3. Confirm that your Node-RED instance is no longer running. ```shell systemctl status node-red ``` In the output look for the text that signifies the service has been stopped. > iot2050-debian systemd [1] : Stopped Node-RED. 4. Now it is time to upgrade your Node.js version. To check the version before we get started run `node -v`. You should see an output like this: > v12.22.5 ::div --- style: "background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;" --- Warning: updating Node.js will break the [MRAA](https://www.npmjs.com/package/mraa) library. This will prevent communication to the GPIO of the device. Details can be found [here](https://support.industry.siemens.com/forum/WW/en/posts/iot2050-node-js-versions/297170). :: Then, install a tool called *n* that will allow you to change your versions of Node.js with the following command. ```shell npm install n -g ``` 5. Next we will install the version 18.17.x (LTS) of Node.js. ```shell n v18.17 ``` Now run `node -v` again to confirm the installation. You should see the latest version now installed. > v18.17.1 6. Now that we have Node.js installed, we can proceed with the standard installation process. First [install](https://flowfuse.com/docs/device-agent/install/) the FlowFuse Device agent. Then, to connect your FlowFuse Device Agent, follow these [instructions](https://flowfuse.com/docs/device-agent/register/). 7. Lastly, if you want your device to run on boot. Follow these [instructions](https://flowfuse.com/blog/2023/05/device-agent-as-a-service/). ## Switching between versions of Node.js Switching between versions of Node.js can now be completed by leveraging *n* command that was installed in step 4. To do so simply run the following to switch back. ```shell n v12.22.5 ``` ## More on MRAA The MRAA library is a "Low Level Skeleton Library for Communication on GNU/Linux platform." It has been key for various solutions to communicate to hardware boards GPIO, General Purpose Input Output. The MRAA library only supports version 6.x.x of Node.js, but Siemens put in the effort to patch their deployment up to version 12.22.x of Node.js. # Using Node-RED Node-RED is the open-source runtime FlowFuse runs, governs and scales. This section provides reference documentation for working with Node-RED itself: connecting it to databases, communication protocols, hardware and notification services, and understanding what each core node does. It is here to answer a question you already have. It is not a path into FlowFuse, and it does not need to be read in order. If you are setting FlowFuse up, start with [Using FlowFuse](https://flowfuse.com/docs/user/) instead. ::callout{icon="i-lucide-book-open"} **The Node-RED project's own documentation is the primary source.** Installing Node-RED, the editor, the message model, writing functions and the API reference all live at [nodered.org/docs](https://nodered.org/docs/){rel=""nofollow""}. This section covers the ground that documentation does not: per-node reference on the web, and connecting Node-RED to specific databases, protocols, hardware and services. :: ## Getting started Installing Node-RED, finding your way around the editor, and the basics of shaping a message. [Getting started](https://flowfuse.com/docs/node-red/getting-started/) ## Core nodes Reference documentation for each node in the default Node-RED palette, with a worked reason to reach for it. [Core nodes](https://flowfuse.com/docs/node-red/core-nodes/) ## Communication protocols Modbus, OPC UA, MQTT, AMQP, WebSocket and LwM2M, for connecting Node-RED to controllers and edge equipment. [Communication protocols](https://flowfuse.com/docs/node-red/protocol/) ## Databases Reading and writing SQL, NoSQL and time-series databases with Node-RED, one guide per database. [Databases](https://flowfuse.com/docs/node-red/database/) ## Integration technologies Webhooks, REST APIs and GraphQL, for connecting Node-RED to the rest of your systems. [Integration technologies](https://flowfuse.com/docs/node-red/integration-technologies/) ## Notification services Sending alerts from Node-RED by email, Telegram and Discord. [Notification services](https://flowfuse.com/docs/node-red/notification/) ## Hardware Running Node-RED on a Raspberry Pi, a Siemens IoT2050, and other industrial gateways. [Hardware](https://flowfuse.com/docs/node-red/hardware/) ## Peripheral devices Webcams, barcode scanners and other devices connected to the machine Node-RED runs on. [Peripheral devices](https://flowfuse.com/docs/node-red/peripheral/) ## Reference [Terminology](https://flowfuse.com/docs/node-red/terminology/) for the words used in Node-RED documentation, and [keyboard shortcuts](https://flowfuse.com/docs/node-red/keyboard/) for the editor. ::callout{icon="i-lucide-arrow-right"} **Running Node-RED for an organisation?** Access control, version history, deployment across environments and remote instance management are what FlowFuse adds on top. See [Using FlowFuse](https://flowfuse.com/docs/user/). :: # Integrating GraphQL APIs in Node-RED GraphQL is transforming the way APIs are designed. Unlike traditional REST APIs, which often require multiple requests to different endpoints, GraphQL provides a single, flexible endpoint that allows you to fetch exactly the data you need, nothing more, nothing less. In this article, you will learn how to integrate GraphQL with Node-RED and build APIs that efficiently serve your application's data requirements. ## Getting Started First, you'll need to install the GraphQL package for Node-RED. This adds the essential nodes for working with GraphQL endpoints. **Installation Steps:** 1. Open Node-RED and navigate to **Menu → Manage palette** 2. Go to the **Install** tab 3. Search for `node-red-contrib-graphql` and install it ## Setting Up Your GraphQL Connection Once installed, you'll configure your first GraphQL endpoint. This is where you define how Node-RED connects to your GraphQL server. **Configuration Process:** 1. Drag a `graphql` node onto your canvas 2. Double-click to open the configuration panel 3. Click the pencil icon next to **Endpoint** to create a new configuration 4. Fill in these essential settings: - **Name**: Use a descriptive name like "User Management API" or "Countries Database" - **Endpoint**: Your GraphQL server URL (e.g., `https://api.example.com/graphql`) - **Token**: Add authentication if required (Bearer tokens are the most common) *Important: For sensitive credentials such as tokens, use environment variables to prevent them from being exposed when sharing flows. Learn more about using environment variables in Node-RED [here](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/).* ## Understanding GraphQL Query Structure GraphQL queries are intuitive once you understand the basic pattern. You're essentially describing the shape of the data you want to receive. **Basic Query Example:** ```graphql query { countries { code name capital } } ``` This query says: "Get me a list of countries, but only return the code, name, and capital for each one." The server won't send population, area, or any other fields, just what you requested. **Response Structure:** ```json { "data": { "countries": [ { "code": "US", "name": "United States", "capital": "Washington D.C." } ] } } ``` Notice how the response mirrors your query structure, this consistency makes GraphQL predictable and easy to work with. ## Building Your First Query Flow Let's create a practical example using a public GraphQL API to fetch country information, which is perfect for learning the basics without needing authentication. **Step-by-Step Flow Creation:** 1. Drag an **Inject** node onto the canvas to trigger the query. 2. Drag a **GraphQL** node onto the canvas and configure it with the endpoint `https://countries.trevorblades.com`. 3. Use the following query in the GraphQL node: ```graphql query GetCountries { countries { code name capital currency } } ``` 4. Drag a **Debug** node onto the canvas and set it to display `msg.payload`. 5. **Connect the Inject node to the GraphQL node, and then connect the GraphQL node to the Debug node.** 6. Deploy the flow and click the **Inject** button. Check the **Debug** panel for the output. ### What to Expect in Debug Output The debug panel will display an array of country objects. Here's a trimmed example of what you should see: ```json { "data": { "countries": [ { "code": "AD", "name": "Andorra", "capital": "Andorra la Vella", "currency": "EUR" }, { "code": "AE", "name": "United Arab Emirates", "capital": "Abu Dhabi", "currency": "AED" }, { "code": "US", "name": "United States", "capital": "Washington D.C.", "currency": "USD,USN,USS" } // ... more countries ] } } ``` Each country object contains exactly the fields you requested. This demonstrates GraphQL's precision in data fetching. ## Working with Dynamic Data Before writing dynamic queries, note that the GraphQL node has a Syntax setting. You can select GraphQL (default) for standard queries and mutations, or Plain to send raw GraphQL payloads. This is useful for advanced or dynamic queries. ### Method 1: Mustache Templates (Simple Approach) For straightforward use cases, you can inject data directly into your queries using Mustache syntax: ```graphql query GetSpecificCountry($countryCode: ID!) { country(code: $countryCode) { name capital currency emoji } } ``` **Set up your input message in function node:** ```javascript msg.countryCode = "FR"; return msg; ``` *When to use: Simple queries with one or two variables that don't need type checking.* ### Method 2: GraphQL Variables For production applications, GraphQL variables provide better security and maintainability: **Query with Variables:** ```graphql query GetCountry($code: ID!) { country(code: $code) { name capital currency languages { name native } } } ``` **Variables Setup:** ```javascript msg.variables = { "code": "JP" }; ``` ### What to Expect in Debug Output When querying a single country with variables, you'll see: ```json { "data": { "country": { "name": "Japan", "capital": "Tokyo", "currency": "JPY", "languages": [ { "name": "Japanese", "native": "日本語" } ] } } } ``` ## Modifying Data with Mutations While queries retrieve data, mutations allow you to **create, update, or delete data**, similar to POST, PUT, and DELETE operations in REST APIs. *Note: The Countries demo API is read-only and does not include mutations. The examples below use a fictional device schema to illustrate how mutations work in Node-RED.* ### Basic Mutation Structure ```graphql mutation CreateNewDevice($input: DeviceInput!) { createDevice(input: $input) { id name model location createdAt success } } ``` ### Setting Up Variables in Node-RED Use `msg.variables` to pass dynamic input to your mutation: ```javascript msg.variables = { "input": { "name": "Raspberry Pi 4A", "model": "Raspberry Pi 4", "location": "Factory Floor 1" } }; return msg; ``` ### What to Expect in Debug Output When a device is successfully created, you'll see: ```json { "data": { "createDevice": { "id": "7", "name": "Raspberry Pi 4A", "model": "Raspberry Pi 4", "location": "Factory Floor 1", "createdAt": "2024-03-21T12:30:45.123Z", "success": true } } } ``` If validation fails, the error structure helps you identify the issue: ```json { "data": { "createDevice": { "id": null, "success": false, "errors": [ { "field": "name", "message": "Name is required" } ] } } } ``` ## Example: Complete Device Management System Here's how you might structure a comprehensive **device management** in GraphQL: *Note: The Device examples are illustrative. The specific types and fields such as DeviceInput, updateDevice, and deactivateDevice must exist in the target GraphQL schema, which can vary depending on the API you are working with.* ### Fetching Devices with Pagination ```graphql query GetDevicesPaginated($limit: Int = 10, $offset: Int = 0, $searchTerm: String) { devices(limit: $limit, offset: $offset, search: $searchTerm) { id name type location lastSeenStatus createdAt lastSeenAt } deviceCount(search: $searchTerm) } ``` *This query supports pagination and search, useful when managing large fleets of devices. Note that some GraphQL APIs use **cursor-based pagination** instead of `limit` and `offset`, so you may need to adapt your query accordingly.* #### Expected Output ```json { "data": { "devices": [ { "id": "1", "name": "Raspberry Pi 4A", "type": "Sensor", "location": "Factory Floor 1", "lastSeenStatus": "Online", "createdAt": "2024-01-15T10:30:00Z", "lastSeenAt": "2024-03-20T14:22:00Z" }, { "id": "2", "name": "Temperature Monitor A1", "type": "Sensor", "location": "Warehouse Section B", "lastSeenStatus": "Online", "createdAt": "2024-02-10T08:15:00Z", "lastSeenAt": "2024-03-21T09:10:00Z" } ], "deviceCount": 6 } } ``` ### Creating New Devices ```graphql mutation CreateDevice($input: DeviceInput!) { createDevice(input: $input) { id name type location createdAt success errors { field message } } } ``` *Always return `success` and any validation errors to confirm the device was created properly.* #### Expected Output (Success) ```json { "data": { "createDevice": { "id": "7", "name": "Smart Thermostat", "type": "Controller", "location": "Office Area", "createdAt": "2024-03-21T12:30:00Z", "success": true, "errors": [] } } } ``` ### Updating Existing Devices ```graphql mutation UpdateDevice($id: ID!, $input: DeviceUpdateInput!) { updateDevice(id: $id, input: $input) { id name type location lastSeenStatus updatedAt success } } ``` #### Expected Output ```json { "data": { "updateDevice": { "id": "1", "name": "Raspberry Pi 4A", "type": "Sensor", "location": "Factory Floor 3", "lastSeenStatus": "Maintenance", "updatedAt": "2024-03-21T13:45:00Z", "success": true } } } ``` ### Deleting Devices (Soft Delete) ```graphql mutation DeactivateDevice($id: ID!) { deactivateDevice(id: $id) { id isActive deactivatedAt success } } ``` *Soft deletes allow you to retain historical device data for audits and compliance.* #### Expected Output ```json { "data": { "deactivateDevice": { "id": "4", "isActive": false, "deactivatedAt": "2024-03-21T14:00:00Z", "success": true } } } ``` ## Advanced Techniques ### Custom Headers for Authentication and Metadata Some GraphQL APIs require additional headers for authentication or client identification. You can add them in the message object before sending the request: ```javascript msg.customHeaders = { "Authorization": "Bearer xyz", "X-API-Version": "v2", "X-Client-ID": "node-red-integration" }; return msg; ``` ### Using Fragments for Code Reusability When queries start to grow, you'll often find yourself requesting the same fields across multiple operations. Fragments let you define those fields once and reuse them, keeping queries clean and consistent. ```graphql fragment DeviceBasicInfo on Device { id name type location createdAt } fragment DeviceOperationalInfo on Device { ...DeviceBasicInfo lastSeenStatus lastSeenAt maintenanceDue } query GetDeviceProfile($deviceId: ID!) { device(id: $deviceId) { ...DeviceOperationalInfo } } ``` *Here, `DeviceBasicInfo` is reused inside `DeviceOperationalInfo`, so you can easily expand or maintain your schema without duplicating fields.* #### Expected Output with Fragments The output includes all fields from both fragments combined: ```json { "data": { "device": { "id": "1", "name": "Raspberry Pi 4A", "type": "Sensor", "location": "Factory Floor 1", "createdAt": "2024-01-15T10:30:00Z", "lastSeenStatus": "Online", "lastSeenAt": "2024-03-20T14:22:00Z", "maintenanceDue": "2024-06-15T00:00:00Z" } } } ``` Notice how the response includes all fields from `DeviceBasicInfo` (id, name, type, location, createdAt) plus the additional fields from `DeviceOperationalInfo` (lastSeenStatus, lastSeenAt, maintenanceDue). This demonstrates how fragments compose together to build the complete response. ## Complete Example Flow The following example flow demonstrates creating, reading, updating, and deleting data using GraphQL, including performing queries with fragments for reusable field selections. This flow and the GraphQL node are for demonstration purposes only and do not include a demo API. ::render-flow ```json [{"id":"7cb18349cfb7f014","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Get Single Device By ID","graphql":"fd163a325aa21cdb","format":"text","template":"query GetDevice($id: ID!) {\n device(id: $id) {\n id\n name\n type\n model\n location\n lastSeenStatus\n maintenanceDue\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":490,"y":240,"wires":[["1515cbad98355c5f"],["0c4068bd1821923b"]]},{"id":"6ac3b625b7e6e954","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{\"id\":\"1\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":240,"wires":[["7cb18349cfb7f014"]]},{"id":"66d017b14ed8c60d","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Get Devices","graphql":"fd163a325aa21cdb","format":"text","template":"query GetDevices {\n devices {\n id\n name\n type\n location\n lastSeenStatus\n createdAt\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":450,"y":140,"wires":[["fdcc15f68872b4d0"],["beb63abd92f155ce"]]},{"id":"572aa9e659f5e27e","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":140,"wires":[["66d017b14ed8c60d"]]},{"id":"6d4f9a2cb159d8e5","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Create Device","graphql":"fd163a325aa21cdb","format":"text","template":"mutation CreateDevice($input: DeviceInput!) {\n createDevice(input: $input) {\n id\n name\n type\n model\n location\n createdAt\n success\n errors {\n field\n message\n }\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":460,"y":340,"wires":[["f5527e179e1c8911"],["9fc70e8f8715f994"]]},{"id":"e0b89bf0a47dbae3","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{\"input\":{\"name\":\"Smart Thermostat\",\"type\":\"Controller\",\"model\":\"Nest V3\",\"location\":\"Office Area\"}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":340,"wires":[["6d4f9a2cb159d8e5"]]},{"id":"fdcc15f68872b4d0","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":120,"wires":[]},{"id":"beb63abd92f155ce","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":160,"wires":[]},{"id":"1515cbad98355c5f","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":220,"wires":[]},{"id":"0c4068bd1821923b","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":260,"wires":[]},{"id":"f5527e179e1c8911","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":320,"wires":[]},{"id":"9fc70e8f8715f994","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":360,"wires":[]},{"id":"e0d2739c55386644","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Create Device","graphql":"fd163a325aa21cdb","format":"text","template":"mutation CreateDevice($input: DeviceInput!) {\n createDevice(input: $input) {\n id\n name\n type\n model\n location\n createdAt\n success\n errors {\n field\n message\n }\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":460,"y":440,"wires":[["f5c2e0346b4979aa"],["597bac49938cd7bc"]]},{"id":"b45422c8bffaa52d","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{\"input\":{\"name\":\"Smart Thermostat\",\"type\":\"Controller\",\"model\":\"Nest V3\",\"location\":\"Office Area\"}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":440,"wires":[["e0d2739c55386644"]]},{"id":"f5c2e0346b4979aa","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":420,"wires":[]},{"id":"597bac49938cd7bc","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":460,"wires":[]},{"id":"dae18c118b5a7d25","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Update Device","graphql":"fd163a325aa21cdb","format":"text","template":"mutation UpdateDevice($id: ID!, $input: DeviceUpdateInput!) {\n updateDevice(id: $id, input: $input) {\n id\n name\n location\n lastSeenStatus\n updatedAt\n success\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":460,"y":540,"wires":[["8372c53fd374ca0c"],["81ee27d5d002cdf7"]]},{"id":"06e715476d4a05a1","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{ \"id\": \"1\", \"input\": { \"location\": \"Factory Floor 3\", \"lastSeenStatus\": \"Maintenance\" } }","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":540,"wires":[["dae18c118b5a7d25"]]},{"id":"8372c53fd374ca0c","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":520,"wires":[]},{"id":"81ee27d5d002cdf7","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":560,"wires":[]},{"id":"b59476221e135579","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Deactivate Device","graphql":"fd163a325aa21cdb","format":"text","template":"mutation DeactivateDevice($id: ID!) {\n deactivateDevice(id: $id) {\n id\n isActive\n deactivatedAt\n success\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":470,"y":640,"wires":[["bc0b5067c9b64b4a"],["9bcd78c44b2d304a"]]},{"id":"2f45ba641ea8d272","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{ \"id\": \"4\" }","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":640,"wires":[["b59476221e135579"]]},{"id":"bc0b5067c9b64b4a","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":620,"wires":[]},{"id":"9bcd78c44b2d304a","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":660,"wires":[]},{"id":"7d7e6689f0c42fac","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"fragment","graphql":"fd163a325aa21cdb","format":"text","template":"fragment DeviceBasicInfo on Device {\n id\n name\n type\n location\n createdAt\n}\n\nfragment DeviceOperationalInfo on Device {\n ...DeviceBasicInfo\n lastSeenStatus\n lastSeenAt\n maintenanceDue\n}\n\nquery GetDeviceProfile($deviceId: ID!) {\n device(id: $deviceId) {\n ...DeviceOperationalInfo\n }\n}","syntax":"mustache","token":"","showDebug":false,"x":440,"y":740,"wires":[["e818060c00087930"],["621b732245671f2f"]]},{"id":"ac37b7540e536255","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{ \"deviceId\": \"4\" }","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":740,"wires":[["7d7e6689f0c42fac"]]},{"id":"e818060c00087930","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":720,"wires":[]},{"id":"621b732245671f2f","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":760,"wires":[]},{"id":"f99baa69918d2a9a","type":"graphql","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Pagination","graphql":"fd163a325aa21cdb","format":"text","template":"query GetDevicesPaginated($limit: Int = 10, $offset: Int = 0, $searchTerm: String) {\n devices(limit: $limit, offset: $offset, search: $searchTerm) {\n id\n name\n type\n location\n lastSeenStatus\n createdAt\n lastSeenAt\n }\n deviceCount(search: $searchTerm)\n}","syntax":"mustache","token":"","showDebug":false,"x":450,"y":840,"wires":[["368fbd3d4f756b6a"],["cc1ef9d881065082"]]},{"id":"509af177c480ed03","type":"inject","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"","props":[{"p":"variables","v":"{ \"deviceId\": \"4\" }","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":840,"wires":[["f99baa69918d2a9a"]]},{"id":"368fbd3d4f756b6a","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":820,"wires":[]},{"id":"cc1ef9d881065082","type":"debug","z":"98a60b6dd0896e47","g":"9a0c28902989b739","name":"Error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":860,"wires":[]},{"id":"fd163a325aa21cdb","type":"graphql-server","name":"","endpoint":"none","token":""},{"id":"574504396881aa85","type":"global-config","env":[],"modules":{"node-red-contrib-graphql":"2.2.0"}}] ``` :: # Using Different Technologies for Building Applications with Node-RED. Developing powerful and scalable applications frequently necessitates the integration of various technologies. This integration is crucial for creating seamless and efficient systems that can handle complex tasks and large volumes of data. Whether you're working with REST APIs to enable communication between different services or implementing GraphQL for more efficient data querying, the ability to blend these technologies effectively is essential for modern application development. Node-RED offers a versatile and robust platform to meet these needs. Its intuitive flow-based interface allows developers to easily design and deploy sophisticated workflows, which makes it an invaluable tool for integrating REST APIs, GraphQL, webhooks, and more. ## Resources Here are some resources to help you integrate Node-RED with various different technologies: - [Creating REST API's with Node-RED](https://flowfuse.com/docs/node-red/integration-technologies/rest/): Learn how to create REST APIs in Node-RED and fetch data from an API. - [Integrating GraphQL APIs in Node-RED](https://flowfuse.com/docs/node-red/integration-technologies/graphql/): Learn how to integrate GraphQL APIs in Node-RED. This guide covers setting up endpoints, executing queries, handling variables, and using mutations for dynamic data. - [Using Webhook with Node-RED](https://flowfuse.com/docs/node-red/integration-technologies/webhook/): Learn how to seamlessly integrate webhooks into your Node-RED applications for automating tasks and enhancing communication. # Creating REST API's with Node-RED REST APIs are how applications talk to each other over the web. They use standard HTTP methods (GET, POST, PUT, DELETE) to send and receive data, usually in JSON format. This guide shows you how to build your own REST APIs in Node-RED and how to pull data from existing APIs. ## Creating a GET API 1. Drag an "http-in" onto the workspace, double click on it and select the Method to for which operation you need, set URL endpoint. 2. Drag an chagne node onto the workspace and set the `msg.payload` to data you want to send as response. 3. Then Drag an http response node, in it and set the status code if want. 4. Connect the "http-in" node's output to the input of the function node and the function node's output to the input of the http response node. ::render-flow ```json [{"id":"27333f67794bdc72","type":"http in","z":"977143edb097b685","name":"","url":"/test","method":"get","upload":false,"swaggerDoc":"","x":320,"y":220,"wires":[["f351033226953150"]]},{"id":"a7ee48616541a36a","type":"http response","z":"977143edb097b685","name":"","statusCode":"200","headers":{},"x":760,"y":220,"wires":[]},{"id":"dcfc8d1126f139d5","type":"comment","z":"977143edb097b685","name":"Http-in node created API sending todo list as response","info":"","x":540,"y":140,"wires":[]},{"id":"f351033226953150","type":"change","z":"977143edb097b685","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"todos","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":220,"wires":[["a7ee48616541a36a"]]}] ``` :: ## Creating a POST, PUT, and DELETE API 1. Drag an "HTTP In" node onto the workspace. Double-click on it and select the desired method (POST, PUT, DELETE). 2. Add a node to the canvas based on your application's needs. For example, if you've selected DELETE, you may use a Change node to perform operations to delete data stored in Node-RED context. Set `msg.payload` to the response data you want to send, make sure the msg.payload is originated from "http-in" node. 3. Drag an "HTTP Response" node onto the workspace. Configure it and set the status code if needed. 4. Connect the output of the "HTTP In" node to the input of the node handling your application logic (e.g., Change node for DELETE operation). Then, connect the output of this node to the input of the HTTP Response node. ::render-flow ```json [{"id":"8893fc84b3391b34","type":"http in","z":"977143edb097b685","name":"","url":"/todo/delete","method":"delete","upload":true,"swaggerDoc":"","x":250,"y":720,"wires":[["088808484586fdc1"]]},{"id":"04a60e7af4d6d522","type":"http response","z":"977143edb097b685","name":"","statusCode":"204","headers":{},"x":740,"y":720,"wires":[]},{"id":"088808484586fdc1","type":"function","z":"977143edb097b685","name":"Delete the todo item","func":"let todoList = global.get('todos') || [];\nlet id = msg.payload.id;\n\n// Find the index of the item to delete\nlet index = todoList.findIndex(item => item.id === id);\n\nif (index !== -1) {\n // Remove the item from the todoList array\n todoList.splice(index, 1);\n global.set('todos', todoList);\n msg.payload = \"Item deleted successfully.\";\n msg.statusCode = 204; // No Content\n} else {\n msg.payload = \"Item not found.\";\n msg.statusCode = 404; // Not Found\n}\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":720,"wires":[["04a60e7af4d6d522"]]}] ``` :: ::render-flow ```json [{"id":"825a6296456b7c27","type":"http in","z":"977143edb097b685","name":"","url":"/todo","method":"post","upload":true,"swaggerDoc":"","x":260,"y":1460,"wires":[["0133494821cf99ca","9fe48410514631f8"]]},{"id":"0133494821cf99ca","type":"debug","z":"977143edb097b685","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":540,"y":1420,"wires":[]},{"id":"5284a06365a7f8f7","type":"http response","z":"977143edb097b685","name":"","statusCode":"201","headers":{},"x":860,"y":1500,"wires":[]},{"id":"9fe48410514631f8","type":"function","z":"977143edb097b685","name":"store todo in todolist ","func":"let todoList = global.get('todos') || [];\nlet newTodo = msg.payload;\n\ntodoList.push(newTodo);\nglobal.set('todos',todoList)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":1500,"wires":[["5284a06365a7f8f7"]]}] ``` :: ::render-flow ```json [{"id":"8893fc84b3391b34","type":"http in","z":"977143edb097b685","name":"","url":"/todo/update","method":"put","upload":true,"swaggerDoc":"","x":400,"y":280,"wires":[["088808484586fdc1"]]},{"id":"04a60e7af4d6d522","type":"http response","z":"977143edb097b685","name":"","statusCode":"200","headers":{},"x":900,"y":280,"wires":[]},{"id":"088808484586fdc1","type":"function","z":"977143edb097b685","name":"update the todo item","func":"let todoList = global.get('todos') || [];\nlet id = msg.payload.id;\nlet newTodo = msg.payload.newtodo;\n\n// Find the index of the item to update\nlet index = todoList.findIndex(item => item.id === id);\n\nif (index !== -1) {\n // Update the todo item\n todoList[index].task = newTodo;\n global.set('todos', todoList);\n msg.payload = \"Item updated successfully.\";\n msg.statusCode = 200; // OK\n} else {\n msg.payload = \"Item not found.\";\n msg.statusCode = 404; // Not Found\n}\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":280,"wires":[["04a60e7af4d6d522"]]},{"id":"ea3e7e96bbccf530","type":"comment","z":"977143edb097b685","name":"Http in node created api for updating the todo item","info":"","x":650,"y":200,"wires":[]}] ``` :: For more details, refer to the [CRUD API Blueprint](https://flowfuse.com/blueprints/getting-started/crud/), where we have created CRUD APIs to store, retrieve, delete, and update the data from MongoDB database. ## Securing Your APIs APIs without security are open doors to your application. Here are essential practices for protecting your endpoints: ### Authentication The simplest approach is HTTP Basic Authentication. Add authentication to your http-in nodes: 1. Open your **http-in** node 2. Enable "Use authentication" 3. Set a username and password Node-RED will reject requests without valid credentials. For production systems, consider more robust options like: - **API Keys**: Send a secret key in headers that your flow validates - **OAuth 2.0**: Industry-standard authorization for third-party access - **JWT Tokens**: Stateless authentication tokens that carry user information ### Rate Limiting Prevent abuse by limiting how often someone can hit your endpoints. Use the `node-red-contrib-rate-limit` node to throttle requests: ```text npm install node-red-contrib-rate-limit ``` Place it after your http-in node to block excessive requests from the same source. ### HTTPS Only Never expose APIs over plain HTTP in production. Always use HTTPS to encrypt data in transit. If you're using FlowFuse, HTTPS is handled automatically. For self-hosted instances, configure Node-RED behind a reverse proxy (nginx, Apache) with SSL certificates. ### Input Validation Always validate incoming data. Don't trust anything users send: ```javascript // In a function node if (!msg.payload.id || typeof msg.payload.id !== 'string') { msg.statusCode = 400; msg.payload = { error: "Invalid ID" }; return msg; } ``` Check data types, required fields, and acceptable values before processing. ### CORS Configuration If your API is called from web browsers, configure CORS properly. Add an **http response** node and set headers: ```text Access-Control-Allow-Origin: https://yourdomain.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization ``` Never use `*` for `Allow-Origin` in production, specify exact domains. ## Example: Reading Data Now that you've learned how to create REST APIs in Node-RED, let's explore an example of reading data using a HTTP GET request. This example will demonstrate how to fetch data from an external API and process it and display on dashboard chart. For the example we will fetch the data of [Node-RED Dashboard 2.0](https://flowfuse.com/platform/dashboard/) Downloads from npm registry api. `https://api.npmjs.org/downloads/range/last-month/@flowforge/node-red-dashboard`. A simple flow to fetch data from npm registry would be: ::render-flow ```json [{"id":"32b083d0ca67265f","type":"inject","z":"977143edb097b685","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":1100,"wires":[["53db14b9a848d5ce"]]},{"id":"53db14b9a848d5ce","type":"http request","z":"977143edb097b685","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.npmjs.org/downloads/range/last-month/@flowforge/node-red-dashboard","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":410,"y":1100,"wires":[["9e08fa8d25a19f24"]]},{"id":"9e08fa8d25a19f24","type":"debug","z":"977143edb097b685","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":580,"y":1100,"wires":[]}] ``` :: Where we paste the API URL into the settings panel: !["HTTP GET URL setting"](https://flowfuse.com/docs/node-red/integration-technologies/images/http-get-npmapi.png "HTTP GET URL setting") When running this flow you'll see a blob of text in the `Debug` pane. This is a great first start, but a blob isn't useful for the rest of the flow. We need to parse the data as JSON. While the [JSON node](https://flowfuse.com/docs/node-red/core-nodes/parsers/json/) would work, the HTTP request node can do this natively. Let `a parsed JSON object` the `Return` settings of the HTTP request node. So now we got the data, and a little more than we need, so let's change the message output to keep only what we're interested in; `payload.downloads`. To do this, we'll use the [change node](https://flowfuse.com/docs/node-red/core-nodes/function/change/). ![Change node to set the payload with downloads](https://flowfuse.com/docs/node-red/integration-technologies/images/change-node-set-downloads-payload.png "Change node to set the payload") ### Building the Dashboard Follow the [Dashboard getting started guide](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to get up and running. Now we drag in the `chart` node that's available after installing the dashboard package and make sure it' input comes from the configured `change` node. Before hitting the deploy button the dashboard itself needs configuring: First add configuration for the `ui-group`: ![Configure the UI Group](https://flowfuse.com/docs/node-red/integration-technologies/images/dashboard-config-chart.png "Configure the chart") To setup the `ui-group` correctly you'll need to add configuration for the `ui-page`: !["Configure the ui-group"](https://flowfuse.com/docs/node-red/integration-technologies/images/dashboard-config-ui-group.png "Configure the UI group"). To create the UI page it requires another 2 config settings, `ui-base`, and the theming through `ui-theme`. ![Configure the UI Base](https://flowfuse.com/docs/node-red/integration-technologies/images/dashboard-config-ui-base.png) The default theme is great, so just accept that, and save all dialogs to continue the chart creation. #### Normalizing the data The data for the chart needs to be changed before we can show it. The messages should have a `x` and `y` key. So let's prepare the data with a combination of the [Split](https://flowfuse.com/docs/node-red/core-nodes/sequence/split/) and change node. The Split node with the default configuration allows to 30 elements of the array to be mapped individually. The change node will set the `payload.x` and `payload.y` on the message: ![Change node to prepare the data for a chart](https://flowfuse.com/docs/node-red/integration-technologies/images/change-node-prepare-data-chart.png "Prepare data for the chart") Connect the change node output to a new chart node, and voila: ![Data in the chart node](https://flowfuse.com/docs/node-red/integration-technologies/images/chart-with-data.png) ### Keeping the data up-to-date While we created a chart and it has some data, there's one more thing to explain. How can the data be kept up-to-date? It's straight forward to have the `Inject` node [run every night](https://flowfuse.com/docs/node-red/core-nodes/common/inject/), but the chart would now have multiple data points for the same day. This paints multiple lines on top of each other. While that works, the hover of the chart will display the duplication and it's wastefull. So before we update the chart we need to send a message to the chart where the [payload is `[]`](https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html#removing-data){rel=""nofollow""}. That way the chart is emptied first, and right afterwards it will receive the new data to write. ::render-flow ```json [{"id":"da9a67e8c3ea7742","type":"inject","z":"977143edb097b685","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"00 12 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":250,"y":960,"wires":[["efd22a89abc3c06f","a6851a41dba2c39b"]]},{"id":"a6851a41dba2c39b","type":"http request","z":"977143edb097b685","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.npmjs.org/downloads/range/last-month/@flowforge/node-red-dashboard","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":430,"y":960,"wires":[["7931f7457880f7c3"]]},{"id":"7931f7457880f7c3","type":"change","z":"977143edb097b685","name":"Only get the Downloads","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.downloads","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":960,"wires":[["74e3b15c7b09726a"]]},{"id":"74e3b15c7b09726a","type":"link out","z":"977143edb097b685","name":"link out 1","mode":"link","links":["43e4aff34b989e83"],"x":815,"y":960,"wires":[]},{"id":"43e4aff34b989e83","type":"link in","z":"977143edb097b685","name":"Normalize daily data","links":["74e3b15c7b09726a"],"x":195,"y":1040,"wires":[["ca8c62bfbdf75715"]]},{"id":"ca8c62bfbdf75715","type":"split","z":"977143edb097b685","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":310,"y":1040,"wires":[["3f462d2c7e3bca50"]]},{"id":"3f462d2c7e3bca50","type":"change","z":"977143edb097b685","name":"Prepare data for the chart","rules":[{"t":"set","p":"payload.x","pt":"msg","to":"$toMillis(payload.day)","tot":"jsonata"},{"t":"set","p":"payload.y","pt":"msg","to":"payload.downloads","tot":"msg"},{"t":"delete","p":"payload.day","pt":"msg"},{"t":"delete","p":"payload.downloads","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":1040,"wires":[["f4e6a85b8cb8dac0"]]},{"id":"4b71fc28c2da66e7","type":"ui-chart","z":"977143edb097b685","group":"bac8effac57694e1","name":"","label":"Daily Downloads","order":9007199254740991,"chartType":"line","xAxisType":"time","removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":0,"height":0,"className":"","x":330,"y":1120,"wires":[[]]},{"id":"f4e6a85b8cb8dac0","type":"link out","z":"977143edb097b685","name":"link out 2","mode":"link","links":["6f7068445bfe4311"],"x":675,"y":1040,"wires":[]},{"id":"6f7068445bfe4311","type":"link in","z":"977143edb097b685","name":"Update the chart","links":["f4e6a85b8cb8dac0","79067215ee592ec9","efd22a89abc3c06f"],"x":195,"y":1120,"wires":[["4b71fc28c2da66e7"]]},{"id":"efd22a89abc3c06f","type":"link out","z":"977143edb097b685","name":"link out 3","mode":"link","links":["6f7068445bfe4311"],"x":415,"y":920,"wires":[]},{"id":"bac8effac57694e1","type":"ui-group","name":"NPM Downloads","page":"f10b4d0259e43aeb","width":"6","height":"1","order":-1},{"id":"f10b4d0259e43aeb","type":"ui-page","name":"Main","ui":"cb79bc4520925e32","path":"/","layout":"grid","theme":"2c5d702b11de7dd1","order":-1},{"id":"cb79bc4520925e32","type":"ui-base","name":"My UI","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false},{"id":"2c5d702b11de7dd1","type":"ui-theme","name":"Theme Name","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}] ``` :: # Using Webhook with Node-RED Webhooks let different systems talk to each other automatically when something happens. Instead of constantly asking "anything new?", one system just tells the other "hey, this just happened." This guide shows you how to set up webhooks in Node-RED, using a real manufacturing example where temperature sensors trigger maintenance alerts. ## What are Webhooks? A webhook is basically an automated HTTP request that fires when a specific event occurs. Think of it like setting up a notification system between two apps, when something happens in App A, it immediately sends a message to App B with all the relevant details. The technical term is "user-defined HTTP callbacks," but here's what that means in practice: you tell a system "when X happens, send this data to this URL." From then on, it handles everything automatically. ## How Webhook works !["Image displaying how webhook works"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-how-webhook-works.png "Image displaying how webhook works"){dataZoomable=""} - Event Initiator: This refers to the event specified to trigger the WebHook. Whenever this event occurs, the WebHook will be triggered. - Webhook Server: The webhook server is responsible for managing webhook configurations and endpoints. It listens for the specified event. When the event is detected, the webhook server automatically sends an HTTP POST request containing relevant data to the designated third-party application or service. - Data Reception by Third-Party Application: The third-party application will receive the data sent via the WebHook to the designated URL or listener provided during registration. - Custom Action Execution: Upon receiving the POST request, specific actions can be performed. ## API Vs Webhook It's common and understandable to get confused between APIs and webhooks, especially when you are learning about webhooks for the first time. However, comparing the two can help dispel these confusions. | Aspect | API | Webhook | | -------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | | Direction | Typically involves client-to-server communication. | Typically involves server-to-server communication. | | Initiation | The client initiates requests. | The server initiates requests. | | Request Method | Usually employs HTTP methods like GET, POST, etc. | Typically uses the HTTP POST method. | | Response | Provides an immediate response upon request. | Does not provide an immediate response; asynchronous. | | Data Transfer | Utilizes a pull model where the client fetches data. | Operates on a push model where the server pushes data to the client. | | Polling | Requires periodic polling for updates. | No need for polling; receives updates directly. | | Payload | The client specifies the payload in the request. | The server defines the payload in the outgoing request. | | Error Handling | Typically includes error codes and messages. | Errors are handled by retry mechanisms or manual intervention. | ### Example Scenario: Consider a manufacturing facility that utilizes temperature sensors to monitor temperature levels in critical areas. When the temperature falls or exceeds predefined thresholds, it triggers a series of actions for maintenance and monitoring. !["Diagram explaining how component works in Webhook"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-diagram.png "Diagram explaining how component works in Webhook"){dataZoomable=""} - Raspberry Pi with connected temperature sensor (Server 1): Physical sensors are installed in the manufacturing facility and connected to a Raspberry Pi running Node-RED for reading and monitoring temperature data. The application running on Node-RED triggers webhook requests to Server 2 whenever abnormal temperature patterns are detected. - Webhook Server (Server 2): This server creates and hosts the webhook endpoint. It receives HTTP requests from (Server 1) when abnormal temperatures are detected. The request contains temperature data. Server 2 then processes this data and sends a POST request with relevant information to Server 3. - Maintenance System (Server 3): This system receives POST requests from Server 2 containing event-related data on a specific endpoint provided to Server 2. It then automatically schedules maintenance tasks based on the received information. ## Practical implementation In this section, we will construct the practical implementation of the scenario described above. all three components or servers will be hosted on the seprate Node-RED instance in our example. ### Setting Up a Webhook (Server 2) Having a separate server for webhooks is crucial as it will receive data from multiple sensors. You might wonder why we need a separate Server 2 instead of using one server running on the Raspberry Pi (Server 1) to send data directly to Server 3. The answer is simple: the Raspberry Pi is hardware with limited memory and power, which can slow down communication if the server running on it receives a lot of traffic. Therefore, running a separate Node-RED instance on each Raspberry Pi and having one centralized separate webhook server is necessary. This central server, running on the cloud, will have significantly more power and resources to handle the incoming traffic efficiently. 1. Drag an **http-in** node onto the canvas. Configure the method as POST and set the path as **/test-webhook**. !["Screenshot displaying webhook http-in nodes configuration"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-http-in-node-endpoint-for-receiving-data-from-server-2.png "Screenshot displaying webhook http-in nodes configuration"){dataZoomable=""} 2. Drag an **http request** node onto the canvas. Configure the method as POST and set the URL to `https://.flowfuse.cloud/schedule-maintenance`, replace :your-instance-name[with your actual name of the instance. **/schedule-maintenance** will be the endpoint for posting requests to the maintenance monitoring system provided by Server 3.] !["Screenshot displaying http request nodes configuration for sending post request to server 3"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-request-node-sending-request-to-server3.png "Screenshot displaying http request nodes configuration for sending post request to server 3"){dataZoomable=""} 3. Drag an **http response** node onto the canvas and connect its input to the output of the http-in node. Also, connect an http request node's input to the same http in the node's output. ## Setting Up a Temperature sensors For this practicle, the DHT11 sensor is connected to a Raspberry Pi 4, which is running the FlowFuse device agent. Node-RED on the Raspberry Pi allows direct reading and monitoring of sensor data, while the FlowFuse device agent enables remote editing and management of Node-RED applications from anywhere in the world. For more details, refer to the [Running the FlowFuse Device Agent as a service on a Raspberry Pi](https://flowfuse.com/blog/2023/05/device-agent-as-a-service/). ### Installing custom node for reading sensor data 1. Click the Node-RED Settings (top-right). 2. Click "Manage Palette." 3. Switch to the "Install" tab. 4. Search for `node-red-contrib-dht-sensor`. 5. Click "Install" ### Reading and formatting sensor data Before proceeding with this step, it is necessary to run Node-RED on your Raspberry Pi as a superuser and ensure that the DHT11 sensor is correctly connected with wires. Also, make sure to install the [BCM2835](https://www.airspayce.com/mikem/bcm2835/){rel=""nofollow""}. 1. Drag an **inject** node onto the canvas and set the interval to your preference so that it triggers readings after a specific interval of time. !["Screenshot displaying the rpi-dht22 node's configuration for reading data from dht 11 sensor"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-dht-sensor-node.png "Screenshot displaying the rpi-dht22 node's configuration for reading data from dht 11 sensor"){dataZoomable=""} 2. Drag an **rpi-dht22** sensor node onto the canvas. This node will return an object containing humidity, temperature (as the payload), etc. 3. Select the sensor model. Since I am using the DHT11 sensor, I have selected "DHT11." 4. Choose the pin numbering as **BCM GPIO**. 5. Select the GPIO pin to which your sensor's data output is connected. 6. Drag the **change** node onto canvas. 7. Set `msg.payload` to `{"Temperature":$number(payload),"name":topic}` as JSON expression. !["Screenshot of the change node formating sensor data"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-change-node-formating-sensor-data.png "Screenshot of the change node formating sensor data"){dataZoomable=""} 8. Connect the **inject** node's output to the **rpi-dht22** node's input and **rpi-dht22** node's output to **change** node's input. ## Monitoring Temperature ( Server 1 ) 1. Drag a **switch** node onto the canvas, click on it, and set up three conditions: one to check if the temperature is less than 20, the second to check if the temperature is greater than 30, and the last one for other cases. !["Screenshot displaying the switch node with conditions checking whether the temperature is normal or not"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-switch-node.png "Screenshot displaying the switch node with conditions checking whether the temperature is normal or not"){dataZoomable=""} 2. Drag an **http request** node onto the canvas, click on it, set the method as POST, and set the URL as `https://.flowfuse.cloud/test-webhook` !["Screenshot displaying HTTP request node configuration for triggering or sending a POST request to the webhook server in case of abnormal temperature."](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-webhook-trigger.png "Screenshot displaying HTTP request node configuration for triggering or sending a POST request to the webhook server in case of abnormal temperature."){dataZoomable=""} 3. Connect the **change** node's output to the switch node's input and the **http request** node’s output to the first and second output of the switch node. then connect the third output of the switch node to the debug node. ## Setting Up a Server 3 Before moving further install Dashboard 2.0 as we will display the scheduled maintenance on the table, For more information for more information refer to [Getting started with Dashboard 2.0](https://flowfuse.com/blog/2024/03/dashboard-getting-started/). 1. Drag the **http in** node onto canvas, select the method as POST, and set the method as **/schedule-maintenance**. !["Screenshot displaying HTTP In node configuration for creating the POST request endpoint."](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-http-in-node-endpoint-for-receiving-data-from-server-2.png "Screenshot displaying HTTP In node configuration for creating the POST request endpoint."){dataZoomable=""} 2. Drag a **change** node onto the canvas, and set `msg.payload` to `msg.req.body`. Name this node "Set payload as request body." !["Screenshot displaying the change node setting payload as request body"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-change1-node.png "Screenshot displaying the change node setting payload as request body"){dataZoomable=""} 3. Drag another **change** node onto the canvas, and set `msg.payload` as `{ "ocured_at":$moment(), "temperature": payload.temperature, "name": payload.name }` as JSON expression. Name this node "Format the payload." !["Screenshot displaying the change node formating sensor data"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-change2-node.png "Screenshot displaying the change node formating sensor data"){dataZoomable=""} 4. Drag the **function** node onto Canvas and copy the below code in it. ```js // Retrieve or initialize scheduled maintenance data let scheduledMaintenanceData = global.get('scheduledMaintenance') || []; // Randomly assign maintenance task let assignedTo = Math.random() < 0.5 ? "Bob Smith": "Alice Walker"; msg.payload.assignedTo = assignedTo // Add recent maintenance data to records scheduledMaintenanceData.push(maintenanceScheduleRecentData); // Update scheduled maintenance data to global context global.set('scheduledMaintenance', scheduledMaintenanceData); return msg; ``` !["Screenshot displaying function node processing and storing data to global context"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-function-node.png "Screenshot displaying function node processing and storing data to global context"){dataZoomable=""} 5. Drag the **http response** node onto the canvas. 6. Drag another **change** node onto the canvas and set `msg.payload` to `global.scheduledMaintenance`. Name this node "Retrieve data from global context." !["Screenshot displaying the change node retriving data"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-change-node.png "Screenshot displaying the change node retriving data"){dataZoomable=""} 7. Drag the **ui-table** widget onto Canvas, and create a new **ui-group** for it in which it will render. 8. Connect the output of the **http in** node to the input of the "Set payload as request body" **change** node. 9. Connect the output of the "Set payload as request body" **change** node to the input of the "Format the payload" **change** node, and subsequently, connect the output of the "Format the payload" **change** node to the input of the **function** node. 10. Connect the output of the **function** node to the input of the **http response** node, and connect the output of the "Retrieve data from global context" **change** node to the input of the **ui-table** widget. ### Deploying the flow !["Screenshot Displaying the flow of server 1"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-server-1-instance.png "Screenshot Displaying the flow of server 1"){dataZoomable=""} !["Screenshot Displaying the flow of server 2"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-server-2-instance.png "Screenshot Displaying the flow of server 2"){dataZoomable=""} !["Screenshot Displaying the flow of server 3"](https://flowfuse.com/docs/node-red/integration-technologies/images/using-webhook-with-node-red-server-3-instance.png "Screenshot Displaying the flow of scheduled maintenance table"){dataZoomable=""} 1. With your flow updated to include the above, click the "Deploy" button in the top-right corner of the Node-RED Editor in each Node-RED instance. 2. In server 3 Node-RED instance (Maintenance scheduling system), Locate the 'Open Dashboard' button at the top-right corner of the Dashboard 2.0 sidebar and click on it to navigate to the dashboard. :video{ariaLabel="Video displaying the flow of scheduled maintenance table" autoPlay="true" height="338" loop="true" muted="true" playsInline="true" preload="none" width="600"} # Node-RED Keyboard Shortcuts Using keyboard shortcuts in Node-RED helps you work faster by making it easier to navigate, edit, and manage your flows. Below is a list of Node-RED keyboard shortcuts: ### General Shortcuts - **Ctrl + e / ⌘ + e**: Open the export dialog. - **Ctrl + i / ⌘ + i**: Open the import dialog. - **Ctrl + z / ⌘ + z**: Undo the last action. - **Ctrl + y / ⌘ + y**: Redo the last undone action. - **Ctrl + d / ⌘ + d**: Deploy flow. - **Ctrl + Space / ⌘ + Space**: Open/Close sidebar. - **Ctrl + p / ⌘ + p**: Open/Close node palette. - **Ctrl + Shift + p / ⌘ + Shift + p**: Show action list. - **Ctrl + Shift + l / ⌘ + Shift + l**: Show event log. - **Alt + Shift + p / ⌥ + Shift + p**: Open Manage palette. - **Shift + , / ⇧ + ,**: Open keyboard shortcuts settings. - **Ctrl + Alt + r / ⌘ + ⌥ + r**: Show remote difference/Review changes. - **Ctrl + g, then c / ⌘ + g, then c**: Open configuration nodes tab in the sidebar. - **Ctrl + g, then i / ⌘ + g, then i**: Open information tab in the sidebar. - **Ctrl + g, then h / ⌘ + g, then h**: Open help tab in the sidebar. - **Ctrl + g, then d / ⌘ + g, then d**: Open debug panel in the sidebar. - **Alt + Alt + l / ⌥ + ⌥ + l**: Clear the debug panel. - **Ctrl + Enter / ⌘ + Enter**: Confirm edit tray. - **Ctrl + Escape / ⌘ + Escape**: Cancel edit tray. ### Workspace - **Ctrl + + / ⌘ + +**: Zoom in. - **Ctrl + - / ⌘ + -**: Zoom out. - **Ctrl + 0 / ⌘ + 0**: Reset zoom to 100%. - **Ctrl + , / ⌘ + ,**: Customize view in user settings. - **Shift + ↑ / ⇧ + ↑**: Move the view up by 10 grid spaces. - **Shift + ↓ / ⇧ + ↓**: Move the view down by 10 grid spaces. - **Shift + ← / ⇧ + ←**: Move the view left by 10 grid spaces. - **Shift + → / ⇧ + →**: Move the view right by 10 grid spaces. - **Ctrl + ↑ / ⌘ + ↑**: Scroll the workspace up. - **Ctrl + ↓ / ⌘ + ↓**: Scroll the workspace down. - **Ctrl + ← / ⌘ + ←**: Scroll the workspace to the left. - **Ctrl + → / ⌘ + →**: Scroll the workspace to the right. ### Flow Tab - **Alt + w / ⌥ + w**: Hide current flow. - **Alt + Shift + w / ⌥ + ⇧ + w**: Show/reopen last hidden flow. - **Alt + Shift + f / ⌥ + ⇧ + f**: Show list of flows to switch between. - **Ctrl + \[ / ⌘ + \[**: Open previous flow tab. - **Ctrl + ] / ⌘ + ]**: Open next flow tab. - **Ctrl + Shift + → / ⌘ + Shift + →**: Go to next location. - **Ctrl + Shift + ← / ⌘ + Shift + ←**: Go to previous location. ### Group - **Ctrl + Shift + c / ⌘ + Shift + c**: Copy selected group style. - **Ctrl + Shift + v / ⌘ + Shift + v**: Paste/apply copied group style to selected group. - **Ctrl + Shift + g / ⌘ + Shift + g**: Add selected group(s) or node(s) to a new group. - **Ctrl + Shift + u / ⌘ + Shift + u**: Ungroup group(s) or node(s) from the group. ### Node - **Ctrl + Delete / ⌘ + Delete**: Delete selected node and reconnect previous and next connected nodes. - **Enter / ⏎**: Open properties dialog of selected node. - **Ctrl + f / ⌘ + f**: Search nodes within the flow. ### Wire - **Alt + l, then l / ⌥ + l, then l**: Split selected wire(s) with link nodes. ### Selection - **Ctrl + c / ⌘ + c**: Copy selection (node, group) to internal clipboard. - **Ctrl + x / ⌘ + x**: Cut selection (node, group) to internal clipboard. - **Ctrl + v / ⌘ + v**: Paste the copied/cut (node, group) on the flow. - **Delete/BackSpace / Delete/⌫**: Delete selection (node, group, wire). - **↑ / ↑**: Move the selection up by one nearest node. - **↓ / ↓**: Move the selection down by one nearest node. - **← / ←**: Move the selection left by one nearest node. - **→ / →**: Move the selection right by one nearest node. - **Ctrl + a / ⌘ + a**: Select all config nodes when in config tab. - **Ctrl + a / ⌘ + a**: Select all nodes. - **Escape / ⎋**: Select none. - **Alt + s, then c / ⌥ + s, then c**: Select connected nodes. - **Alt + s, then d / ⌥ + s, then d**: Select downstream nodes. - **Alt + s, then u / ⌥ + s, then u**: Select upstream nodes. - **Alt + a, then b / ⌥ + a, then b**: Align selected node(s) or group(s) to bottom. - **Alt + a, then c / ⌥ + a, then c**: Align selected node(s) or group(s) to center. - **Alt + a, then g / ⌥ + a, then g**: Align selected node(s) or group(s) to grid. - **Alt + a, then l / ⌥ + a, then l**: Align selected node(s) or group(s) to left. - **Alt + a, then m / ⌥ + a, then m**: Align selected node(s) or group(s) to middle. - **Alt + a, then r / ⌥ + a, then r**: Align selected node(s) or group(s) to right. - **Alt + a, then t / ⌥ + a, then t**: Align selected node(s) or group(s) to top. - **Alt + a, then h / ⌥ + a, then h**: Distribute selected node or group(s) horizontally. - **Alt + a, then v / ⌥ + a, then v**: Distribute selected node or group(s) vertically. ### Custom Keyboard Shortcuts Node-RED lets you customize keyboard shortcuts to fit your workflow, making it easier to use the editor and speed up common tasks. #### How to Set Custom Keyboard Shortcuts: 1. To set custom keyboard shortcuts, go to the keyboard settings in the user settings. Click `Shift + ?` or click the top-right menu icon and select "Settings." In the settings menu, switch to "Keyboard Settings." 2. In the Keyboard Settings, you will see actions with assigned shortcuts as well as those that are unassigned. To change or set shortcuts, click on "Unassigned" or the existing shortcut next to the action you want to modify. 3. Enter your preferred key combination for the action. Then, select the appropriate scope and click the check icon to save your changes. Your new shortcuts will now be active. # Sending and receiving Discord messages with Node-RED This guide explains how to integrate Discord with Node-RED to send and receive messages. You'll learn how to configure a Discord bot, send messages to users and channels, and handle incoming messages. Discord is commonly used for notifications in IoT applications. This document covers the setup process and includes troubleshooting steps for common integration issues. For information on integrating other notification services, see the guides on [Email](https://flowfuse.com/docs/node-red/notification/email/) and [Telegram](https://flowfuse.com/docs/node-red/notification/telegram/). ## Prerequsite Before proceeding further, make sure you have installed the following node: - [node-red-contrib-discord-advance](https://flows.nodered.org/node/node-red-contrib-discord-advanced){rel=""nofollow""} ## Creating Bot in Discord 1. Navigate to the [Discord developer portal](https://discord.com/developers/applications){rel=""nofollow""}. 2. Login with your Discord account credentials. 3. To create an application, click on the top-right "Create Application" button. !["Screenshot showing the 'create application' button"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-new-application-button.png "Screenshot showing the 'create application' button") 4. Enter the name for your application and click on the "Create" button. !["Screenshot showing the 'create' button"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-create-app.png "Screenshot showing the 'create' button") 5. After the successful creation of the application, you'll be redirected to that application's setting. Click on the "Bot" option from the left sidebar. !["Screenshot showing the 'bot' sidebar option and 'reset token' button"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-bot-reset-token.png "Screenshot showing the 'bot' sidebar option and 'reset token' button") 6. Click on the "Reset Token" and copy the regenerated bot secret token. 7. Next, you'll need to enable [Privileged Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents){rel=""nofollow""} for your bot. To do so, navigate to the bot page and enable three intents as seen below. !["Screenshot showing the 'Privileged Gateway Intents' options"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-privillage-itents.png "Screenshot showing the 'Privileged Gateway Intents' options") *Note:- If your application is verified or if your bot is in more than 100 servers, you'll need to apply for Privileged Gateway Intents to use them.* ## Adding configuration for the discord nodes Before proceeding further, make sure you have added the environment variable for your bot's secret token. Using environment variables prevents the configuration token from being exposed in the application flow. For more information refer to the guide on [Using environment variable](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). 1. Drag the DiscordMessageManager Node onto the canvas. 2. Double-click on it and then click on the pencil icon next to "Token" input field, and add the environment variable added for the bot token into the Token input field. ## Sending a message to User To send a message to a user, you will need the ID of that user. Before copying the ID, ensure that you have enabled "Developer Mode" in Discord, which starts displaying IDs for users, channels, and messages. Navigate to the Discord app, go to "Settings" -> "Advanced," and enable the "Developer Mode" option. !["Screenshot showing the 'Developer mode' option"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-developer-mode.png "Screenshot showing the 'Developer mode' option") 1. In the Discord app, click on the user profile you want to send a message to, and click on "Copy User ID" to copy the ID. !["Screenshot showing 'copy user id' option"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-user-id.png "Screenshot showing 'copy user id' option") 2. Drag an Inject node onto the canvas. 3. Set the `msg.payload` to the message you want to send and the `msg.user` to the user ID of the user you want to send the message to. 4. Connect the Inject node's output to the input of the DiscordMessageManager node. :video{ariaLabel="Sending messages to Discord users" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} ## Sending messages to the Discord server To send a message to the Discord server, you have to make sure that your bot is a member of that server with appropriate permissions. ### Adding the bot to the Discord server 1. Navigate to the Discord Developer Portal and click on your application, then click on the OAuth2 option from the sidebar. 2. Select "bot" from the OAuth2 URL Generator's scope section and select the permissions you want to give to the bot in the server. 3. At the bottom, you'll find the "Copy" button. Click on it to copy the OAuth2 URL. 4. Paste that URL into the browser field and press Enter. 5. Now, you'll see a Discord popup. Select the server to which you want to add the bot and click on the "Continue" button. !["Screenshot showing discord popup to add bot into the server"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-select-the-server.png "Screenshot showing discord popup to add bot into the server") 6. Next, confirm that you want to give the permissions you selected while generating the OAuth2 URL to the bot on the server and click "Authorize" to add it to the server along with those permissions. !["Screenshot showing conformation discord popup asking to conform the permission should be given to bot into server"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-conform-add-to-server.png "Screenshot showing conformation discord popup asking to conform the permission should be given to bot into server") ### Sending messages to Discord server 1. Drag the Inject node onto the canvas. 2. Set the `msg.payload` to the message you want to send, and set `msg.channel` (you can also set it in the node) to the Channel ID to which you want to send the message. To grab the Channel ID, go to the Discord app, right-click on the server in the sidebar where the channel is located that you want to send the message to, click on the channel, and then click on "Copy Channel ID. !["Screenshot showing 'copy channel id' option"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-channel-id.png "Screenshot showing 'copy channel id' option") 3. Connect the Inject node's output to the input of the DiscordMessageManager node. :video{ariaLabel="Sending message to Discord server's channel" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} ## Receiving messages from Discord 1. Drag the DiscordMessage node onto the canvas. 2. Double-click on it to ensure you have configured your bot correctly. 3. Drag the Debug node onto the canvas, double-click on it, and select the output to "Complete message object". After deploying the flow, you will start receiving messages sent to your bot. In the debug panel in the sidebar, you will see the message object printed for each message, which contains different objects. Each object shows different details; for example, the author object contains details about the sender, and the channel object includes information of the channel if the message was sent in a channel. :video{ariaLabel="Receiving messages from users and server channels" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} Below, I have provided the complete flow we built throughout the guide. Make sure to replace the environment variable 'BOT\_TOKEN' with your actual bot token. ::render-flow ```json [{"id":"7a8dd49f9614608e","type":"inject","z":"4674ed668685adf6","name":"Sending mesasge to Discord server 's channel","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello, This is from Node-RED","payloadType":"str","x":570,"y":420,"wires":[["3984c20a52db033f"]]},{"id":"3984c20a52db033f","type":"discordMessageManager","z":"4674ed668685adf6","name":"","channel":"56454645657765656","token":"","x":930,"y":420,"wires":[["b99ae75425047b6b"]]},{"id":"b99ae75425047b6b","type":"debug","z":"4674ed668685adf6","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1200,"y":420,"wires":[]},{"id":"6af740e11aba6ca2","type":"inject","z":"4674ed668685adf6","name":"Sending message to Discord user","props":[{"p":"payload"},{"p":"user","v":"65454534534345365","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello, Sumit","payloadType":"str","x":550,"y":320,"wires":[["f69363f341b62333"]]},{"id":"f69363f341b62333","type":"discordMessageManager","z":"4674ed668685adf6","name":"","channel":"","token":"","x":930,"y":320,"wires":[["c5001a783992ae01"]]},{"id":"c5001a783992ae01","type":"debug","z":"4674ed668685adf6","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1220,"y":320,"wires":[]},{"id":"820b70826545a401","type":"debug","z":"4674ed668685adf6","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":540,"wires":[]},{"id":"f044d3784ef8c74a","type":"discordMessage","z":"4674ed668685adf6","name":"","channelIdFilter":"","token":"","x":720,"y":540,"wires":[["820b70826545a401"]]}] ``` :: ## Debugging and Troubleshooting Below are common errors that can occur while integrating Discord with Node-RED, along with troubleshooting tips. !["User disallowed intent"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-error-3.png "User disallowed intent") If your Discord nodes show a status similar to the image above, you might still need to enable the [Privileged Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents){rel=""nofollow""} option. If you have already enabled it but are still encountering this issue, it could be due to your app bot is verified. A verified app bot is required to apply for the privileged gateway intents. For more information, refer to [Discord support article](https://support-dev.discord.com/hc/en-us/articles/6205754771351-How-do-I-get-Privileged-Intents-for-my-bot){rel=""nofollow""}. !["DiscordAPIError\:Unknwon channel"](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-error-2.png "DiscordAPIError\:Unknwon channel") ![DiscordAPIError\:Unknwon user](https://flowfuse.com/docs/node-red/notification/images/discord-with-node-red-error-1.png "DiscordAPIError\:Unknwon user") If you are getting errors similar to the images above, it's likely because the `channelId` or `userId` is invalid. Double-check and correct these identifiers to resolve the errors. # Sending and receiving emails with Node-RED This guide shows you how to integrate email with Node-RED for sending and receiving messages. You'll learn how to configure email nodes, set up Gmail integration, and follow best practices to ensure your notifications reach their destination. ## When to Use Email for IoT Notifications Email offers unique advantages that make it suitable for specific notification scenarios: - **Non-urgent notifications** - Email works well for updates that don't require immediate action, allowing users to review them at their convenience. - **Compliance and audit trails** - Email provides documented communication records that are essential for regulatory compliance and audit requirements. - **Detailed information** - Email supports attachments and longer content, making it useful for sharing reports, logs, and comprehensive documentation. - **Multiple recipients** - Email can deliver notifications to several users simultaneously, ensuring information reaches everyone who needs it. ## Installing the Email Node 1. Open Node-RED Settings (top-right menu) 2. Select "Manage Palette" 3. Go to the "Install" tab 4. Search for `node-red-node-email` 5. Click "Install" ## Understanding Email Node Configuration ### Server The server address determines where your emails are sent or retrieved from. Outgoing mail uses SMTP servers (like `smtp.example.com`), while incoming mail uses IMAP or POP3 servers (like `imap.example.com` or `pop.example.com`). **Email Protocols:** - **SMTP** (Simple Mail Transfer Protocol) - Handles sending outgoing messages from your application to the recipient's mail server - **POP3** (Post Office Protocol v3) - Downloads messages to your client and typically removes them from the server - **IMAP** (Internet Message Access Protocol) - Manages email directly on the server, keeping messages synchronized across all your devices ### Port Different ports serve different purposes for email communication: **Outgoing (SMTP):** - **465** - Uses SSL encryption from the start of the connection - **587** - Uses TLS encryption with STARTTLS (recommended) - **25** - The original SMTP port, though many ISPs block it for security reasons **Incoming:** - **993** - IMAP with SSL encryption - **143** - IMAP without encryption - **995** - POP3 with SSL encryption - **110** - POP3 without encryption ### Use Secure Connection Enable this option to encrypt your connection using TLS, which protects your email credentials and content during transmission. **Note:** If you're using port 587 or 25 with a server that supports STARTTLS, you should leave this option disabled since the connection will upgrade to encrypted automatically. ### Auth Type Choose the authentication method your email provider requires: - **Basic** - Standard username and password authentication - **XOAuth** - OAuth authentication using a username and access token - **None** - No authentication required (rare and not recommended for outgoing mail) ### TLS Option When enabled, this option verifies that your mail server's SSL/TLS certificate is valid, adding an extra layer of security to your connection. ### Format to SASL This option handles SASL (Simple Authentication and Security Layer) XOAuth2 token formatting: - **Enabled** - The node automatically formats your OAuth2 token by combining the username and token, encoding it in base64 - **Disabled** - You'll need to manually format the token before passing it to the email node ## Gmail Configuration This guide uses Gmail as the email provider. Here's what you need to configure: - **Server (outgoing):** `smtp.gmail.com` - **Server (incoming):** `imap.gmail.com` - **User ID:** Your Gmail address () - **Port:** 465 (SSL) or 587 (TLS) - **Password:** You'll need to generate an app password for your Google account. Visit [Sign in with app passwords](https://support.google.com/mail/answer/185833?hl=en){rel=""nofollow""} to create one. Generate a separate app password for each Node-RED application you create. ## Setting Up Environment Variables Storing your email credentials directly in Node-RED flows exposes sensitive information. Environment variables keep your credentials secure by storing them separately from your flow configuration. For a detailed explanation, see [Using Environment Variables with Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). !["Screenshot of FlowFuse instance settings Environment tab"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-node-red_setting_environment_variables.png "Screenshot of FlowFuse instance settings Environment tab"){dataZoomable=""} 1. Navigate to your instance Settings and select the Environment tab 2. Click "Add variable" and create variables for both `userid` and `password` 3. Click "Save" to store your variables 4. Restart your instance using the Actions menu (top-right) and selecting Restart ## Configuring the Email Output Node 1. Drag an **e-mail** node onto the canvas and double-click to open it 2. Enter the recipient's email address in the "to" field. You can also set this dynamically using `msg.to`, and include CC or BCC recipients with `msg.cc` and `msg.bcc`. See the [Node README](https://flows.nodered.org/node/node-red-node-email){rel=""nofollow""} for more details. 3. Enter `smtp.gmail.com` as your server address 4. Choose port 465 for SSL or 587 for TLS (either works) 5. Set auth type to "Basic" 6. Enter your environment variables for user ID and password in the corresponding fields 7. Enable the "Use secure connection" option !["Screenshot displaying configuration of e-mail node for sending emails"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-e-mail-in-node-configuration.png "Screenshot displaying configuration of e-mail node for sending emails") {data-zoomable} ## Sending Emails 1. Drag an **inject** node onto the canvas 2. Set `msg.payload` to your email body content. For more control, use `msg.plaintext` for plain text emails, `msg.html` for HTML-formatted emails, or `msg.attachment` (as an array) for attachments in [Nodemailer](https://nodemailer.com/message/attachments){rel=""nofollow""} format. 3. Set `msg.topic` to your email subject line 4. Connect the inject node's output to the e-mail node's input !["Screenshot of the inject node setting payload for sending email notification"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-inject-node.png "Screenshot of the inject node setting payload for sending email notification") {data-zoomable} ## Receiving Emails 1. Drag an **e-mail in** node onto the canvas 2. Select your preferred "Get mail" option 3. Set the Protocol to "IMAP" (recommended for third-party applications) 4. Enter your environment variables for userid and password 5. Add a **debug** node to the canvas 6. Connect the debug node's input to the e-mail in node's output !["Screenshot displaying configuration of e-mail in node for sending emails"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-e-mail-node-configuration.png "Screenshot displaying configuration of e-mail in node for sending emails"){dataZoomable=""} ## Deploying the Flow !["Screenshot displaying Node-RED flow: Sending and Receiving Emails using Node-RED"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-node-red-flow.png "Screenshot displaying Node-RED flow: Sending and Receiving Emails using Node-RED"){dataZoomable=""} !["Screenshot of Gmail inbox displaying received email notification"](https://flowfuse.com/docs/node-red/notification/images/sending-and-receiving-email-with-node-red-gmail-inbox.png "Screenshot of Gmail inbox displaying received email notification"){dataZoomable=""} Click the "Deploy" button in the top-right of the Node-RED Editor. Once deployed, you can send emails by clicking the inject button, or configure triggers to send notifications based on specific events in your IoT application. ## Ensuring Your Emails Reach Their Destination ### Understanding Anti-Spam Measures Email servers use sophisticated filtering systems to protect users from unwanted messages. These systems analyze various aspects of incoming emails: **Content filtering** scans your email text for keywords and patterns commonly found in spam. **Sender authentication** verifies that your email address and domain are legitimate, often using protocols like SPF to confirm your server is authorized to send emails on behalf of your domain. **IP filtering** blocks messages from IP addresses with known spam activity. **Reputation scoring** tracks your sending history and behavior, assigning a score that affects whether your emails land in inboxes or spam folders. ### Best Practices for Email Delivery Even legitimate emails can sometimes trigger spam filters. Follow these practices to keep your notifications flowing smoothly: - **Write clear, purposeful messages** - Keep your content focused and action-oriented, clearly stating why you're sending the notification and what action recipients should take. - **Avoid spam trigger words** - Stay away from phrases like "free," "limited time offer," or "urgent" that commonly appear in spam messages. - **Authenticate your emails** - Implement SPF, DKIM, and DMARC protocols to verify your email's legitimacy and improve deliverability. - **Manage your sending frequency** - Avoid sending too many emails in a short period. Maintain a consistent schedule and ensure each message provides value. - **Keep your email list clean** - Regularly remove invalid or inactive addresses. High bounce rates and spam complaints damage your sender reputation. - **Monitor your reputation** - Use tools like SenderScore or Google Postmaster Tools to track your sender reputation and identify potential issues before they affect delivery. # Notification Services Real-time notifications are essential in our automation world, helping to keep us informed, responsive, and efficient. Whether it's a critical system alert, a customer inquiry, or a simple reminder, timely notifications can significantly enhance productivity and efficiency. However, managing and integrating such services can be challenging in traditional development environments. Node-RED simplifies this process by supporting a wide range of notification services, including email, Telegram, Slack, WhatsApp, and more. ## Resources Here are some resources to help you get started with Node-RED on diffrent notification services: - [Sending and receiving Discord messages with Node-RED](https://flowfuse.com/docs/node-red/notification/discord/): Learn how to send and receive Discord messages with Node-RED. - [Sending and receiving emails with Node-RED](https://flowfuse.com/docs/node-red/notification/email/): Learn how to send and receive emails using Node-RED, along with best practices for sending email notifications. - [Sending and receiving Telegram messages with Node-RED](https://flowfuse.com/docs/node-red/notification/telegram/): Learn to seamlessly integrate Telegram with Node-RED for messaging. Create bots, obtain chat IDs, and send/receive messages, including group messaging. # Sending and receiving Telegram messages with Node-RED Telegram has become a popular choice for messaging in home automation applications. This guide shows you how to integrate Telegram with Node-RED, covering bot creation, chat ID retrieval, and both sending and receiving messages. ## Creating a Bot in Telegram 1. Open your Telegram application and click the search icon in the top-right corner. 2. Search for "BotFather" and select the account with the blue verified checkmark. !["screenshot displaying searching for botFather bot for creating custom bot"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-botfather.png "screenshot displaying searching for botFather bot for creating custom bot"){dataZoomable=""} 3. In the chat interface, type `/newbot` and press Enter to send the command. 4. The bot will ask for a name, which will be the display name of your bot. You can choose any name you like. 5. Next, it will ask for a unique username for your bot. The username cannot include spaces and must end with 'bot' (for example: `telegram_bot` or `telegrambot`). 6. Once you've entered a valid and unique username, you'll receive a confirmation message with your bot's secret access token and a link to start your bot. 7. Click the provided link to open the chat interface with your bot, then click the Start button at the bottom to activate it. !["screenshot displaying chat interface with start button for activating your bot"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-activating-bot.png "screenshot displaying chat interface with start button for activating your bot"){dataZoomable=""} ## Obtaining Your Telegram Chat ID The Telegram chat ID is a unique identifier for a chat or group in Telegram, which is required for sending and receiving messages. This section covers how to obtain both your personal chat ID and group chat IDs. ### Obtaining Your Personal Chat ID 1. Open your Telegram app, click the search icon in the top-right corner, and search for "Get My ID". !["screenshot displaying searching 'Get My ID' bot for obtaining chat id"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-getmyid.png "screenshot displaying searching 'Get My ID' bot for obtaining chat id"){dataZoomable=""} 2. Select the first result to open the chat interface, type `/start`, and press Enter. You'll receive a message containing your Chat ID and User ID. ### Obtaining Your Telegram Group Chat ID 1. Add `@getmyid_bot` to the group where you want to send or receive messages using Node-RED. 2. Once the bot joins the group, it will automatically send the group's chat ID. ## Installing the Custom Node 1. Click Node-RED Settings (top-right menu). 2. Select "Manage Palette". 3. Switch to the "Install" tab. 4. Search for `node-red-contrib-telegrambot`. 5. Click "Install". ## Adding Environment Variables Environment variables keep your sensitive information secure by preventing it from being exposed in your flow\.json file. This is especially important when configuring nodes with credentials like access tokens. For more details, see [Using Environment Variables with Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). !["Screenshot displaying flowfuse instance settings for adding environment variable"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-flowfue-instance-settings.png "Screenshot displaying flowfuse instance settings for adding environment variable"){dataZoomable=""} 1. Navigate to Instance Settings and switch to the "Environment" tab. 2. Click the "Add variable" button (top-right). 3. Add variables for your bot's secret access token and chat ID. 4. Click "Save settings" and restart the instance by clicking Actions (top-right) and selecting "Restart". ## Configuring the Custom Node 1. Drag a **Sender** node onto the canvas and double-click it. 2. Enable the "Send error to second output" option. This separates error messages from successful send confirmations, making it easier to handle different outcomes. !["Screenshot displaying 'Send error to second output' option"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-enabling-send-error-to-second-option.png "Screenshot displaying 'Send error to second output' option"){dataZoomable=""} 3. Click the edit icon next to the Bot field. 4. Enter your bot name and add the environment variable for your access token in the Token field, then add the environment variable for your chat ID in the chatIds field as shown below. !["Screenshot displaying telegram custom node configuration"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-telegram-node-configuration.png "Screenshot displaying telegram custom node configuration"){dataZoomable=""} ## Sending a Message to Telegram 1. Drag an **Inject** node onto the canvas. 2. Drag a **Change** node onto the canvas and configure it to set `msg.payload.type` as "message". To explore other message types, refer to the [node readme](https://flows.nodered.org/node/node-red-contrib-telegrambot){rel=""nofollow""}. Set `msg.payload.chatId` to the environment variable you added for the chat ID, and set `msg.payload.content` to the message you want to send. !["Screenshot displaying the change node setting payload for sending message"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-change-node.png "Screenshot displaying the change node setting payload for sending message"){dataZoomable=""} 3. Drag two **Debug** nodes onto the canvas. 4. Connect the Inject node's output to the Change node's input, then connect the Change node's output to the Sender node's input. 5. Connect one Debug node's input to the first output of the Sender node, and the second Debug node's input to the Sender node's second output. ## Receiving a Message from Telegram 1. Drag a **Receiver** node onto the canvas. 2. Double-click the node and make sure you've selected the correct bot configuration. 3. The Receiver node has two outputs: one for messages from authorized users, and another for messages from unauthorized users. 4. To add users to your authorized list, click the Receiver node, click the edit icon next to the Bot field, and add usernames separated by commas in the users field. 5. Drag two **Debug** nodes onto the canvas. 6. Connect the first Debug node's input to the first output of the Receiver node, and the second Debug node's input to the second output of the Receiver node. ## Deploying the Flow !["Screenshot displaying Node-RED flow for sending and receiving telegram messages"](https://flowfuse.com/docs/node-red/notification/images/sending-telegram-with-node-red-flow.png "Screenshot displaying Node-RED flow for sending and receiving telegram messages"){dataZoomable=""} 1. Deploy the flow by clicking the Deploy button in the top-right corner. Your Telegram bot is now ready to use. Click the Inject button to send a message, and you'll receive a notification in Telegram. You can also check your bot's chat to see messages sent via Node-RED. To test receiving messages, send a message to your bot and watch the Debug panel display the message object containing the message content and additional information. # Connecting Arduino to Node-RED This documentation explains how to use Node-RED to interact with an Arduino board via serial communication using the Firmata protocol. It covers how to write to and read from digital and analog pins using the `node-red-node-arduino` package. ## Requirements - An Arduino board connected via USB to the device running Node-RED. - The [StandardFirmata](https://github.com/firmata/protocol){rel=""nofollow""} sketch uploaded to the Arduino board. - Node-RED installed and running on the connected device. The quickest way to set up and run Node-RED is [FlowFuse](https://flowfuse.com). - The Node-RED package [`node-red-node-arduino`](https://flows.nodered.org/node/node-red-node-arduino){rel=""nofollow""} installed. ## Step 1: Install Arduino Nodes 1. Open the Node-RED editor. 2. Open the main menu and select **Manage Palette**. 3. Switch to the **Install** tab. 4. Search for `node-red-node-arduino` and install it. This will add `arduino in` and `arduino out` nodes to the palette. ## Step 2: Configure Arduino Connection 1. Drag either an `arduino in` or `arduino out` node onto the canvas. 2. Double-click the node to open the configuration window. 3. Click the pencil icon next to the **Arduino board** field. 4. Enter the correct serial port: - Windows: `COMx` (e.g., `COM5`) - Linux/macOS: `/dev/ttyUSB0`, `/dev/ttyACM0`, etc. 5. Click **Add** and then **Done**. 6. Click **Deploy**. A green status indicator confirms successful connection. ## Step 3: Write to Arduino Pins ### 3.1 Write Digital Output To write a digital value (`0` or `1`) to a pin: 1. Drag an `arduino out` node to the canvas. 2. Double-click and set: - **Pin**: e.g., `13` - **Type**: `Digital` 3. Use `inject` nodes to send `true` (HIGH) and `false` (LOW) payloads. 4. Connect the inject nodes to the `arduino out` node. 5. Deploy and test. ### 3.2 Write Analog Output (PWM) To write a PWM signal (`0–255`) to a supported pin: 1. Use an `arduino out` node. 2. Set: - **Pin**: e.g., `5` (PWM-capable) - **Type**: `Analog` 3. Use an `inject` node to send a numeric payload (e.g., `128`). 4. Connect and deploy. ### 3.3 Write Servo Angle To control a servo: 1. Use an `arduino out` node. 2. Set: - **Pin**: e.g., `9` - **Type**: `Servo` 3. Use `inject` nodes to send values between `0–180`. 4. Connect and deploy. ## Step 4: Read from Arduino Pins ### 4.1 Read Digital Input To read a digital value from a pin: 1. Drag an `arduino in` node. 2. Set: - **Pin**: e.g., `9` - **Type**: `Digital` 3. Connect the output to a `debug` node. 4. Deploy to see input values in real-time. ### 4.2 Read Analog Input To read from an analog input pin: 1. Use an `arduino in` node. 2. Set: - **Pin**: e.g., `A0` - **Type**: `Analog` 3. Connect to a `debug` node. 4. Deploy and monitor readings in the debug sidebar. Once you have basic read/write functionality working with `inject` and `debug` nodes, you can easily build a **dashboard interface** using `ui_button`, `ui_slider`, or `ui_gauge` nodes to control and monitor your Arduino through a web-based UI. 🔗 If you are looking for a more practical, step-by-step article with examples and a video demo, refer to this guide: :br[Interacting with Arduino using Node-RED](https://flowfuse.com/blog/2025/02/interacting-with-arduino-using-node-red/) # How to connect a barcode scanner to your Node-RED application Barcode scanners, functioning as Human Interface Devices (HID) similar to keyboards, offer versatile programming options. Variations of barcode scanners can be seen used from anything from checkout counters, logistics, and to manufacturing erp systems. In our case, we kept it basic and we used one to trigger a Node-RED flow, keeping the process straightforward and efficient. Don't let that limit your imagination though, with QR codes, you can store just about anything including recipes in a JSON structure. ## Configuring the scanner and scanning barcodes We revitalized an older project for this purpose, ensuring it's up-to-date. For Windows users, the setup is straightforward. Start by importing the project, [@gdziuba/node-red-usbhid](https://flows.nodered.org/node/@gdziuba/node-red-usbhid){rel=""nofollow""}, via the palette manager. Import these [flows](https://flows.nodered.org/flow/3e08565bc0e024e81325dc028c5da792){rel=""nofollow""} to get started. This initial flow identified as **getHIDdevices** will detect all devices connected to your Node-RED environment. Locate your barcode scanner in the debug output. You will see everything from your mouse and keyboard. If you have just recently added the barcode scanner to your computer, it will probably be found at the end. Once you find it note its **Product ID** and **Vendor ID**. For us, they would be identified as 1536 and 1504 respectively. ![USB HID Node-RED](https://flowfuse.com/docs/node-red/peripheral/images/usbhid-barcode-node-red.png) Next, configure the **HIDdevice** node: replace the default **PID** with your scanner’s Product ID, and the **VID** with its Vendor ID. ![USB HID Config Node-RED](https://flowfuse.com/docs/node-red/peripheral/images/usbhid-config-node-red.png) Test your barcode scanner against any barcode your scanner works with and you should observe an event being triggered in Node-RED and output to debug should be the contents of the barcode. ![USB HID Scanned Barcode in Node-RED](https://flowfuse.com/docs/node-red/peripheral/images/usbhid-scanned-barcode.png) You could even take it a step further and create a [QR code](https://smalldev.tools/qr-code-generator-online){rel=""nofollow""} for your favorite pizza ingredients as seen here on the new [Dashboard 2.0](https://flowfuse.com/platform/dashboard/). ![USB HID Scanned Barcode Pizza Ingredients](https://flowfuse.com/docs/node-red/peripheral/images/usbhid-qr-pizza-order.png) ## Linux Setup Linux users might face a slightly more complicated setup, as access to communication ports isn't always granted by default, and specific drivers are needed for optimal node functionality. This is due to the security around applications having access to specific devices connected to the system. For this, we recommend following the detailed instructions available in the project's [GitHub](https://github.com/gdziuba/node-red-contrib-usbhid){rel=""nofollow""} repository. # Connect ESP32 with Node-RED using MQTT This document outlines the procedure for establishing MQTT communication between an ESP32 microcontroller and a Node-RED instance. ## Requirements - An ESP32 development board. - An active MQTT broker with access credentials. - A running Node-RED instance. The quickest way to get started is with **[FlowFuse](https://flowfuse.com)**, which allows you to effortlessly deploy and manage Node-RED instances and also includes a built-in MQTT broker service. - Arduino IDE configured with the ESP32 core and the **PubSubClient** library installed. - Two MQTT clients configured ## Set Up MQTT Clients To create the necessary MQTT clients (one for ESP32 and one for Node-RED), follow the official guide: [Creating MQTT Clients in FlowFuse](https://flowfuse.com/docs/cloud/introduction/#enterprise-team-broker) Once created, note down the client ID, username, and password for each client. These credentials will be used later to establish communication. ## Node-RED Configuration 1. Open your Node-RED editor. 2. Drag either an **`mqtt in`** node (to receive data from ESP32) or an **`mqtt out`** node (to send commands to ESP32) into your flow. 3. Double-click the node and configure the MQTT connection: - **Server**: Your MQTT broker’s address (e.g., `broker.flowfuse.cloud`) - **Port**: Typically `1883` or `8883` for TLS - **Client ID**, **Username**, and **Password**: Use the credentials created for the Node-RED client in your broker 4. Specify a **Topic** such as `/esp32/control` for sending commands or `/esp32/data` for receiving sensor data. 5. Click **Deploy**. Once configured properly, the MQTT node should display a “connected” status. ## ESP32 Programming The ESP32 firmware must perform the following actions: 1. Establish a connection to the local Wi-Fi network. 2. Connect to the MQTT broker using its designated client credentials. 3. Subscribe to the topic specified in Node-RED (`/esp32/control`). 4. Implement a callback function to process received messages and execute corresponding actions. Make sure to program the ESP32 accordingly using the Arduino IDE and the PubSubClient library to ensure reliable communication with the MQTT broker. ## Live Demo: Remote LED Control ::lite-youtube --- params: rel=0 style: "margin-top: 20px; margin-bottom: 20px; width: 100%; height: 480px;" title: YouTube video player videoid: ecfJ-9MxyVE --- :: This section provides a practical demonstration with an importable Node-RED flow and corresponding ESP32 code to remotely control the onboard LED. For more detailed, practical steps, please refer to our article [Interacting with ESP32 Using Node-RED and MQTT](https://flowfuse.com/blog/2024/11/esp32-with-node-red/) ### 1. Node-RED Demo Flow Import the following JSON into your Node-RED editor. This flow creates a simple dashboard with ON/OFF buttons that publish to the `/esp32/led` topic. You must configure the **`mqtt out`** node with your specific broker credentials. ::render-flow{:height='300'} ```json [{"id":"59887a8115c95eae","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"02c25e8a30f9379d","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"cfb2ab9ff30660fc","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"d263574af6876c7a","type":"ui-page","name":"ESP32","ui":"02c25e8a30f9379d","path":"/page1","icon":"home","layout":"grid","theme":"cfb2ab9ff30660fc","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"3ae115ea7ede6827","type":"ui-group","name":"Group 1","page":"d263574af6876c7a","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"def97b29f5f7baab","type":"mqtt-broker","name":"","broker":"broker.flowfuse.cloud","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"5a9162986a34a4d6","type":"ui-button","z":"59887a8115c95eae","group":"3ae115ea7ede6827","name":"","label":"ON","order":1,"width":"3","height":"2","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"1","payloadType":"num","topic":"topic","topicType":"msg","buttonColor":"green","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":190,"y":120,"wires":[["9239f8a7cca5c858"]]},{"id":"f9c194994d9491a8","type":"ui-button","z":"59887a8115c95eae","group":"3ae115ea7ede6827","name":"","label":"OFF","order":2,"width":"3","height":"2","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"2","payloadType":"num","topic":"topic","topicType":"msg","buttonColor":"red","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":190,"y":160,"wires":[["9239f8a7cca5c858"]]},{"id":"9239f8a7cca5c858","type":"mqtt out","z":"59887a8115c95eae","name":"","topic":"/LedControl","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"def97b29f5f7baab","x":390,"y":140,"wires":[]}] ``` :: ### 2. ESP32 Demo Code The following code should be uploaded to your ESP32 board. Replace the placeholder values with your specific network and MQTT credentials. ```cpp #include #include // --- User-defined Credentials --- const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; const char* mqtt_server = "YOUR_MQTT_BROKER_IP"; // e.g., "192.168.1.100" const char* mqtt_user = "YOUR_MQTT_USERNAME"; const char* mqtt_pass = "YOUR_MQTT_PASSWORD"; // --- Pin Definitions --- #define LED_PIN 2 // Onboard LED pin // --- Global Objects --- WiFiClient espClient; PubSubClient client(espClient); // --- MQTT Message Handler --- void callback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; } if (String(topic) == "/esp32/led") { if (message == "ON") { digitalWrite(LED_PIN, HIGH); } else if (message == "OFF") { digitalWrite(LED_PIN, LOW); } } } // --- MQTT Reconnection Logic --- void reconnect() { while (!client.connected()) { if (client.connect("esp32-client-demo", mqtt_user, mqtt_pass)) { client.subscribe("/esp32/led"); } else { delay(5000); // Wait 5 seconds before retrying } } } // --- Setup Function --- void setup() { pinMode(LED_PIN, OUTPUT); WiFi.begin(ssid, password); client.setServer(mqtt_server, 1883); client.setCallback(callback); } // --- Main Loop --- void loop() { if (!client.connected()) { reconnect(); } client.loop(); } ``` ### 3. Verification 1. Deploy the imported flow in Node-RED and open the dashboard interface. 2. Upload the configured sketch to the ESP32 board. 3. Operate the ON and OFF buttons on the dashboard to toggle the ESP32's onboard LED. # Peripheral Devices Node-RED supports a wide range of peripheral devices, allowing users to connect Node-RED to many external inputs and outputs. This enables the creation of interactive and automated systems where data from peripheral devices can trigger actions or responses in the digital realm. By bridging the gap between software and hardware, Node-RED expands its applicability to a wide range of use cases. # Using webcam with Node-RED Dashboard 2.0 has introduced its first third-party webcam widget, simplifying the integration of webcam features with Node-RED applications. In this documentation, you will learn how to utilize the ui-webcam widget in your Node-RED applications. Additionally, if you are willing to develop your own third-party widget, we have our [example widget](https://github.com/FlowFuse/node-red-dashboard-2-ui-example){rel=""nofollow""} which helps you develop your widget. Additionally for a detailed step-by-step guide refer to [Building Third Party Widgets](https://dashboard.flowfuse.com/contributing/widgets/third-party.html){rel=""nofollow""}. Install Node-RED Dashboard 2.0. Follow these [instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to get started. ::callout{icon="i-lucide-badge-check"} **Certified nodes for this technology.** This page covers browser webcams through the Dashboard widget. For network cameras that stream over RTSP, FlowFuse certifies and maintains a separate node: [rtsp](https://flowfuse.com/integrations/?certified=1) :: ## Using a webcam custom widget Once Dashboard 2.0 is installed, proceed to install the ui-webcam widget: 1. Install `@sumit_shinde_84/node-red-dashboard-2-ui-webcam` by the palette manager. 2. Select a created group for the ui-webcam widget in which it will render. 3. Deploy the flow by clicking on the top-right red deploy button. ## Inner Workings of the Webcam Widget In this section, we will take a closer look at the inner workings of the webcam widget. The widget is built using Vue.js and provides a highly engaging and interactive user interface that follows Node-RED and Dashboard 2.0 standards. To enable webcam functionality, the widget makes use of the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia){rel=""nofollow""}, which facilitates access to connected media input devices like cameras and microphones. ## Capturing images using controls This webcam widget offers nice interactive controls that will allow you to interact with the webcam easily. 1. Navigate to the dashboard by accessing `https://.flowfuse.cloud/dashboard`. 2. Initially, you'll see a black interface with a power button on the dashboard. Clicking this button will activate the webcam. Ensure that you grant permission to the dashboard to access your webcam. 3. Once the webcam is active, you can capture images by clicking the button with the camera icon located at the bottom center of the webcam interface. 4. The widget returns a Base64 string containing the captured image in PNG format. :video{ariaLabel="capturing images using webcam widget controls" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="442"} ## Capturing images by passing payload 1. Drag an inject node onto the canvas. 2. Set `msg.payload` to `capture` as string. You can also set an interval time to automatically send the message after a specific interval, or you can keep it unchanged to manually send the payload by clicking the inject button. 3. Connect the output of the inject node to the input of the webcam widget. 4. Deploy the flow. By passing the "capture" string as payload, the webcam widget will activate (if it's off) and capture images automatically, without requiring user interaction. This method is commonly used in industrial applications which depend on automated actions. :video{ariaLabel="capturing images by passing payload" autoPlay="true" height="291" loop="true" muted="true" playsInline="true" preload="none" width="600"} ## Selecting different camera devices The webcam widget also allows you to select different camera devices connected to your system: 1. Click on the ellipsis icon located at the top-right corner of the webcam interface. 2. A dropdown menu will display the connected cameras. Select your preferred camera to use. Additionally, you can turn off the camera by selecting the "Turn camera off" option. :video{ariaLabel="selecting different camera" autoPlay="true" height="606" loop="true" muted="true" playsInline="true" preload="none" width="600"} ## Browser support and privacy - Browser Compatibility: The webcam widget is compatible with all modern browsers, except Internet Explorer. Whether you're using Chrome, Firefox, Safari, or Edge, you can seamlessly integrate webcam features into your Node-RED applications. - Control Limitation: It's important to note that this widget is designed to interact with webcams directly accessible to the system running Node-RED. For example, if the webcam is connected to a different device or network and not directly accessible to the Node-RED running system, the video stream from that webcam won't be displayed on a dashboard using this widget. - HTTPS Requirement: When accessing Dashboard 2.0 remotely (not via `localhost`), it's crucial to use HTTPS. Failure to do so may result in the browser blocking access to the webcam. - User Permission: Before the webcam can be activated, the browser will prompt the user for permission to access the webcam device. This ensures user privacy and consent before any image capture occurs. The widget cannot capture images until the user has given their permission. # Using AMQP with Node-RED Imagine your Node-RED flow working well, handling data from different sources, until suddenly, messages start disappearing or arriving out of order. [MQTT](https://flowfuse.com/docs/node-red/protocol/mqtt/) works fine for basic messaging, but it can struggle in more complex situations where you need delivery guarantees and advanced routing. That’s where AMQP comes in. AMQP solves these issues with features that MQTT doesn’t have. In this guide, we'll explain what AMQP is and how to use it with Node-RED. ## What is AMQP AMQP, or Advanced Message Queuing Protocol, is a set of rules for managing messages between systems. It ensures that messages are sent and received reliably, even if there are network issues. AMQP uses message queues to store messages until they can be processed, making sure they are delivered in the correct order. It supports various messaging patterns, such as one-to-one or one-to-many. In short, AMQP helps different systems communicate with each other effectively and consistently. At the heart of AMQP is the **Message Broker**, which acts as the central hub for managing and routing messages. Producers, the systems or applications that create and send messages, send their data to the broker. The broker uses **Exchanges** to determine how to route these messages. There are several types of exchanges: - **Direct Exchange (point-to-point):** Routes messages to specific queues based on an exact match with the routing key. For example, if a message has a routing key of "error," it will only go to queues set up to receive messages with that key. - **Topic Exchange (publish-subscribe):** Routes messages to queues based on patterns in the routing key. This allows messages to be sent to multiple queues based on partial matches or wildcard patterns. For instance, a routing key of "logs.error" could match queues set up to handle "logs.\*" or "logs.error". - **Fanout Exchange:** Broadcast messages to all queues bound to it without considering the routing key. Every queue connected to this exchange receives a copy of the message. - **Headers Exchange (publish-subscribe):** Routes messages based on attributes in the message headers instead of the routing key. For example, messages with specific header attributes can be directed to particular queues. Messages are placed in **Queues**, where they are stored until they are processed. Queues ensure that messages are delivered in the correct order and are kept until they are successfully handled. Finally, **Consumers** are systems or applications that retrieve and process messages from the queues. They perform actions based on the messages they receive. AMQP uses acknowledgments to confirm that messages have been processed before removing them from the queues, ensuring reliable message handling. ## Using AMQP with Node-RED In this section, we will guide you through integrating AMQP with Node-RED. The guide will cover setting up AMQP in Node-RED, configuring various exchange types, and incorporating them into your flows. You will learn how to send and receive messages based on different exchange methods. To effectively demonstrate these concepts, we will use a variety of scenarios and examples. ### Prequiste - **AMQP Supported Broker Server:** Ensure you have a running AMQP-supported broker server. For this guide, we are using RabbitMQ. - **Node-RED AMQP Node:** Install the [AMQP contrib node](https://flows.nodered.org/node/@stormpass/node-red-contrib-amqp){rel=""nofollow""} via Node-RED palette manager. ### Understanding AMQP Node configuration settings. #### AMQP Broker - **Host:** Specify the hostname or IP address where your AMQP broker is located. This tells your node where to connect. - **Port:** This is the network port the AMQP broker communicates with. The default port for AMQP is 5672, but it might differ if configured otherwise. - **vhost:** Virtual hosts segregate different environments or applications within the same broker instance. The default is `/,` but you might have specific virtual hosts for various use cases. - **Use TLS:** Enable TLS/SSL if the broker requires encrypted communication to ensure data security during transmission. - **User:** The username required for authentication with the broker. RabbitMQ, for example, defaults to `guest`. - **Password:** The password associated with the username for authentication. RabbitMQ’s default is `guest`. Configure the node by dragging an AMQP node onto the canvas. Double-click the node, then click the "+" icon next to the pencil icon. In the prompt that opens, enter the details of your broker server. For added security, ensure you use environment variables to configure nodes. For more information, refer to [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/). #### AMQP Out - **Broker** Select the broker configuration you’ve set up using the AMQP Broker node. This links your outgoing messages to the correct broker instance. - **Reconnect On Error:** Determines whether the node should attempt to reconnect automatically if it encounters an error. This helps maintain communication with the broker even if temporary issues occur. - **Exchange Configuration** - **Type:** Choose the exchange type that dictates how messages are routed such as fanout, direct, topic and headers: - **Exchange Name:** Name of the exchange where messages will be published. This is where the message is sent before being routed to the appropriate queue. - **Routing Key:** This key is Used to direct messages to the correct queues based on the exchange type. It helps specify which queue should receive the message. - **Durable:** Specifies whether the exchange should survive broker restarts. A durable exchange retains its messages through broker restarts. - **Message Properties** - **AMQP Properties:** Allows setting additional properties such as priority, expiration, or delivery mode for messages, influencing their handling and delivery. - **Remote Procedure Call (RPC) Settings** - **Request RPC Response:**Configure whether to request a response for RPC calls: - **YES:** Request a response from the server. - **NO:** Do not request a response. - **RPC Timeout (ms):** Set the timeout for waiting for an RPC response in milliseconds. #### AMQP In - **Broker:** Use the broker configuration details provided by the AMQP Broker node to ensure incoming messages are received from the correct broker. - **Prefetch:** Determines the number of messages to fetch from the broker in advance. Reducing the number of times the broker needs to send messages can help with performance. - **Reconnect On Error:** Configure whether the node should automatically reconnect if it encounters an error. This helps maintain a continuous flow of data. - **noAck:** When enabled, the node will automatically acknowledge messages as soon as they are received. This can be useful for ensuring messages are processed but might lead to message loss if the node fails to process the message correctly. - **Exchange Configuration** - **Type:**Select the exchange type used to route incoming messages: - **Topic** - **Direct** - **Fanout** - **Headers** - **Exchange Name:** The exchange name from which messages are routed. This helps direct incoming messages to the appropriate queue. - **Routing Key:** Specifies how to route messages from the exchange to the correct queue(s). This is essential for ensuring messages are received by the proper consumers. - **Headers:** Set specific headers to filter messages according to routing criteria when the headers exchange type is selected. - **Durable:** Indicates whether the exchange should survive broker restarts. - **Queue Info** - **Queue Name:** Name of the queue where messages are received. This is the storage location for messages before they are processed. Leave it blank if you want it to be generated automatically. - **Exclusive:** If set to true, the queue is exclusive to the connection and will be deleted when the connection closes. - **Durable:** Whether the queue should survive broker restarts, retaining messages until they are consumed. - **Auto Delete:** Determines whether the queue should be deleted automatically when it is no longer in use, helping to manage resources efficiently. ### Direct Exchange **Scenario:** You have a smart irrigation system with multiple zones. You want to send commands to specific zones, such as turning irrigation on or off. #### Sending Data using Direct Exchange 1. Drag two `inject` nodes on to the canvas. Configure the first `inject` node to send data with a `msg.routingKey` of `"zone1"` and the second with a `msg.routingKey` of `"zone2"`. Set the payload for each inject node you want to send to zones. 2. Add an `amqp-out` node. Set the exchange to `irrigation_control`, where the commands will be sent. 3. Connect the `inject` nodes to the `amqp-out` node. #### Receiving Data using Direct Exchange 1. Add two `amqp-in` nodes on to the canvas. Configure one to listen for messages with the `routingKey` of `"zone1"` and the other with `"zone2"`. Both nodes should be set to the `"irrigation_control"` exchange. 2. Connect each `amqp-in` node to a `debug` node to see the received commands for each zone. :video{ariaLabel="Video showing the flow that uses the Direct exchange type to send messages and receive messages" autoPlay="true" height="792" loop="true" muted="true" playsInline="true" preload="none" width="1918"}*Video showing the flow that uses the Direct exchange type to send messages and receive messages* ::render-flow ```json [{"id":"efe7a260307e6202","type":"amqp-out","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","reconnectOnError":true,"exchangeName":"irrigation_control","exchangeType":"direct","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":true,"amqpProperties":"{ \"headers\": {} }","rpcTimeoutMilliseconds":3000,"outputs":0,"x":470,"y":160,"wires":[]},{"id":"538de33f548833ac","type":"inject","z":"807758ec576fbfd8","name":"Send command to zone 1","props":[{"p":"payload"},{"p":"routingKey","v":"zone1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{ \"command\": \"start\" }","payloadType":"json","x":230,"y":100,"wires":[["efe7a260307e6202"]]},{"id":"20cfe04fab562ea9","type":"inject","z":"807758ec576fbfd8","name":"Send command to zone 2","props":[{"p":"payload"},{"p":"routingKey","v":"zone2","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{ \"command\": \"stop\" }","payloadType":"str","x":230,"y":240,"wires":[["efe7a260307e6202"]]},{"id":"7a5e0f3a66dc1ccc","type":"amqp-in","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","prefetch":0,"reconnectOnError":true,"noAck":true,"exchangeName":"irrigation_control","exchangeType":"direct","exchangeRoutingKey":"zone1","exchangeDurable":true,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{}","x":230,"y":360,"wires":[["63c5671d6f4efd07"]]},{"id":"4bf1b44b656c35c2","type":"amqp-in","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","prefetch":0,"reconnectOnError":true,"noAck":true,"exchangeName":"irrigation_control","exchangeType":"direct","exchangeRoutingKey":"zone2","exchangeDurable":true,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{}","x":230,"y":440,"wires":[["e7d4fabe9ef668fe"]]},{"id":"63c5671d6f4efd07","type":"debug","z":"807758ec576fbfd8","name":"Zone 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":500,"y":360,"wires":[]},{"id":"e7d4fabe9ef668fe","type":"debug","z":"807758ec576fbfd8","name":"Zone 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":500,"y":440,"wires":[]},{"id":"bfb1e7e97eef5e04","type":"amqp-broker","name":"AMQP Config","host":"localhost","port":"5672","vhost":"","tls":false,"credsFromSettings":false}] ``` :: We configured a Direct type exchange in Node-RED to route messages to specific queues based on the routing key. We demonstrated how to send and receive commands in a smart irrigation system, ensuring that messages for different zones are delivered correctly. This setup is proper when you need precise message delivery based on an exact match with the routing key. ## Topic Exchange **Scenario**: You have a smart weather station that collects data from multiple sensors, such as temperature, humidity, and air quality. You want to publish and handle data based on the type of sensor and data, such as all temperature or humidity sensor data. You can use topic exchange, which allows you to use wild cards. #### Sending Data using Topic Exchange 1. Drag multiple `inject` nodes onto the canvas. Configure these nodes to send payloads representing temperature data. Set the `msg.routingKey` to values like `temperature.sensor1`, `temperature.sensor2`, etc. 2. Similarly, add `inject` nodes for humidity sensor data. Set the `msg.routingKey` for these nodes to `humidity.sensor1`, `humidity.sensor2`, etc. 3. Drag an `amqp-out` node onto the canvas. Set the exchange type to `"Topic"` and specify the exchange name as `"weather_data"`. 4. Connect each `inject` node to the `amqp-out` node. This setup ensures that each `inject` node sends its data to the `weather_data` exchange with the corresponding routing key. #### Receiving Data using Topic Exchange 1. Add two `amqp-in` nodes on to the canvas. Configure one to listen for messages with the `routingKey` of `"temperature.*"` and the other with `"humidity.*"`. Both nodes should be set to the `"weather_data"` exchange. 2. Connect each `amqp-in` node to a `debug` node to view the sensor data received. :video{ariaLabel="Video showing the flow that uses the Topic exchange type to send messages and receive messages." autoPlay="true" height="826" loop="true" muted="true" playsInline="true" preload="none" width="1920"}*Video showing the flow that uses the Topic exchange type to send messages and receive messages* ::render-flow ```json [{"id":"06ca737a23c93c75","type":"inject","z":"807758ec576fbfd8","name":"Temp sensor 1","props":[{"p":"payload"},{"p":"routingKey","v":"temperature.sensor1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100\t","payloadType":"jsonata","x":180,"y":600,"wires":[["653aec372ecb68a3"]]},{"id":"653aec372ecb68a3","type":"amqp-out","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","reconnectOnError":false,"exchangeName":"weather_data","exchangeType":"topic","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":true,"amqpProperties":"{ \"headers\": {} }","rpcTimeoutMilliseconds":3000,"outputs":0,"x":420,"y":660,"wires":[]},{"id":"39546cb6c75044e2","type":"inject","z":"807758ec576fbfd8","name":"Temp sensor 2","props":[{"p":"payload"},{"p":"routingKey","v":"temperature.sensor2","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100\t","payloadType":"jsonata","x":180,"y":660,"wires":[["653aec372ecb68a3"]]},{"id":"04d4056a8719343d","type":"inject","z":"807758ec576fbfd8","name":"Temp sensor 3","props":[{"p":"payload"},{"p":"routingKey","v":"temperature.sensor3","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100\t","payloadType":"jsonata","x":180,"y":720,"wires":[["653aec372ecb68a3"]]},{"id":"a7c55eb38bb5f828","type":"amqp-in","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","prefetch":0,"reconnectOnError":false,"noAck":true,"exchangeName":"weather_data","exchangeType":"topic","exchangeRoutingKey":"temperature.*","exchangeDurable":true,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{}","x":460,"y":840,"wires":[["0052a9fab812002f"]]},{"id":"4ac0d82df137f4be","type":"amqp-in","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","prefetch":0,"reconnectOnError":false,"noAck":true,"exchangeName":"weather_data","exchangeType":"topic","exchangeRoutingKey":"humidity.*","exchangeDurable":true,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{}","x":450,"y":920,"wires":[["58f86db958f9c32d"]]},{"id":"a367734d77bd5dcd","type":"inject","z":"807758ec576fbfd8","name":"Hum sensor 1","props":[{"p":"payload"},{"p":"routingKey","v":"humidity.sensor1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 200\t","payloadType":"jsonata","x":690,"y":580,"wires":[["04248194cccf8c7a"]]},{"id":"04248194cccf8c7a","type":"amqp-out","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","reconnectOnError":false,"exchangeName":"weather_data","exchangeType":"topic","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":true,"amqpProperties":"{ \"headers\": {} }","rpcTimeoutMilliseconds":3000,"outputs":0,"x":940,"y":640,"wires":[]},{"id":"3cd4806e1bb64564","type":"inject","z":"807758ec576fbfd8","name":"Hum sensor 2","props":[{"p":"payload"},{"p":"routingKey","v":"humidity.sensor2","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100\t","payloadType":"jsonata","x":690,"y":640,"wires":[["04248194cccf8c7a"]]},{"id":"35725dfe285e0db1","type":"inject","z":"807758ec576fbfd8","name":"Hum sensor 3","props":[{"p":"payload"},{"p":"routingKey","v":"humidity.sensor3","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 100\t","payloadType":"jsonata","x":690,"y":700,"wires":[["04248194cccf8c7a"]]},{"id":"0052a9fab812002f","type":"debug","z":"807758ec576fbfd8","name":"Temperature sensors data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":770,"y":840,"wires":[]},{"id":"58f86db958f9c32d","type":"debug","z":"807758ec576fbfd8","name":"Humidity sensors data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":760,"y":920,"wires":[]},{"id":"bfb1e7e97eef5e04","type":"amqp-broker","name":"AMQP Config","host":"localhost","port":"5672","vhost":"","tls":false,"credsFromSettings":false}] ``` :: We explored the Topic type exchange, which allows for more flexible routing using wildcard patterns in the routing key. The example involved a smart weather station where data from various sensors is published and handled based on sensor types. This setup is ideal for situations where you need to route messages based on partial matches or patterns, offering more granular control over message delivery. ## Fanout Exchange Scenario: You have a smart home system with various components, such as lights, thermostats, and security cameras, and you want to broadcast status updates to all components simultaneously. #### Sending Data using Fanout Exchange 1. Drag some inject nodes onto the canvas and set the payload for each. These inject nodes will act as the components sending updates such as lights, thermostats, etc 2. Drag the mqtt-out node onto the canvas, Set the exchange type to `"Fanout"` and specify the exchange name as `"system_updates"` 3. Connect each inject node to the `amqp-out` node. This setup ensures that each status update payload is sent to the "system\_updates" exchange, broadcasting to all subscribed components. #### Receiving Data from Fanout Exchange 1. Drag `amqp-in` nodes onto the canvas. Configure one to listen for messages from the `"weather_data"` exchange. 2. Connect the `amqp-in` node to a `debug` node to see the update received from all your components' data. :video{ariaLabel="Video showing the flow that uses the Fanout exchange type to send messages and receive messages." autoPlay="true" height="824" loop="true" muted="true" playsInline="true" preload="none" width="1918"}*Video showing the flow that uses the Fanout exchange type to send and receive messages.* ::render-flow ```json [{"id":"7beb4237ba09010b","type":"inject","z":"807758ec576fbfd8","name":"Light update","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Light turned on","payloadType":"str","x":170,"y":1220,"wires":[["d699cd735cc8a0ae"]]},{"id":"8818f122c8937e58","type":"inject","z":"807758ec576fbfd8","name":"thermostats update","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"A new firmware update is available for your thermostat","payloadType":"str","x":190,"y":1280,"wires":[["d699cd735cc8a0ae"]]},{"id":"517aad7b163d5bde","type":"inject","z":"807758ec576fbfd8","name":"Camera update","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Movement detected","payloadType":"str","x":180,"y":1340,"wires":[["d699cd735cc8a0ae"]]},{"id":"d699cd735cc8a0ae","type":"amqp-out","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","reconnectOnError":false,"exchangeName":"system_updates","exchangeType":"fanout","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":true,"amqpProperties":"{ \"headers\": {} }","rpcTimeoutMilliseconds":3000,"outputs":0,"x":460,"y":1280,"wires":[]},{"id":"1db4056b4c66cb22","type":"amqp-in","z":"807758ec576fbfd8","name":"","broker":"bfb1e7e97eef5e04","prefetch":0,"reconnectOnError":true,"noAck":false,"exchangeName":"system_updates","exchangeType":"fanout","exchangeRoutingKey":"","exchangeDurable":true,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{}","x":180,"y":1520,"wires":[["2392e7d9139813a3"]]},{"id":"2392e7d9139813a3","type":"debug","z":"807758ec576fbfd8","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":1520,"wires":[]},{"id":"bfb1e7e97eef5e04","type":"amqp-broker","name":"AMQP Config","host":"localhost","port":"5672","vhost":"","tls":false,"credsFromSettings":false}] ``` :: We used a Fanout type exchange to broadcast messages to all queues connected to the exchange. We illustrated this with a smart home system where status updates from different components are sent to all devices simultaneously. This type of exchange is perfect for scenarios where you need to send the same message to multiple recipients without concern for routing keys. ## Headers Exchange **Scenario**: Suppose you have different machines in a factory sending data about their operational status, such as whether they are running, idle, or experiencing an error. You want to route messages based on machine type, operational status, and priority level. Has two components in your monitoring system: one that receives updates from only the CNC machine with the status of error and priority high and another that receives updates from all of the machines with the idle status and high priority. #### Sending Data from Headers Exchange 1. Drag two inject nodes on to the canvas. Configure the first `inject` node to send data with a `msg.properties` of `{"headers":{"machine-type": "CNC," "status": "error," "priority": "high"}}` and the second with a `msg.properties` of `{"headers":{"machine-type": "A," "status": "idle," "priority": "high"}}.` set the payload for each of the inject node you want to send. 2. Drag the amqp-out node onto the canvas, Set the exchange type to `headers,` and specify the exchange name as `system_update.` #### Receiving Data from Headers Exchange 1. Drag two `amqp-in` nodes on to the canvas. Configure one to listen for messages with the `headers` of `{ "x-match": "all," "machine-type": "CNC," "status": "error," "priority": "high"}` and the other with `{ "x-match": "any," machine-type": "A," "status": "idle," "priority": "high"}.` Both nodes should be set to the `system_update` exchange. 2. Connect each `amqp-in` node to a `debug` node to see the updates received for each component. :video{ariaLabel="Video showing the flow that uses the Headers exchange type to send messages and receive messages." autoPlay="true" height="844" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing the flow that uses the Headers exchange type to send messages and receive messages* ::render-flow ```json [{"id":"12c8048f.4eaefb","type":"amqp-in","z":"e4fe9c44.6dee1","name":"","broker":"83e9bf71fbe099c8","prefetch":0,"reconnectOnError":true,"noAck":true,"exchangeName":"machines_update","exchangeType":"headers","exchangeRoutingKey":"","exchangeDurable":false,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{\"x-match\":\"all\",\"machine-type\":\"CNC\",\"status\":\"error\",\"priority\":\"high\"}","x":170,"y":1000,"wires":[["8ec3fa87.70c338"]]},{"id":"6eccc4f.c6a2a3c","type":"amqp-out","z":"e4fe9c44.6dee1","name":"","broker":"83e9bf71fbe099c8","reconnectOnError":true,"exchangeName":"machines_update","exchangeType":"headers","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":false,"amqpProperties":"{}","rpcTimeoutMilliseconds":"","outputs":0,"x":530,"y":780,"wires":[]},{"id":"cb5093bf.1d524","type":"inject","z":"e4fe9c44.6dee1","name":"CNC machine: error occured","props":[{"p":"payload"},{"p":"properties","v":"{\"headers\":{\"machine-type\":\"CNC\",\"status\":\"error\",\"priority\":\"high\"}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Errror occured in the CNC machine","payloadType":"str","x":200,"y":780,"wires":[["6eccc4f.c6a2a3c"]]},{"id":"8ec3fa87.70c338","type":"debug","z":"e4fe9c44.6dee1","name":"Only from CNC machines that has status error and priority high","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":610,"y":1000,"wires":[]},{"id":"d4c2f84b51d2d3cb","type":"amqp-out","z":"e4fe9c44.6dee1","name":"","broker":"83e9bf71fbe099c8","reconnectOnError":true,"exchangeName":"machines_update","exchangeType":"headers","exchangeRoutingKey":"","exchangeRoutingKeyType":"str","exchangeDurable":false,"amqpProperties":"{}","rpcTimeoutMilliseconds":"","outputs":0,"x":530,"y":840,"wires":[]},{"id":"81c21e9941e06a58","type":"inject","z":"e4fe9c44.6dee1","name":"Update from Machine A","props":[{"p":"payload"},{"p":"properties","v":"{\"headers\":{\"machine-type\":\"A\",\"status\":\"idle\",\"priority\":\"high\"}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Machine A is currently idle, awaiting next operation.","payloadType":"str","x":180,"y":840,"wires":[["d4c2f84b51d2d3cb"]]},{"id":"abe55aafb5f65eac","type":"debug","z":"e4fe9c44.6dee1","name":"From all of the machines having status idle or priority high","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":570,"y":1120,"wires":[]},{"id":"adfcaa95122a3556","type":"amqp-in","z":"e4fe9c44.6dee1","name":"","broker":"83e9bf71fbe099c8","prefetch":0,"reconnectOnError":true,"noAck":true,"exchangeName":"machines_update","exchangeType":"headers","exchangeRoutingKey":"","exchangeDurable":false,"queueName":"","queueExclusive":true,"queueDurable":false,"queueAutoDelete":true,"headers":"{\"x-match\":\"any\",\"machine-type\":\"A\",\"status\":\"idle\",\"priority\":\"high\"}","x":170,"y":1120,"wires":[["abe55aafb5f65eac"]]},{"id":"83e9bf71fbe099c8","type":"amqp-broker","name":"","host":"localhost","port":"5672","vhost":"","tls":false,"credsFromSettings":false}] ``` :: Finally, we configured a Headers type exchange, which routes messages based on attributes in the message headers. The example focused on a factory monitoring system, where updates from machines are routed based on criteria like machine type, status, and priority. This exchange type is powerful for complex routing scenarios where decisions are based on multiple attributes rather than just the routing key. # Using Different Protocols for Building Applications with Node-RED In IoT development, effective communication between devices is essential. This communication is facilitated by various protocols like MQTT, HTTP, CoAP, and WebSockets. Each protocol brings its own set of strengths and is suited for different IoT scenarios. However, understanding which protocol is suited for what scenario and utilizing it can be quite difficult. That's why we have created this section of resources where you will find documentation on using different communication protocols with Node-RED. Node-RED, with its intuitive visual programming interface, simplifies the integration of these protocols. Whether you're publishing sensor data over MQTT, triggering HTTP requests, querying CoAP endpoints, or enabling real-time communication with WebSockets, Node-RED provides a flexible and powerful platform. ## Resources Here are some resources to help you get started with integrating Node-RED with various communication protocols: - [Building Secure OPC-UA Server in Node-RED.](https://flowfuse.com/docs/node-red/protocol/opc-ua/): Learn how to build Build and Deploy a custom OPC UA Server in Node-RED - [Using AMQP with Node-RED](https://flowfuse.com/docs/node-red/protocol/amqp/): Learn how to integrate AMQP with Node-RED for reliable message delivery, advanced routing, and improved data management in your flows. - [Using LwM2M with Node-RED](https://flowfuse.com/docs/node-red/protocol/lwm2m/): Learn how to integrate LwM2M with Node-RED for effective IoT device management. This guide covers setup, data handling, and remote commands. - [Using Modbus with Node-RED](https://flowfuse.com/docs/node-red/protocol/modbus/): Learn to use Modbus with Node-RED, including how to build a Modbus server and how to send and read data to and from that server. - [Using MQTT with Node-RED](https://flowfuse.com/docs/node-red/protocol/mqtt/): Learn how to use MQTT with Node-RED. - [Using Websocket with Node-RED](https://flowfuse.com/docs/node-red/protocol/websocket/) ::callout{icon="i-lucide-badge-check"} **Certified nodes for these protocols.** Two protocols are also covered by a FlowFuse certified node, maintained for production use. EtherNet/IP, for Rockwell and Allen-Bradley controllers, has no page of its own in this section, and is documented with its certified node: [cip-suite](https://flowfuse.com/integrations/?certified=1), [opcua](https://flowfuse.com/integrations/?certified=1) :: # Using LwM2M with Node-RED IoT devices, especially those designed for low-power operation, can be difficult to manage due to their limited resources and the need for efficient communication and control. This is where LwM2M (Lightweight Machine-to-Machine) comes in. LwM2M is designed to help you monitor, update, and control your devices with minimal overhead, making it ideal for everything from smart sensors to industrial equipment. In this post, we'll explore how you can use LwM2M with Node-RED. It is ideal for anyone starting their journey with LwM2M or Node-RED. ## What is LwM2M [LwM2M](https://lwm2m.openmobilealliance.org/){rel=""nofollow""} (Lightweight Machine-to-Machine) is a protocol specifically crafted to handle and interact with IoT devices, particularly those that consume less energy and have limited resources. LwM2M facilitates easy remote oversight, control, and administration by linking these devices to a central server. In LwM2M, the server can send commands, gather data, and change device settings using the Constrained Application Protocol (CoAP), which is designed for environments with limited resources. LwM2M also includes security features like DTLS, making it a reliable choice for managing many devices in large IoT systems, such as smart cities and industrial applications. ## Using LwM2M with Node-RED In this section, I will demonstrate how you can monitor and control IoT devices using LwM2M with Node-RED. For demonstration purposes, I'll show you how to monitor and control an Ubuntu machine running Node-RED via the [FlowFuse Device Agent](https://flowfuse.com/platform/device-agent/). This setup will help you understand how to use LwM2M to manage your devices remotely. ### Prerequisites - **node-red-contrib-lwm2m:** Install the LwM2M contribution node via the Palette Manager in Node-RED. - **LwM2M Server:** Ensure you have a running OMA LwM2M server available and have its configuration details on hand. For more information, refer to [Eclipse Leshan](https://eclipse.dev/leshan/){rel=""nofollow""}. *Note: In this guide, we have used the Eclipse Leshan demo server for testing and demonstration purposes. It is not recommended for production use due to its security and scalability limitations* ### Configuring LwM2M Node 1. Drag the LwM2M node onto the canvas in Node-RED. 2. Double-click on the LwM2M node to open its configuration window. 3. Click the "+" icon next to the Client field to add a new client. 4. Enter a unique endpoint in URN format. This endpoint should be a unique identifier for your device, typically in the format `urn:uuid:,` where `` is a UUID or custom string specific to your device. Ensure that each device has a distinct endpoint to avoid conflicts. 5. Enter the server host. 6. Enter the server port: - For plain UDP, use port `5683`. - For DTLS (encrypted communication), use port `5684`. 7. Check the "Enable DTLS" option to enable secure communication. 8. Enter the PSK (Pre-Shared Key) details if DTLS is enabled. 9. Add the object to the "Objects" field. This allows you to include custom objects and resources specific to your application that can be handled similarly to other objects, the id of this custom object should be in the range of 10241 - 32768. Below is an example object: ```json { "32764": { "0": { "0": { "type": "STRING", "acl": "RW", "value": "abcd" }, "1": { "type": "INTEGER", "acl": "RW", "value": 123456 }, "2": { "type": "BOOLEAN", "acl": "RW", "value": true } } } } ``` 9. If you need to manage sensitive data, ensure you enable the "Hide Sensitive Data" option. This will prevent sensitive information about the device from being exposed. 10. Click "Add" to save the configuration. Once you've configured the LwM2M node with the server details, you can confirm that the client is connected by visiting the server's web UI. Navigate to `/#/clients` and enter the endpoint of your client device in the search field. If the client appears in the list, it means it is connected. Alternatively, you can check the node's status in Node-RED, which will display "connected" if the connection is successful. ### Reading Device Configuration and Data on the Server 1. Navigate to your server’s clients section and select the device URN you registered. 2. In the new window that opens, look at the top-left for information about your device, including registration and update details. You can also configure settings such as request timeout and data types for single and multi-value writes. 3. Below this, you'll find the available objects that you can control for your devices and server. 4. Click on the "Device" object option to read device realted information. In the instance 0 section click on the "R" for each object you want. Alternatively, you can click on the top "R" next to instance 0 or "Device-v1.0" to read all values at once. Note that this may not work if any values are unavailable for your device, it will return 404 not found. :video{ariaLabel="Video showing LwM2M Server reading the device details" autoPlay="true" height="838" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing LwM2M Server reading the device details* You can now read information such as device battery level, available memory, device manufacturer, timezone, device type, and a lot. ### Writing Data and Executing Commands to a Device on the Server 1. Navigate to your server’s clients section and select the device URN you registered. 2. Click on the object to which you want to write data. 3. In the list of resources, identify those with write permission and click on the "w" icon to initiate the write process. 4. In the form that opens, enter the new value in the correct format. 5. Click on "Write" to update the value for that resource. :video{ariaLabel="Video showing how to perform write operation in the LwM2M server" autoPlay="true" height="838" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing how to perform write operation in the LwM2M server* 6. Drag the **lwm2m client** node onto the canvas, select the correct configuration, and enable the "Subscribe LwM2M object events" option. This setting will trigger and send an event object when commands are executed on the server. 7. Drag an exec node onto the canvas and add the command you want to execute. For example, you can add the "reboot" command. 8. Connect the output of the **lwm2m client** node to the input of the **exec** node. 9. To execute the commands, click on the 'exec' option next to resources such as Reboot. :video{ariaLabel="Video showing the LwM2M server executing reboot command for device" autoPlay="true" height="772" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing the LwM2M server executing reboot command for device* ### Reading Data and Configuration from the LwM2M Server in Node-RED 1. Drag the **lwm2m client** node onto the canvas. Ensure that you have selected the correct configuration for it. 2. Drag the **inject** node onto the canvas. Set the topic in the format `/ObjectID/ObjectInstanceID/ResourceID`. For example, to read the manufacturer’s available free memory, which is in the Object `3`, Instance `0`, and has Resource ID `10`, set the topic to `/3/0/10`. 3. Add a **debug** node onto the canvas to display the read values in the debug panel for verification. 4. Connect the output of the **inject** node to the input of the LwM2M client node, and connect the output of the **lwm2m client** node to the input of the **debug** node. 5. Deploy the flow by clicking on the top-right "deploy" button. :video{ariaLabel="Video showing Node-RED flow that reading data from LwM2M Server" autoPlay="true" height="830" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing Node-RED flow that is reading data from LwM2M Server* ### Writing data and configuration to the LwM2M Server from Node-RED 1. Drag an **inject** node onto the canvas. 2. Double-click on it and set the `msg.payload` to the updated value. 3. Set the `msg.topic` to the resource notion in the correct format `/ObjectID/ObjectInstanceID/ResourceID` 4. Drag the **lwm2m client** out node onto the canvas, and select the correct server configuration. 5. Connect the **inject** node's output to the input of **lwm2m client out** node. 6. Deploy the flow and click the inject button to perform the write operation. :video{ariaLabel="Video showing Node-RED flow that writing data to LwM2M Server" autoPlay="true" height="832" loop="true" muted="true" playsInline="true" preload="none" width="1908"}*Video showing Node-RED flow that is writing data to LwM2M Server* In the same way, you can execute commands from node-red. You have to replace the notion and end that notion with `execute`, like `0/0/4/execute.` When executing the command, you will not have to specify the `msg.payload`. # Using Modbus with Node-RED In manufacturing companies there is often a small set of production data, currently only available to an equipment operator through the HMI, which would be enormously valuable to a greater audience if there were some way to easily display and share it. Node-RED, along with Modbus and Dashboard modules, can easily create a web-based dashboard, shareable with a weblink and viewable on any web browser on the network. Imagine the advantages of digital signage in the breakroom spurring healthy competition or a manager being able to check daily totals and live process values from the phone in their pocket. ::cta-image --- alt: Read Modbus data from every device on your floor, not just one instance cta: sign-up reference: "Node-RED: Using Modbus with Node-RED" src: https://flowfuse.com/docs/node-red/protocol/images/modbus-node-red-cta-1.png --- :: ## What is Modbus Modbus is a serial protocol that is often found in the industrial world to allow devices to communicate. Originally developed by Schneider Electric, it is an open protocol and has been adopted by brands across the industry. [Simply Modbus](https://www.simplymodbus.ca/){rel=""nofollow""} is a terrific resource to learn more about how the communication is structured. The beauty of Node-RED’s low-code environment is that a user only has to understand Modbus at the highest level to be able to implement it. The transport layer for Modbus can be either TCP over the Internet or RTU over RS-485/422/232. There is a client-server relationship among devices where the clients read and write data which is stored by server using a numerical address. There are four types of these addresses, 1) Output Coil and 2) Discrete Input addresses, which hold 1-bit data, and 3) Input Register and 4) Holding Register addresses, which hold 16-bit data. Typically a PLC will be the Server and an HMI will be the client, reading and writing to the memory in the PLC, in order to give an operator control over machinery. ## What is an HMI An HMI, or human machine interface, is a piece of software that allows an operator to use a machine. An HMI development environment typically allows programmers to choose among an array of digital assets to visualize the machine on the screen and create an intuitive interface to control the machine. The HMI software may also offload some of the high-level logic from the PLC, however, the time-critical lower-level logic should stay on the PLC. Node-RED can take this a step further, you may use it to create a simple HMI, but its real strength comes from its internet based heritage, and its ability to help share data from the PLC to the cloud. Let’s look at the details of how you would use Node-RED for HMI and Modbus to build an HMI with Node-RED to connect Modbus data to a dashboard accessible from any web browser. ## Installation of the Modbus package The most popular package used for connecting Modbus devices is [node-red-contrib-modbus](https://flows.nodered.org/node/node-red-contrib-modbus){rel=""nofollow""}; it has a wide range of configuration options and is well-documented in many blogs. On its own, this Modbus package just provides the means of communicating the 1-bit and 16-bit data. In doing so, your flow will be able to write 1-bit and 16-bit data to the PLC and read 1-bit and 16-bit data, which will arrive in an array. So, just like with other protocols (MQTT, HTTP, etc) fully integrating Modbus into your flow requires data manipulation and a well-thought-out schema for how this data will be packed into your msg objects. For example, below a payload of [false,false,false] comes in from a “Modbus Read” node, but how do you turn that into useful information? Maybe, you want to work with all alarms as a group, use a “Change” node to create a payload that is an object holding the related keys, with a topic that lets us know that these are all “alarms.” ![Configuring the Modbus node](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-13.png "Configuring the Modbus node") Note: for an even more comprehensive node to parse this data, check out [node-red-contrib-buffer-parser](https://flows.nodered.org/node/node-red-contrib-buffer-parser){rel=""nofollow""} by Flowforge’s own, Steve McLaughlin. To install, first click on the hamburger menu in the upper right of the Node-RED editor and then click on “Manage palette.” ![Accessing the palette manager](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-8.png "Accessing the palette manager") Next, click on the “Install” tab, search for “modbus” in the search bar, and click on the “install” button next to [node-red-contrib-modbus](https://flows.nodered.org/node/node-red-contrib-modbus){rel=""nofollow""}. As you can see there are many other custom nodes, but this one is a great jumping off point. It's always good to try other options too, and see what the community has to offer. ![Installing the custom node](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-10.png "Installing the custom node") Finally, click on the “Install” button in the pop-up. ![Installing the custom node](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-1.png "Installing the custom node") Success, your new set of nodes are ready to use. ![The new nodes are now in the palette](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-6.png "The new nodes are now in the palette") Similarly, install one more package `@flowfuse/node-red-dashboard`. This package contains a set of widgets we will use to build a dashboard for visualizing data. For more information visit [Node-RED Dashboard 2.0 Official website](https://dashboard.flowfuse.com/){rel=""nofollow""}. ## Bulding Modbus server When using Modbus for communication, it is necessary to have a Modbus server, which acts as a middleman. In our case we are building that server on the Node-RED instance running on PLC running a motor turning a belt with a belt scale. ![Configuring the Modbus server node](https://flowfuse.com/docs/node-red/protocol/images/modbus-server.png "Configuring the Modbus server node") Add the Modbus server node and configure it as shown in the above image. This node is set to handle up to 1000 coils, discrete inputs, holding registers, and input registers. This means the server can manage up to 1000 binary states for control, monitor 1000 binary states, store up to 1000 read/write data points, and monitor 1000 read-only data points. Setting these parameters to 1000 allows the server to handle a broad range of devices and data points within your Modbus network, ensuring flexibility and scalability. ## Sending data to Modbus server To send data to the Modbus server, add two write nodes to the workspace. Double click on each node to configure them. Click on the pencil icon next to the "Server" field to add the Modbus server details. Once the server is added into one write node, it will be available for the other write node as well. We have added two write nodes because we will be sending following simulated data to the server. One write node will handle coil data and the other will handle register data. ![Example data from Modbus](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-14.png "Example data from Modbus") For the first write node, set the quantity to 5 since we will be sending five types of coil data (isEStopReleased, isMotorSwitchedOn, isMotorRunning, isTailSwitchPulsing, isMaterialOnBelt ). For the second write node, set the quantity to 4 since we will be sending four types of register data( MotorAmps, motorHourMeter, beltTonsPerHour, and beltTotalTons ). ![Configuring the Modbus send output coils write node](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-send-output-coil.png "Configuring the Modbus send output coils write node") ![Configuring the Modbus send output coils write node](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-send-holding-registers.png "Configuring the Modbus send output coils write node") After configuring the nodes, add an inject node and set the `msg.payload` to "true" as a boolean for the coil data and set repeat to the "20 seconds" of interval. Then, add a join node to combine the messages into an array and connect the wires towards the write node configured for coil data. ![Configuring the join node combine output coil data](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-combine-output-coils-data.png "Configuring the join node combine output coil data") Similarly, add another inject node and set the `msg.payload` to `random() * 200` as a JSONata expression for the register data and set repeat to the "20 seconds" of interval. Use a join node to combine the 4 messages into an array and connect the wires towards the write node configured for register data. ![Configuring the join node combine output coil data](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-combine-holding-register-data.png "Configuring the join node combine output coil data") Finally, you can add Modbus response nodes to see the data sent over modbus server. ## Reading data from Modbus server Now we will read that simulated that is getting sent on modbus server. All of this data is related so it has been grouped by consecutive numbers to make acquiring the data simpler. You can also group data by the rate you expect to be polling for it, so that your Modbus nodes don’t have to make several calls to collect the data. In the Modbus protocol the client specifies a start address and a number of subsequent addresses to read, and the server responds with all of this data at once. Creating groups allows much more efficient communication. This PLC uses the coil/register numbering convention with output coils in the 0nnnnn format and the holding registers in the 4nnnnn format. Our Modbus nodes in Node-RED use a data address numbering convention which is zero-based, so we will have to remember to subtract 1 from the coils and registers. Two “Modbus Read” nodes will work to capture these two types of data, coils and registers. Drag them into the flow and double click on one to start configuring them. First we will have to specify our Modbus Server, so click on the pencil icon to “add new.” In the next “Modbus Read” node we configure, we can just select our newly added server from the drop-down menu. ![Adding a Modbus server](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-3.png "Adding a Modbus server") Let’s assume that your PLC is connected to your local area network and we will be communicating over TCP. Enter in the IP address of the PLC, the rest of the configuration can be left as-is. 502 is the default port for Modbus and generally the Unit-Id is 1, sometimes 0, sometimes ignored. The “Queues” and “Optionals” can stay as-is as well. ![Setting the protocol and IP address](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-2.png "Setting the protocol and IP address") Click On “Add” and you will see your new server selected in the drop-down menu. Now let’s set this “Modbus Read” node to read our Coils once every second. ![Setting how often data is read in the first Modbus read node](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-5.png "Setting how often data is read in the first Modbus read node") Similarly, set up the other “Modbus Read” node to read the holding registers. Click on the “Done” Button. Why the Modbus standard uses FC 3 to read the 4nnnnn registers and why there is both a zero-based and one-based convention is just a painful reality when using Modbus. ![Setting how often data is read in the second Modbus read node](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-15.png "Setting how often data is read in the second Modbus read node") You can add some “Modbus Response” nodes to the “Modbus Read” nodes and click “Deploy” in order to see the data coming through, right in the editor. ![Checking the data is arriving OK](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-7.png "Checking the data is arriving OK") ## Simple visualization Finally, let’s create a dashboard of this incoming data using node-red-dashboard. ![Example dashboard showing the data](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-dashboard.png "Example dashboard showing the data") “Change” nodes are an easy way to split apart the arrays of coils and registers into discrete messages. ![Splitting up the data using change nodes](https://flowfuse.com/docs/node-red/protocol/images/modbus-1-9.png "Splitting up the data using change node") The `msg.payload` is set to the entry at the correct index of the incoming `msg.payload` array, and the msg.fontColor is set using conditional formatting of “green” and “red”, for true and false, respectively. ![Image showing change node config for retrieving and setting data from an array read from Modbus.](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-change-node.png "Image showing change node config for retrieving and setting data from an array read from Modbus.") The output coil data is displayed on the dashboard using a text widget. When the value is false, the color will be red; otherwise, it will be green, indicating the active status. ![Configuring the change node](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-text-node.png "Configuring the change node") ![Image showing added style to the tempalate widget](https://flowfuse.com/docs/node-red/protocol/images/modbus-with-node-red-template-widget-stylesheet.png "Image showing added style to the tempalate widget") Final flow is given below: ::render-flow{:height='300'} ```json [{"id":"4cf44f4cbc592b99","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"b48b43414657ca4e","type":"modbus-read","z":"4cf44f4cbc592b99","name":"","topic":"text","showStatusActivities":false,"logIOActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"Coil","adr":"0","quantity":"5","rate":"1","rateUnit":"s","delayOnStart":false,"startDelayTime":"","server":"145bc96e15c34554","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"x":130,"y":540,"wires":[["f6fb47f18928d28f","3bf66c619694a1a4","35bc45f58dc5c661","5b3a8b0c7cd968fa","f87af242b2287b7c"],[]]},{"id":"e28ae04240af481e","type":"modbus-read","z":"4cf44f4cbc592b99","name":"","topic":"","showStatusActivities":false,"logIOActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"HoldingRegister","adr":"0","quantity":"4","rate":"1","rateUnit":"s","delayOnStart":false,"startDelayTime":"","server":"145bc96e15c34554","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"x":130,"y":800,"wires":[["9dd39216e9bbd6fd","c4389848cebd29c7","dffa222847521883","286d69415ef78e7c"],[]]},{"id":"c302edd625716894","type":"modbus-server","z":"4cf44f4cbc592b99","name":"Modbus server","logEnabled":false,"hostname":"127.0.0.1","serverPort":"10502","responseDelay":100,"delayUnit":"ms","coilsBufferSize":10000,"holdingBufferSize":10000,"inputBufferSize":10000,"discreteBufferSize":10000,"showErrors":false,"x":520,"y":260,"wires":[[],[],[],[],[]]},{"id":"7f20fcaea773cb1b","type":"inject","z":"4cf44f4cbc592b99","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":260,"wires":[["c302edd625716894"]]},{"id":"bc7bdfd9d81369af","type":"inject","z":"4cf44f4cbc592b99","name":"","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$random() * 150","payloadType":"jsonata","x":110,"y":1020,"wires":[["2c019b52b54a2e0d"]]},{"id":"2c019b52b54a2e0d","type":"join","z":"4cf44f4cbc592b99","name":"combine holiding register data","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"4","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":350,"y":1020,"wires":[["8bf7d588de60fb33"]]},{"id":"6c14cb9ab888c12d","type":"inject","z":"4cf44f4cbc592b99","name":"","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":110,"y":1140,"wires":[["1a59e443e0258194"]]},{"id":"1a59e443e0258194","type":"join","z":"4cf44f4cbc592b99","name":"combine coil output data","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"5","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":330,"y":1140,"wires":[["cdb7e5d53210cb6d"]]},{"id":"62338bd2af1d3c0d","type":"modbus-response","z":"4cf44f4cbc592b99","name":"","registerShowMax":20,"x":870,"y":1020,"wires":[]},{"id":"d1eff51eac0af203","type":"modbus-response","z":"4cf44f4cbc592b99","name":"","registerShowMax":20,"x":870,"y":1120,"wires":[]},{"id":"8bf7d588de60fb33","type":"modbus-write","z":"4cf44f4cbc592b99","name":"send holding registers","showStatusActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"MHoldingRegisters","adr":"0","quantity":"4","server":"145bc96e15c34554","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":640,"y":1020,"wires":[["62338bd2af1d3c0d"],[]]},{"id":"cdb7e5d53210cb6d","type":"modbus-write","z":"4cf44f4cbc592b99","name":"send output coils ","showStatusActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"MCoils","adr":"0","quantity":"5","server":"145bc96e15c34554","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":630,"y":1140,"wires":[["d1eff51eac0af203"],[]]},{"id":"405acf6300a072d6","type":"ui-text","z":"4cf44f4cbc592b99","group":"a00b86fb96b216a6","order":1,"width":0,"height":0,"name":"","label":"isEStopReleased","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":690,"y":460,"wires":[]},{"id":"52cb610e23488ba2","type":"ui-text","z":"4cf44f4cbc592b99","group":"a00b86fb96b216a6","order":3,"width":0,"height":0,"name":"","label":"isMotorSwitchedOn","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":690,"y":500,"wires":[]},{"id":"224a2c2948d7d9a5","type":"ui-text","z":"4cf44f4cbc592b99","group":"a00b86fb96b216a6","order":4,"width":0,"height":0,"name":"","label":"isMotorRunning","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":680.111083984375,"y":537.7777709960938,"wires":[]},{"id":"82fce135993ad1ab","type":"ui-text","z":"4cf44f4cbc592b99","group":"a00b86fb96b216a6","order":5,"width":0,"height":0,"name":"","label":"isTailswitchPulsing","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":690,"y":580,"wires":[]},{"id":"f6fb47f18928d28f","type":"change","z":"4cf44f4cbc592b99","name":"isEStopReleased","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0]","tot":"msg"},{"t":"set","p":"class","pt":"msg","to":"msg.payload ? \"green\":\"red\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":460,"wires":[["405acf6300a072d6"]]},{"id":"3bf66c619694a1a4","type":"change","z":"4cf44f4cbc592b99","name":"isMotorSwitchedOn","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[1]","tot":"msg"},{"t":"set","p":"class","pt":"msg","to":"msg.payload ? \"green\":\"red\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":500,"wires":[["52cb610e23488ba2"]]},{"id":"35bc45f58dc5c661","type":"change","z":"4cf44f4cbc592b99","name":"isMotorRunning","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[2]","tot":"msg"},{"t":"set","p":"class","pt":"msg","to":"msg.payload ? \"green\":\"red\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":540,"wires":[["224a2c2948d7d9a5"]]},{"id":"5b3a8b0c7cd968fa","type":"change","z":"4cf44f4cbc592b99","name":"isTailswitchPulsing","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[3]","tot":"msg"},{"t":"set","p":"class","pt":"msg","to":"msg.payload ? \"green\":\"red\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":580,"wires":[["82fce135993ad1ab"]]},{"id":"f87af242b2287b7c","type":"change","z":"4cf44f4cbc592b99","name":"isMaterialOnBelt","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[4]","tot":"msg"},{"t":"set","p":"class","pt":"msg","to":"msg.payload ? \"green\":\"red\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":620,"wires":[["7ab41bd5d19d5f7f"]]},{"id":"7ab41bd5d19d5f7f","type":"ui-text","z":"4cf44f4cbc592b99","group":"a00b86fb96b216a6","order":2,"width":0,"height":0,"name":"","label":"isMaterialOnBelt","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":680,"y":620,"wires":[]},{"id":"cc8b32c267214e28","type":"ui-template","z":"4cf44f4cbc592b99","group":"","page":"","ui":"5ff82627d2caf841","name":"stylesheet","order":0,"width":0,"height":0,"head":"","format":".green span {\n color: green;\n}\n\n.red span {\n color: red;\n}\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"site:style","className":"","x":940,"y":560,"wires":[[]]},{"id":"c4f0157670882335","type":"ui-gauge","z":"4cf44f4cbc592b99","name":"motorAmps","group":"ef2cffe3b927bbb6","order":1,"width":"0","height":"0","gtype":"gauge-half","gstyle":"needle","title":"gauge","units":"units","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#ba6e26"},{"from":"100","color":"#ba6e26"}],"min":0,"max":"150","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":710,"y":760,"wires":[]},{"id":"df73d7c58bbf6069","type":"ui-text","z":"4cf44f4cbc592b99","group":"ef2cffe3b927bbb6","order":2,"width":0,"height":0,"name":"","label":"motorHourMeter","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":720,"y":800,"wires":[]},{"id":"4d14a50a008a4606","type":"ui-text","z":"4cf44f4cbc592b99","group":"98ab0c19d66959eb","order":2,"width":0,"height":0,"name":"","label":"beltTotalTons","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":710,"y":880,"wires":[]},{"id":"9dd39216e9bbd6fd","type":"change","z":"4cf44f4cbc592b99","name":"motorAmps","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":760,"wires":[["c4f0157670882335"]]},{"id":"c4389848cebd29c7","type":"change","z":"4cf44f4cbc592b99","name":"motorHourMeter","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[1]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":800,"wires":[["df73d7c58bbf6069"]]},{"id":"dffa222847521883","type":"change","z":"4cf44f4cbc592b99","name":"beltTonsPerHour","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[2]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":840,"wires":[["b45120d07ed4a8bb"]]},{"id":"286d69415ef78e7c","type":"change","z":"4cf44f4cbc592b99","name":"beltTotalTons","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[3]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":880,"wires":[["4d14a50a008a4606"]]},{"id":"b45120d07ed4a8bb","type":"ui-chart","z":"4cf44f4cbc592b99","group":"98ab0c19d66959eb","name":"beltTonsPerHour","label":"chart","order":1,"chartType":"line","category":"beltTonsPerHour","categoryType":"str","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisLabel":"","yAxisProperty":"","ymin":"","ymax":"","action":"append","pointShape":"false","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"6","height":"5","className":"","x":730,"y":840,"wires":[[]]},{"id":"af0626137803b160","type":"comment","z":"4cf44f4cbc592b99","name":"Reading holding register data from modbus server","info":"","x":510,"y":700,"wires":[]},{"id":"bf74f24e625763ff","type":"comment","z":"4cf44f4cbc592b99","name":"Sending output coil data to modbus server","info":"","x":480,"y":1080,"wires":[]},{"id":"c5ab6f8dcb96b79b","type":"comment","z":"4cf44f4cbc592b99","name":"Sending holding register data to modbus server","info":"","x":500,"y":960,"wires":[]},{"id":"8e1387959a33f353","type":"comment","z":"4cf44f4cbc592b99","name":"Reading output coil data from modbus server","info":"","x":490,"y":400,"wires":[]},{"id":"b85a4192ee8a8c38","type":"comment","z":"4cf44f4cbc592b99","name":"Modbus server ","info":"","x":300,"y":180,"wires":[]},{"id":"145bc96e15c34554","type":"modbus-client","name":"Modbus server","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"queueLogEnabled":false,"failureLogEnabled":true,"tcpHost":"127.0.0.1","tcpPort":"10502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","serialAsciiResponseStartDelimiter":"0x3A","unit_id":"1","commandDelay":"1","clientTimeout":"1000","reconnectOnTimeout":true,"reconnectTimeout":"2000","parallelUnitIdsAllowed":true,"showErrors":false,"showWarnings":true,"showLogs":true},{"id":"a00b86fb96b216a6","type":"ui-group","name":"Output coils","page":"3876c3cdc50d68a5","width":"3","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"5ff82627d2caf841","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default","titleBarStyle":"default"},{"id":"ef2cffe3b927bbb6","type":"ui-group","name":"Holding registers","page":"3876c3cdc50d68a5","width":"3","height":"3","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"98ab0c19d66959eb","type":"ui-group","name":"holding register ","page":"3876c3cdc50d68a5","width":"6","height":"1","order":3,"showTitle":false,"className":"","visible":"true","disabled":"false"},{"id":"3876c3cdc50d68a5","type":"ui-page","name":"Home","ui":"5ff82627d2caf841","path":"/","icon":"home","layout":"grid","theme":"36efce4ebdf1ae91","order":1,"className":"","visible":false,"disabled":"false"},{"id":"36efce4ebdf1ae91","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}] ``` :: The way that we set up this example worked well for the small sample size and quickly getting content on the dashboard, but as you work through your own needs, think about the data structures that will be the most conducive to efficiently working with your data. Modbus is one of several industrial protocols FlowFuse supports for connecting PLCs to MQTT, cloud platforms, and enterprise systems. See the [FlowFuse PLC integration overview](https://flowfuse.com/landing/plc/) for OPC UA, EtherNet/IP, Siemens S7, and more. # Using MQTT with Node-RED Getting devices to talk to each other in industrial environments isn't trivial. You're dealing with spotty networks, power constraints, and devices that need to share data without constant back-and-forth polling. MQTT solves these problems by keeping communication lightweight and flexible. This guide walks you through what MQTT is, why it works well for IIoT, and how to get it running with Node-RED. ## Understanding MQTT [MQTT](https://en.wikipedia.org/wiki/MQTT){rel=""nofollow""} is a messaging protocol that's been around since 1999. It uses a publish-subscribe model, which is different from the request-response pattern you see with HTTP. Here's the basic setup: devices/systems (clients) connect to a central server called a broker. When a device has data to share, it publishes a message to a specific topic. Other devices/systems subscribe to topics they care about and automatically receive messages when they're published. The broker handles all the routing. This architecture means devices/systems don't need to know about each other or maintain direct connections. A temperature sensor can publish readings to `enterprise/site/area/line/cell/temperature` without caring who's listening. ## Setting Up MQTT in Node-RED Node-RED comes with MQTT nodes built in, so you don't need to install anything extra. You'll need access to an MQTT broker - you can use a cloud service, run your own with Mosquitto, use a public test broker for experimenting, or if you're using FlowFuse, it provides a [built-in MQTT broker service](https://flowfuse.com/blog/2024/10/announcement-mqtt-broker/). ## Configuring the MQTT Broker Connection Before you can publish or subscribe to messages, you need to configure the broker connection. You only need to do this once - the same broker configuration can be reused across multiple MQTT nodes. 1. Add either an **MQTT out** or **MQTT in** node to your canvas 2. Double-click the node to open its configuration 3. Click the pencil icon next to "Server" to add a new broker connection 4. Enter your broker's address (e.g., `broker.flowfuse.cloud`) 5. Add the port (usually 1883 for unencrypted, 8883 for TLS) 6. If your broker requires authentication, switch to the Security tab in the same configuration dialog by clicking on it "Security", then enter your username and password 7. Give the connection a name and click Add Once configured, this broker connection will appear in the Server dropdown for all MQTT nodes in your flow. ![Configuring an MQTT broker in Node-RED](https://flowfuse.com/docs/node-red/protocol/images/mqtt-broker-config.png)*Setting up the broker connection* ## Publishing Messages to a Broker Let's start by sending data to an MQTT broker. 1. Add an **MQTT Out** node. 2. Connect your data source node’s output (use an Inject node to simulate data if you don’t have one) to the **MQTT Out** node. 3. Double-click the **MQTT Out** node. 4. Select your configured broker from the **Server** dropdown (or create a new one by following the steps above). 5. Enter a topic such as `enterprise/site/area/line1/cell/temperature` (topics use forward slashes as separators, similar to file paths). 6. Set the **QoS** level if needed for message reliability, and enable **Retain** if you want the broker to store the last published message for new subscribers. 7. Click **Done**. ![MQTT Out node, publishing data to a topic](https://flowfuse.com/docs/node-red/protocol/images/mqtt-out.png)*MQTT Out node, publishing data to a topic* 9. Click Deploy The MQTT out node should show "connected" with a green dot. ![Connected MQTT node showing green status](https://flowfuse.com/docs/node-red/protocol/images/connected-mqtt-node.png)*Green status means you're connected* ## Subscribing to Messages from a Broker Now let's receive messages from the MQTT broker. 1. Add an **MQTT In** node to the canvas. 2. Add a **Debug** node. 3. Connect the **MQTT In** node to the **Debug** node. 4. Double-click the **MQTT In** node. 5. Select your configured broker from the **Server** dropdown (or create a new one if needed). 6. Set the **Action** to **Subscribe to a single topic**. 7. Enter the topic you want to subscribe to, for example: `enterprise/site/area/+/cell/temperature` (the `+` symbol acts as a wildcard for one level). 8. Set the **QoS** level based on your reliability requirements. 9. Click **Done**, then **Deploy** your flow. Once deployed, you should see the messages appear in the **Debug** sidebar. ![MQTT In node, subscribing to a topic](https://flowfuse.com/docs/node-red/protocol/images/mqtt-in-config.png)*MQTT In node, subscribing to a topic* ## Using Wildcards in Topics MQTT supports wildcards for subscribing to multiple topics at once: - **Single-level wildcard (+)**: Matches one level. `enterprise/site/area/+/cell/temperature` matches `enterprise/site/area/line1/cell/temperature` and `enterprise/site/area/line2/cell/temperature` but not `enterprise/site/area/line1/cell/station1/temperature` - **Multi-level wildcard (#)**: Matches multiple levels. `enterprise/site/area/#` matches everything under that area, including `enterprise/site/area/line1/cell/temperature` and `enterprise/site/area/line1/cell/station1/pressure` When deployed you should again see the status bubble turn green, and have a timestamp appear in the sidebar every second! # Building Secure OPC-UA Server in Node-RED. OPC-UA (OPC Unified Architecture) is a communication protocol designed for industrial automation. It enables seamless data exchange and interoperability between various devices, systems, and software applications in the industrial domain. OPC-UA offers secure and reliable communication, making it a preferred choice for building robust industrial solutions. In this document, we will delve into the creation of a fully custom secure OPC-UA Server for PLCs in Node-RED. If you're not familiar with OPC-UA, you can learn more about it [here](https://flowfuse.com/blog/2023/07/how-to-deploy-a-basic-opc-ua-server-in-node-red/). ## Introduction While it's typical to find PLCs that have built-in OPC-UA server capabilities, such as Omron and Siemens, this is not an industry-wide practice. One notable exception is Allen Bradley PLCs. For Allen Bradley, you have to buy FactoryTalk Linx Gateway (formally RSLinx Enterprise) for OPC-UA Server capability, or you need to employ a 3rd party OPC-UA Server. This documentation will guide you through the process of using Node-RED as a 3rd party OPC-UA Server for Allen Bradley, by creating a custom Information Model for the PLC data, publishing it, then securing the server with SSL to make it production-ready. ## PLC to OPC-UA Server Architecture Overview A visual representation of our PLC to OPC-UA Server architecture is shown in the drawing below, consisting of 6 major parts. ![PLC-Information-Model-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-1.png) 1. Set up the PLC tags to be sent to the OPC Server 2. Read the PLC tags into Node-RED 3. Copy the PLC tags into Node-RED context memory 4. Program the OPC Server address space 5. Encrypt the OPC Server with SSL 6. Set up the OPC Client The PLC is an Allen Bradley, and an instance of Node-RED running on the same OT network as the PLC will act as the OPC UA Server. In our Allen Bradley PLC, we will re-use an example from a [Node-RED as a No-Code EtherNet/IP to S7 Protocol Converter](https://flowfuse.com/blog/2023/06/node-red-as-a-no-code-ethernet_ip-to-s7-protocol-converter/) where the PLC is simulating a conveyor line, called *Line 4 PLC,* depicted as number 1 architecture drawing above. The tags below represent the data to be transferred from the Line 4 PLC to the Node-RED OPC UA server, depicted as number 2 in the architecture drawing. | **Tag** | **Data Type** | **Description** | | ----------------- | ------------- | ----------------------------- | | Conveyor\_RTS | BOOL | Conveyor Ready to Start | | Robot\_RTS | BOOL | Conveyor Robot Ready to Start | | Robot\_Position | REAL | Robot Arm position (degrees) | | Conveyor\_Running | BOOL | Conveyor is running | | Line4\_State | DINT | Line 4 Machine State | | Line4\_Fault | BOOL | Line 4 is faulted | A simple ladder application has been built in the PLC to simulate our conveyor values. ![image-20230717-212515.png](https://flowfuse.com/docs/node-red/protocol/images/image-20230717-212515.png) The Line 4 PLC tags will be read by Node-RED using an Ethernet/IP driver, with each PLC tag copied to flow context memory as part of an object named `conveyorData`, depicted in number 3 of the architecture drawing. Using the `node-red-contrib-opcua-server` node, the `conveyorData` object will become part of a hierarchical OPA UA Information Model representing the Line 4 PLC conveyor data, and stored into the *OPC UA Server Address Space,* depicted as number 4 in the architecture drawing. The OPC Server will publish the Line 4 PLC conveyor data, implementing a self-signed SSL certificate to encrypt the OPC traffic and establish a secure connection with an OPC Client application, depicted as number 5 in the architecture drawing. - note - if you prefer not to secure the server, you can skip this step and still connect to the server anonymously for testing purposes. The OPC client will be a windows-based [Prosys OPC UA Browser](https://www.prosysopc.com/products/opc-ua-browser/){rel=""nofollow""}, depicted on the far right as number 6 in our architecture drawing. Now that we have laid out a concept for our application, let’s build it. ## Install Custom Nodes ::callout{icon="i-lucide-badge-check"} **Certified nodes for this technology.** This guide builds the server from community packages. Both halves of it, OPC UA and the Allen Bradley Ethernet/IP link, also have a FlowFuse certified node maintained for production use: [opcua](https://flowfuse.com/integrations/?certified=1), [cip-suite](https://flowfuse.com/integrations/?certified=1) :: First, we need to add three custom nodes that will allow Node-RED to read Ethernet/IP data and add OPC UA Server functionality. Click the hamburger icon → manage palette ![flow-manage-palette.png](https://flowfuse.com/docs/node-red/protocol/images/flow-manage-palette.png)   On the `install` tab, search for `ethernet` and install the `node-red-contrib-cip-ethernet-ip` node, which will be used to read the Ethernet/IP fieldbus data from our Allen Bradley PLC. ![install-eth-ip-node.png](https://flowfuse.com/docs/node-red/protocol/images/install-eth-ip-node.png) Next, search for `opc` and install `node-red-contrib-opcua` and `node-red-contrib-opc-ua-server`. These nodes take a particularly long time to install, as they require a lot of dependencies. Expect anywhere from 2 to 10 minutes to complete installation, depending on the speed of your system. You will not be able to track the progress of the installation unless you are monitoring the logs on the back-end, so just be patient. ![opc-nodes-install.png](https://flowfuse.com/docs/node-red/protocol/images/opc-nodes-install.png) Go to the `Nodes` tab and confirm the 3 custom nodes have been properly installed. ![custom-nodes-installed.png](https://flowfuse.com/docs/node-red/protocol/images/custom-nodes-installed.png) ## Set Up Ethernet/IP Data Note: this process is largely a recap from the first part of a article where [Node-RED is used as an Ethernet/IP to S7 protocol converter](https://flowfuse.com/blog/2023/06/node-red-as-a-no-code-ethernet_ip-to-s7-protocol-converter/). Let’s start by dragging a `eth-ip in` node onto the palette. Then add a new endpoint, which will point to our Line4 PLC. ![eth-ip-in-palette.png](https://flowfuse.com/docs/node-red/protocol/images/eth-ip-in-palette.png) In the endpoint `Connection` properties, the connection information must match the PLC, so set the IP address and CPU slot number appropriately. Also, the default cycle time is 500ms. Depending on your application, polling the CPU at 500ms may be appropriate. But for our OPC UA application, we will change it to 1000ms, which is a more appropriate polling rate for this type of application. ![ethip-node-connection.png](https://flowfuse.com/docs/node-red/protocol/images/ethip-node-connection.png) On the `Tags` tab, populate the tag information to match our Allen Bradley PLC. Then select `Update` to complete configuration of the `eth-ip endpoint`. ![eth-ip-endpoint-tags.png](https://flowfuse.com/docs/node-red/protocol/images/eth-ip-endpoint-tags.png) Now that we have our endpoint, let’s finish configuring the `eth-ip in` node. 1. select the endpoint we just created 2. Change `Mode` To `All tags` 3. Give the node a descriptive name. ![eth-ip-in-properties.png](https://flowfuse.com/docs/node-red/protocol/images/eth-ip-in-properties.png) As configured, the node is going to read all PLC tags any time a value is changed. Press done to complete the configuration. Before we deploy this flow, let’s wire a `debug` node to our `eth-ip in` node to confirm Node-RED can read the tags from our PLC. ![eth-ip-debug.png](https://flowfuse.com/docs/node-red/protocol/images/eth-ip-debug.png) Deploy the flow. ![deploy-flow.png](https://flowfuse.com/docs/node-red/protocol/images/deploy-flow.png) Click the `debug` tab and confirm data is flowing in from our PLC. ![debug-data.png](https://flowfuse.com/docs/node-red/protocol/images/debug-data.png) We can see that all tags are being read from the PLC in one message as a key/value hash table, or dict. After confirming the PLC data acquisition is working, we can remove the `debug` node and continue building the rest of our flow. Referring back to our architecture drawing, we’ve now taken care of the first 2 objectives of our application. ![PLC-Information-Model-2-of-6-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-2-of-6-1.png) \[x] Set up the PLC tags to be sent to the OPC Server :br \[x] Read the PLC tags into Node-RED :br Let’s move on to objective 3. ## Store the PLC Data In Flow Context Memory Drag a `change` node onto the palette and wire it to the `eth-ip in` node. ![change-node-palette.png](https://flowfuse.com/docs/node-red/protocol/images/change-node-palette.png) We’re going move the data from the PLC into flow context memory, by setting each element of the outgoing `msg.payload` to `flow.conveyorData`. To do this, refer back to the structure of the `msg.payload` from the `debug` node we connected to the `eth-ip in` node earlier - ![msg-payload.png](https://flowfuse.com/docs/node-red/protocol/images/msg-payload.png) Now open up the change node, and press the `+add` button to add a rule for each PLC tag in our `msg.payload` object (6), and `set` each rule so that you're setting a `flow` value to a `msg` value. Then populate each rule as shown - ![change-node-properties.png](https://flowfuse.com/docs/node-red/protocol/images/change-node-properties.png) We've now configured the `change` node to move the data from our PLC into a dict called `conveyorData`, stored in flow context memory. Give the node an appripriate name, hit done and deploy the flow. Our flow should now look like below - ![flow-with-change-palette.png](https://flowfuse.com/docs/node-red/protocol/images/flow-with-change-palette.png) Let’s look at the flow context memory to confirm the data from our PLC is being written to the `conveyorData` object we created. ![context-data-1.png](https://flowfuse.com/docs/node-red/protocol/images/context-data-1.png)![context-refresh.png](https://flowfuse.com/docs/node-red/protocol/images/context-refresh.png) Every time we hit refresh, the values in `conveyorData` change as the value in the PLC changes, confirming things are working as expected. Looking back at the application architecture we laid out, we’ve achieved 3 out of the 6 objectives. ![PLC-Information-Model-3-of-6-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-3-of-6-1.png) \[x] Set up the PLC tags to be sent to the OPC Server :br \[x] Read the PLC tags into Node-RED :br \[x] Copy the PLC tags into Node-RED context memory :br Let’s now tackle the OPC Server address space. ## Program the OPC UA Server Address Space To make our lives significantly easier, we’re going to start from a template, the same template used in [part 1 of our OPC UA Series](https://flowfuse.com/blog/2023/07/how-to-deploy-a-basic-opc-ua-server-in-node-red/). Copy the content of the [example template](https://github.com/BiancoRoyal/node-red-contrib-opcua-server/blob/master/examples/server-with-context.json){rel=""nofollow""}, then paste it into Node-RED to import it. ![import.png](https://flowfuse.com/docs/node-red/protocol/images/import.png)![import-context.png](https://flowfuse.com/docs/node-red/protocol/images/import-context.png) You end up with a new flow that looks like the one below. ![example-flow.png](https://flowfuse.com/docs/node-red/protocol/images/example-flow.png) All we care about here is the `Compact-Server` node. In fact, we’ll just copy that node and paste it into the current flow we’ve been building. Once we’ve copied the server node into our custom flow, we can discard the example flow. The whole purpose of this was to simply populate the `address space` of the `Compact-Server` node with template code that will trivialize the programming for our custom application. Our custom flow should now look something like this. ![flow-with-compact-server.png](https://flowfuse.com/docs/node-red/protocol/images/flow-with-compact-server.png) - note - I’ve added some comments to make the flow even easier to follow. Similar to commenting code, commenting flows is good practice. Open up the `Compact-Server` node and jump straight to the address space. ![compact-server-node-address-space.png](https://flowfuse.com/docs/node-red/protocol/images/compact-server-node-address-space.png) - note - we won’t go into detail on what the address space actually is in this documentation, or the details of the `Compact-Server` node, as it was covered in [part 1 of this OPC UA series](https://flowfuse.com/blog/2023/07/how-to-deploy-a-basic-opc-ua-server-in-node-red/). Please read that documentation if you are unfamiliar with it. There are 4 key things we’ll modify in the address space template code - 1. Bring in our `conveyorData` context variables 2. create our custom folder structure 3. define our context variables as OPC UA nodes 4. create custom browser views for our nodes ### Bring in Context Variables Starting from the section of code where it’s bringing in the context variables defined in the example, delete that code ```javascript this.sandboxFlowContext.set("isoInput1", 0); this.setInterval(() => { flexServerInternals.sandboxFlowContext.set( "isoInput1", Math.random() + 50.0 ); }, 500); this.sandboxFlowContext.set("isoInput2", 0); ... this.sandboxFlowContext.set("isoOutput8", 0); ``` and replace it with our `conveyorData` context variables. ```javascript this.sandboxFlowContext.set("conveyorData.Conveyor_RTS", false); this.sandboxFlowContext.set("conveyorData.Robot_RTS", false); this.sandboxFlowContext.set("conveyorData.Robot_Position", 0); this.sandboxFlowContext.set("conveyorData.Conveyor_Running", false); this.sandboxFlowContext.set("conveyorData.Line4_State", 0); this.sandboxFlowContext.set("conveyorData.Line4_Fault", false); ``` ### Create Custom Folder Structure Starting from the section of code where the example folder structure is defined, delete it and replace it with our custom folder structure defined in our architecture - ![opc-folder-structure.png](https://flowfuse.com/docs/node-red/protocol/images/opc-folder-structure.png) Delete the section of code starting from here - ```javascript const myDevice = namespace.addFolder(rootFolder.objects, { "browseName": "RaspberryPI-Zero-WLAN" }); const gpioFolder = namespace.addFolder(myDevice, { "browseName": "GPIO" }); const isoInputs = namespace.addFolder(gpioFolder, { "browseName": "Inputs" }); const isoOutputs = namespace.addFolder(gpioFolder, { "browseName": "Outputs" }); ``` and replace it with the folder structure shown above - ```javascript const myDevice = namespace.addFolder(rootFolder.objects, { "browseName": "Line 4 PLC" }); const conveyorFolder = namespace.addFolder(myDevice, { "browseName": "Conveyor" }); const conveyorBools = namespace.addFolder(conveyorFolder, { "browseName": "Bools" }); const conveyorDINTs = namespace.addFolder(conveyorFolder, { "browseName": "DINTs" }); const conveyorFloats = namespace.addFolder(conveyorFolder, { "browseName": "Floats" }); ``` ### Define OPC UA Nodes Now we can construct the nodes for each context variable. ![opc-nodes.png](https://flowfuse.com/docs/node-red/protocol/images/opc-nodes.png) Delete the section of code defining the nodes for `isoInput1` through `isoOutput8` - ```javascript const gpioDI1 = namespace.addVariable({ "organizedBy": isoInputs, "browseName": "I1", "nodeId": "ns=1;s=Isolated_Input1", "dataType": "Double", "value": { ... "set": function(variant) { flexServerInternals.sandboxFlowContext.set( "isoOutput8", parseFloat(variant.value) ); return opcua.StatusCodes.Good; } } }); ``` And replace it with our custom nodes, paying respect to the folder structure we defined in our architecture - ```javascript // Construct Nodes const Conveyor_RTS = namespace.addVariable({ "organizedBy": conveyorBools, "browseName": "Conveyor Ready to Start", "nodeId": "ns=1;s=Conveyor_RTS", "dataType": "Boolean", "value": { "get": function () { return new Variant({ "dataType": DataType.Boolean, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Conveyor_RTS") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Conveyor_RTS", variant.value ); return opcua.StatusCodes.Good; } } }); const Robot_RTS = namespace.addVariable({ "organizedBy": conveyorBools, "browseName": "Robot Ready to Start", "nodeId": "ns=1;s=Robot_RTS", "dataType": "Boolean", "value": { "get": function () { return new Variant({ "dataType": DataType.Boolean, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Robot_RTS") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Robot_RTS", variant.value ); return opcua.StatusCodes.Good; } } }); const Conveyor_Running = namespace.addVariable({ "organizedBy": conveyorBools, "browseName": "Conveyor Running", "nodeId": "ns=1;s=Conveyor_Running", "dataType": "Boolean", "value": { "get": function () { return new Variant({ "dataType": DataType.Boolean, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Conveyor_Running") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Conveyor_Running", variant.value ); return opcua.StatusCodes.Good; } } }); const Line4_Fault = namespace.addVariable({ "organizedBy": conveyorBools, "browseName": "Line 4 Faulted", "nodeId": "ns=1;s=Line4_Fault", "dataType": "Boolean", "value": { "get": function () { return new Variant({ "dataType": DataType.Boolean, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Line4_Fault") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Line4_Fault", variant.value ); return opcua.StatusCodes.Good; } } }); const Line4_State = namespace.addVariable({ "organizedBy": conveyorDINTs, "browseName": "Line 4 State", "nodeId": "ns=1;s=Line4_State", "dataType": "Int32", "value": { "get": function () { return new Variant({ "dataType": DataType.Int32, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Line4_State") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Line4_State", variant.value ); return opcua.StatusCodes.Good; } } }); const Robot_Position = namespace.addVariable({ "organizedBy": conveyorFloats, "browseName": "Robot Axis A1 Position", "nodeId": "ns=1;s=Robot_Position", "dataType": "Float", "value": { "get": function () { return new Variant({ "dataType": DataType.Float, "value": flexServerInternals.sandboxFlowContext.get("conveyorData.Robot_Position") }); }, "set": function (variant) { flexServerInternals.sandboxFlowContext.set( "conveyorData.Robot_Position", parseFloat(variant.value) ); return opcua.StatusCodes.Good; } } }); ``` ### Define Browser Views Last, let’s create some custom views. Delete the code starting from - ```javascript //------------------------------------------------------------------------------ // Add a view //------------------------------------------------------------------------------ const viewDI = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "RPIW0-Digital-Ins" }); ... viewDO.addReference({ "referenceType": "Organizes", "nodeId": gpioDO8.nodeId }); ``` And replace with a custom view of your choice. I’ve defined a view that splits the bools, DINTs, and Reals. ```javascript const viewBools = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "Line 4 Conveyor Bools" }); const viewDINTs = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "Line4 Conveyor DINTs" }); const viewFloats = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "Line4 Conveyor Floats" }); viewBools.addReference({ "referenceType": "Organizes", "nodeId": Conveyor_RTS.nodeId }); viewBools.addReference({ "referenceType": "Organizes", "nodeId": Robot_RTS.nodeId }); viewBools.addReference({ "referenceType": "Organizes", "nodeId": Conveyor_Running.nodeId }); viewBools.addReference({ "referenceType": "Organizes", "nodeId": Line4_Fault.nodeId }); viewDINTs.addReference({ "referenceType": "Organizes", "nodeId": Line4_State.nodeId }); viewFloats.addReference({ "referenceType": "Organizes", "nodeId": Robot_Position.nodeId }); ``` We’ve now completed the address space, so all that’s left is to define the OPC UA endpoint. ## Wrap Up Server Configuration Go to the discovery tab and define an endpoint following the convention below, with the ip address matching the ip address of your Node-RED instance. ![image-20230718-155245.png](https://flowfuse.com/docs/node-red/protocol/images/image-20230718-155245.png) Now, our OPC UA Server is set up and ready to be browsable by an OPC UA Client. Deploy the changes and make sure the `Compact-Server` is reporting `active`. ![compact-server-active.png](https://flowfuse.com/docs/node-red/protocol/images/compact-server-active.png) If not, go back and check your code for errors. The Node-RED logfiles will come in handy to track down issues if your server isn’t activating. This wraps up the 4th objective of our application. ![PLC-Information-Model-4-of-6-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-4-of-6-1.png) \[x] Set up the PLC tags to be sent to the OPC Server :br \[x] Read the PLC tags into Node-RED :br \[x] Copy the PLC tags into Node-RED context memory :br \[x] Program the OPC Server address space :br ## Security (Optional) At this point, our OPC UA Server will accept a client connection, but it won’t be secure, so we should take the extra step and encrypt our OPC UA traffic. To do this, go to the `Security` tab of the `Compact-Server` properties. ![security-tab-default.png](https://flowfuse.com/docs/node-red/protocol/images/security-tab-default.png) By default, the server is using no security, and allowing anonymous connections. Let’s fix that by unchecking `Allow Anonymous` , and checking `Use invididual Certificate Files`. ![individual-cert-file-option.png](https://flowfuse.com/docs/node-red/protocol/images/individual-cert-file-option.png) The node gives us some clues on how we can populate this section. When `node-red-contrib-opcua-server` was installed, it created self-signed ssl certificates that are bound to our host system. Let’s make use of them. navigate to `./node-red-contrib-opcua-server/certificates` directory, where the node-red instance has installed the Node-RED module. - I have Node-RED installed in the root path of my server, so my full path to the certs folder is `/root/.node-red/node_modules/node-red-contrib-opcua-server/certificates` - If you’re having trouble finding the directory, do a search for the file `server_selfsigned_cert_2048.pem`. Once you’ve navigated to the correct directory, it should be full of various cert files. ![cert-list.png](https://flowfuse.com/docs/node-red/protocol/images/cert-list.png) The two cert files we care about, which were already pre-defined in the node, are `server_selfsigned_cert_2048.pem`, which is the public cert file, and `server_key_2048.pem`, which is the private cert file. Go back to the node configuration and populate the `Security` tab with the *full absolute path* to the files. ![cert-tab-filled.png](https://flowfuse.com/docs/node-red/protocol/images/cert-tab-filled.png) Hit done and redeploy the node. Make sure the server reports `active`. If not, check the cert paths and try again. You may also run into file permission issues, depending on how you set up your Node-RED instance, so make sure Node-RED also has read access to the files. We’re not done yet. The server is happy, but the OPC Client will need access to these cert files as well. So copy the files to a location that will make the two cert files accessible to the OPC Client. In my case, the OPC Client is being ran on a personal Windows machine on the same network. So I copied the cert files to a nas, which both my Node-RED instance and my Windows machine have access to. ![copy-certs.png](https://flowfuse.com/docs/node-red/protocol/images/copy-certs.png)![copied-certs.png](https://flowfuse.com/docs/node-red/protocol/images/copied-certs.png) Now we can move on to OPC Client Configuration. We’ve achieved 5 out of 6 objectives. ![PLC-Information-Model-5-of-6-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-5-of-6-1.png) \[x] Set up the PLC tags to be sent to the OPC Server :br \[x] Read the PLC tags into Node-RED :br \[x] Copy the PLC tags into Node-RED context memory :br \[x] Program the OPC Server address space :br \[x] Encrypt the OPC Server with SSL :br ## OPC UA Client Configuration and Testing To connect to our Node-RED OPC server, enter the endpoint url and press “connect to server”. ![opc-client-connect.png](https://flowfuse.com/docs/node-red/protocol/images/opc-client-connect.png) Security settings are displayed. We’re going to select `Sign & Encrypt` and change the security policy to `Aes128Sha256RsaOaep` ![sign\&encrypt.png](https://flowfuse.com/docs/node-red/protocol/images/sign\&encrypt.png) When we try to connect, our connection to the server is rejected, because we haven’t pointed the client to our ssl cert files. Press Okay to acknowledge the error and we can fix that problem. ![connect-rejected.png](https://flowfuse.com/docs/node-red/protocol/images/connect-rejected.png) When you acknowledge the connection error, you are taken to the `User Authentication` properties. Select `Certificate and Private key`. We need to point to our certificate and private key files. ![client-cert-path.png](https://flowfuse.com/docs/node-red/protocol/images/client-cert-path.png) When we browse for our certificate file, the client software tells us it’s expecting a `*.der` file, which we don’t have yet. ![der-file.png](https://flowfuse.com/docs/node-red/protocol/images/der-file.png) However, we can create one from our existing cert file using `openssl`. If you don’t already have openssl installed, [install it](https://www.openssl.org/){rel=""nofollow""}. Then from a command prompt, run the following command in the directory where your client-side cert files are stored - ```text openssl x509 -in server_selfsigned_cert_2048.pem -out server_selfsigned_cert_2048.der -outform DER ``` ![ssl-pub-keygen.png](https://flowfuse.com/docs/node-red/protocol/images/ssl-pub-keygen.png) This command will generate the .der file the opc client is expecting to see. ![copied-certs-with-pubkey.png](https://flowfuse.com/docs/node-red/protocol/images/copied-certs-with-pubkey.png) Now we can go back and point to the public key file, which is the `server_selfsigned_cert_2048.der` file, and the private key file, which is the `server_key_2048.pem` file. ![client-cert-path-filled.png](https://flowfuse.com/docs/node-red/protocol/images/client-cert-path-filled.png) The first time you do this, you will be asked to accept the server certificate. ![opc-client-accept-cert.png](https://flowfuse.com/docs/node-red/protocol/images/opc-client-accept-cert.png) If you choose accept permanently, you won’t see this prompt again. You should now have access to browse the OPC Server. As can be seen, our OPC Client sees the data from our PLC matching our OPC Information Model we defined in our Node-RED server address space. ![image-20230718-164326.png](https://flowfuse.com/docs/node-red/protocol/images/image-20230718-164326.png) We’ve now achieved all of our design objectives. ![PLC-Information-Model-6-of-6-1.png](https://flowfuse.com/docs/node-red/protocol/images/PLC-Information-Model-6-of-6-1.png) \[x] Set up the PLC tags to be sent to the OPC Server :br \[x] Read the PLC tags into Node-RED :br \[x] Copy the PLC tags into Node-RED context memory :br \[x] Program the OPC Server address space :br \[x] Encrypt the OPC Server with SSL :br \[x] Set up the OPC Client :br Our custom OPC UA application is complete and production-ready. Test the application by confirming values changed in the PLC are reflected in the OPC UA Client. In my PLC code, I created a sine wave generator that changes the `Robot Axis A1 Position` value continuously, so the value is always changing, making it easy to confirm that the server is passing OPC traffic correctly. ![sine-wave-gen.png](https://flowfuse.com/docs/node-red/protocol/images/sine-wave-gen.png) In this documentation, we covered in detail how to create an OPC UA application that pulls data from an Allen Bradley PLC over Ethernet/IP, store the PLC data in Node-RED context memory, then publish the PLC data from context memory onto a ssl secured OPC UA Server. An OPC Client can subscribe to the OPC UA Server over an encrypted connection, making the application deployable in a production environment, including in the cloud if desired. With the foundation laid in this documentation, you can customize the application to fit your individual needs, with an understanding of what is going on “under the hood” of an OPC UA Server. This application only scratches the surface of what features OPC UA has available. Refer to the [NodeOPCUA sdk](https://node-opcua.github.io/){rel=""nofollow""} and experiment by building on top of this example if you desire to learn more or want to add features this application is lacking. In the next documentation of the OPC UA series, we will establish how to create an OPC UA Client application in Node-RED. OPC UA is one of several industrial protocols FlowFuse uses to connect PLCs to the modern stack. For EtherNet/IP, Siemens S7, Modbus, MQTT, and more, see the [FlowFuse PLC integration overview](https://flowfuse.com/landing/plc/). Source code for flow used in this documentation - ::render-flow ```json [{"id":"2e8c7f5c.ab73d","type":"tab","label":"OPC-UA Custom Context Server","disabled":false,"info":""},{"id":"38ce10de.7d8c","type":"opcua-compact-server","z":"2e8c7f5c.ab73d","port":"54845","endpoint":"","productUri":"","acceptExternalCommands":true,"maxAllowedSessionNumber":"10","maxConnectionsPerEndpoint":"10","maxAllowedSubscriptionNumber":"100","alternateHostname":"","name":"","showStatusActivities":false,"showErrors":true,"allowAnonymous":false,"individualCerts":true,"isAuditing":false,"serverDiscovery":true,"users":[],"xmlsetsOPCUA":[],"publicCertificateFile":"/root/.node-red/node_modules/node-red-contrib-opcua-server/certificates/server_selfsigned_cert_2048.pem","privateCertificateFile":"/root/.node-red/node_modules/node-red-contrib-opcua-server/certificates/server_key_2048.pem","registerServerMethod":"1","discoveryServerEndpointUrl":"opc.tcp://192.168.0.114:54845","capabilitiesForMDNS":"","maxNodesPerRead":1000,"maxNodesPerWrite":1000,"maxNodesPerHistoryReadData":100,"maxNodesPerBrowse":3000,"maxBrowseContinuationPoints":"10","maxHistoryContinuationPoints":"10","delayToInit":"1000","delayToClose":"200","serverShutdownTimeout":"100","addressSpaceScript":"function constructAlarmAddressSpace(server, addressSpace, eventObjects, done) {\n // server = the created node-opcua server\n // addressSpace = address space of the node-opcua server\n // eventObjects = add event variables here to hold them in memory from this script\n\n // internal sandbox objects are:\n // node = the compact server node,\n // coreServer = core compact server object for debug and access to NodeOPCUA\n // this.sandboxNodeContext = node context node-red\n // this.sandboxFlowContext = flow context node-red\n // this.sandboxGlobalContext = global context node-red\n // this.sandboxEnv = env variables\n // timeout and interval functions as expected from nodejs\n\n const opcua = coreServer.choreCompact.opcua;\n const LocalizedText = opcua.LocalizedText;\n const namespace = addressSpace.getOwnNamespace();\n\n const Variant = opcua.Variant;\n const DataType = opcua.DataType;\n const DataValue = opcua.DataValue;\n\n var flexServerInternals = this;\n\n this.sandboxFlowContext.set(\"conveyorData.Conveyor_RTS\", false);\n this.sandboxFlowContext.set(\"conveyorData.Robot_RTS\", false);\n this.sandboxFlowContext.set(\"conveyorData.Robot_Position\", 0);\n this.sandboxFlowContext.set(\"conveyorData.Conveyor_Running\", false);\n this.sandboxFlowContext.set(\"conveyorData.Line4_State\", 0);\n this.sandboxFlowContext.set(\"conveyorData.Line4_Fault\", false);\n\n // this.sandboxFlowContext.set(\"isoInput1\", 0);\n // this.setInterval(() => {\n // flexServerInternals.sandboxFlowContext.set(\n // \"isoInput1\",\n // Math.random() + 50.0\n // );\n // }, 500);\n // this.sandboxFlowContext.set(\"isoInput2\", 0);\n // this.sandboxFlowContext.set(\"isoInput3\", 0);\n // this.sandboxFlowContext.set(\"isoInput4\", 0);\n // this.sandboxFlowContext.set(\"isoInput5\", 0);\n // this.sandboxFlowContext.set(\"isoInput6\", 0);\n // this.sandboxFlowContext.set(\"isoInput7\", 0);\n // this.sandboxFlowContext.set(\"isoInput8\", 0);\n // this.sandboxFlowContext.set(\"isoOutput1\", 0);\n // this.setInterval(() => {\n // flexServerInternals.sandboxFlowContext.set(\n // \"isoOutput1\",\n // Math.random() + 10.0\n // );\n // }, 500);\n\n // this.sandboxFlowContext.set(\"isoOutput2\", 0);\n // this.sandboxFlowContext.set(\"isoOutput3\", 0);\n // this.sandboxFlowContext.set(\"isoOutput4\", 0);\n // this.sandboxFlowContext.set(\"isoOutput5\", 0);\n // this.sandboxFlowContext.set(\"isoOutput6\", 0);\n // this.sandboxFlowContext.set(\"isoOutput7\", 0);\n // this.sandboxFlowContext.set(\"isoOutput8\", 0);\n\n coreServer.debugLog(\"init dynamic address space\");\n const rootFolder = addressSpace.findNode(\"RootFolder\");\n\n node.warn(\"construct new address space for OPC UA\");\n\n const myDevice = namespace.addFolder(rootFolder.objects, {\n \"browseName\": \"Line 4 PLC\"\n });\n const conveyorFolder = namespace.addFolder(myDevice, { \"browseName\": \"Conveyor\" });\n const conveyorBools = namespace.addFolder(conveyorFolder, {\n \"browseName\": \"Bools\"\n });\n const conveyorDINTs = namespace.addFolder(conveyorFolder, {\n \"browseName\": \"DINTs\"\n });\n const conveyorFloats = namespace.addFolder(conveyorFolder, {\n \"browseName\": \"Floats\"\n });\n\n // Construct Nodes\n const Conveyor_RTS = namespace.addVariable({\n \"organizedBy\": conveyorBools,\n \"browseName\": \"Conveyor Ready to Start\",\n \"nodeId\": \"ns=1;s=Conveyor_RTS\",\n \"dataType\": \"Boolean\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Boolean,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Conveyor_RTS\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Conveyor_RTS\",\n variant.value\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n const Robot_RTS = namespace.addVariable({\n \"organizedBy\": conveyorBools,\n \"browseName\": \"Robot Ready to Start\",\n \"nodeId\": \"ns=1;s=Robot_RTS\",\n \"dataType\": \"Boolean\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Boolean,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Robot_RTS\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Robot_RTS\",\n variant.value\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n const Conveyor_Running = namespace.addVariable({\n \"organizedBy\": conveyorBools,\n \"browseName\": \"Conveyor Running\",\n \"nodeId\": \"ns=1;s=Conveyor_Running\",\n \"dataType\": \"Boolean\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Boolean,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Conveyor_Running\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Conveyor_Running\",\n variant.value\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n const Line4_Fault = namespace.addVariable({\n \"organizedBy\": conveyorBools,\n \"browseName\": \"Line 4 Faulted\",\n \"nodeId\": \"ns=1;s=Line4_Fault\",\n \"dataType\": \"Boolean\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Boolean,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Line4_Fault\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Line4_Fault\",\n variant.value\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n const Line4_State = namespace.addVariable({\n \"organizedBy\": conveyorDINTs,\n \"browseName\": \"Line 4 State\",\n \"nodeId\": \"ns=1;s=Line4_State\",\n \"dataType\": \"Int32\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Int32,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Line4_State\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Line4_State\",\n variant.value\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n const Robot_Position = namespace.addVariable({\n \"organizedBy\": conveyorFloats,\n \"browseName\": \"Robot Axis A1 Position\",\n \"nodeId\": \"ns=1;s=Robot_Position\",\n \"dataType\": \"Float\",\n \"value\": {\n \"get\": function () {\n return new Variant({\n \"dataType\": DataType.Float,\n \"value\": flexServerInternals.sandboxFlowContext.get(\"conveyorData.Robot_Position\")\n });\n },\n \"set\": function (variant) {\n flexServerInternals.sandboxFlowContext.set(\n \"conveyorData.Robot_Position\",\n parseFloat(variant.value)\n );\n return opcua.StatusCodes.Good;\n }\n }\n });\n\n //------------------------------------------------------------------------------\n // Add a view\n //------------------------------------------------------------------------------\n const viewBools = namespace.addView({\n \"organizedBy\": rootFolder.views,\n \"browseName\": \"Line 4 Conveyor Bools\"\n });\n\n const viewDINTs = namespace.addView({\n \"organizedBy\": rootFolder.views,\n \"browseName\": \"Line4 Conveyor DINTs\"\n });\n\n const viewFloats = namespace.addView({\n \"organizedBy\": rootFolder.views,\n \"browseName\": \"Line4 Conveyor Floats\"\n });\n\n viewBools.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Conveyor_RTS.nodeId\n });\n\n viewBools.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Robot_RTS.nodeId\n });\n\n viewBools.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Conveyor_Running.nodeId\n });\n\n viewBools.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Line4_Fault.nodeId\n });\n\n\n viewDINTs.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Line4_State.nodeId\n });\n\n viewFloats.addReference({\n \"referenceType\": \"Organizes\",\n \"nodeId\": Robot_Position.nodeId\n });\n coreServer.debugLog(\"create dynamic address space done\");\n node.warn(\"construction of new address space for OPC UA done\");\n\n done();\n}\n","x":960,"y":600,"wires":[]},{"id":"7ae89f134415c51e","type":"eth-ip in","z":"2e8c7f5c.ab73d","endpoint":"f012042b75173b77","mode":"all","variable":"","program":"","name":"Read Line4 Conveyor tags","x":150,"y":600,"wires":[["0c51f44baa08a3b2"]]},{"id":"a0592280baea975b","type":"comment","z":"2e8c7f5c.ab73d","name":"read data from PLC & store in conveyorData context flow memory","info":"","x":350,"y":540,"wires":[]},{"id":"b411a5ce4749fa61","type":"comment","z":"2e8c7f5c.ab73d","name":"Secure OPC UA Server Publishing PLC conveyorData","info":"","x":980,"y":540,"wires":[]},{"id":"0c51f44baa08a3b2","type":"change","z":"2e8c7f5c.ab73d","name":"store PLC Data in flow context memory","rules":[{"t":"set","p":"conveyorData.Conveyor_RTS","pt":"flow","to":"payload.Conveyor_RTS","tot":"msg"},{"t":"set","p":"conveyorData.Conveyor_Running","pt":"flow","to":"payload.Conveyor_Running","tot":"msg"},{"t":"set","p":"conveyorData.Line4_Fault","pt":"flow","to":"payload.Line4_Fault","tot":"msg"},{"t":"set","p":"conveyorData.Line4_State","pt":"flow","to":"payload.Line4_State","tot":"msg"},{"t":"set","p":"conveyorData.Robot_Position","pt":"flow","to":"payload.Robot_Position","tot":"msg"},{"t":"set","p":"conveyorData.Robot_RTS","pt":"flow","to":"payload.Robot_RTS","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":600,"wires":[[]]},{"id":"f012042b75173b77","type":"eth-ip endpoint","address":"192.168.0.5","slot":"0","cycletime":"1000","name":"Line 4 PLC","vartable":{"":{"Conveyor_RTS":{"type":"BOOL"},"Conveyor_Running":{"type":"BOOL"},"Line4_Fault":{"type":"BOOL"},"Line4_State":{"type":"DINT"},"Robot_Position":{"type":"REAL"},"Robot_RTS":{"type":"BOOL"}}}}] ``` :: # Using Websocket with Node-RED This guide covers WebSocket communication in Node-RED. You'll learn how to connect as a client and set up your own WebSocket server. ## What is Websocket? WebSocket enables real-time, bidirectional communication between clients (web browsers, IoT devices, etc.) and servers. Think of it as keeping a phone line open instead of hanging up and calling back for every message. Traditional HTTP works like sending letters back and forth - each request needs its own connection. WebSocket keeps a single connection alive, letting both sides send messages whenever they need to. This makes it ideal for applications that need instant data exchange. ## How Does WebSocket Work? ![WebSocket connection establishment process](https://flowfuse.com/docs/node-red/protocol/images/websocket-handshake.png){dataZoomable=""}*WebSocket connection establishment process* WebSocket establishes a persistent connection between a client (such as a web browser or IoT device) and a server. The process begins when the client sends an initial HTTP request to the server, indicating a desire to upgrade to a WebSocket connection. If the server supports WebSocket, it responds with a confirmation, and the connection is established. Once the connection is open, both the client and server can send messages to each other at any time. This two-way communication allows for real-time data exchange without the need to re-establish connections for each message. When the communication is no longer needed, either party can close the connection. ## Using Websocket with Node-RED This document will explain how to communicate as both a server and a client using WebSocket in Node-RED. Practical examples will be provided to help you follow along, ensuring a comprehensive understanding of the concepts. ## Understanding Servers and Clients in WebSocket Context In the context of WebSocket communication: - **WebSocket Server:** The WebSocket server is a application (such as a web browser or IoT device) that listens for incoming WebSocket connection requests from clients. It manages these connections and facilitates the exchange of messages in real time. The server can handle multiple clients simultaneously, allowing for concurrent communication. - **WebSocket Client:** The WebSocket client is the application (such as a web browser or IoT device) that initiates the connection to the WebSocket server. It sends requests to establish a WebSocket connection and can send and receive messages at any time once the connection is active. ## Building a WebSocket Server in Node-RED and Communicating with Clients Before creating the server, it's important to understand that we will need to listen for incoming messages as well as send messages. Let’s first create the server to listen for connections. ### Building the WebSocket Server to Listen 1. Drag the **WebSocket In** node onto the canvas. 2. Double-click the node and select the type as "Listen On." 3. Click on the "+" icon to add the WebSocket configuration. 4. Enter the path you want to listen on as the server (e.g., `/ws/listen`). 5. Choose whether you want to send and receive the entire message or only `msg.payload`. 6. Drag a **Debug** node onto the canvas and connect it to the WebSocket In node. This will allow you to see incoming messages in the debug window. **Note**: By default, the payload will contain the data sent over or received from the WebSocket. If configured to send the entire message, the object will be in JSON string format. ### Making the Server Able to Send Data to Clients 1. Drag the **Inject** node onto the canvas. This node will be used to send example data, but you can use any other data source node that you wish. 2. Drag the **WebSocket Out** node onto the canvas. Double-click it and set the type to "Listen On." 3. In the URL field, select the configuration you added while building the server in the previous section. 4. Click the **Deploy** button to activate your flow. ### Testing the WebSocket Server :video{ariaLabel="Testing Server with Client" autoPlay="true" height="832" loop="true" muted="true" playsInline="true" preload="none" width="1920"}*Testing the WebSocket server with a Websocket client* Now that you have deployed the Node-RED flow, it is acting as a server that can both send and receive data. To test the server, you can use the [Simple WebSocket Client](https://chromewebstore.google.com/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=en){rel=""nofollow""} extension in your browser. Make sure to install this extension if you want to test the server. 1. In the extension interface, enter the URL to connect to your server: - For a deployed server: `wss:///ws/listen` - For a local server: `ws://localhost:1880/ws/listen` (Note: Use `ws` for unencrypted connections and `wss` for encrypted connections.) 2. Click the **Open** button to connect to the server. Once connected, you should see the status change to "opened." 3. In the request field, enter the message you want to send to the server and click **Send**. You will see the message printed in the debug window of Node-RED. 4. To send a message from websocket server created in Node-RED to the client, click the **Inject** button with `msg.payload` set to the data you want to send. In the extension interface, you will see the sent data in the message log. With these steps, you will have successfully set up a WebSocket server in Node-RED and tested its functionality as both a server and client. This setup allows for real-time communication, which is essential for many applications. ### Sending Data from Node-RED To send a message from Node-RED to the client, click the **Inject** button with `msg.payload` set to the data you want to send. In the extension interface, you will see the sent data in the message log. ## Connecting to a WebSocket Server as a client in Node-RED and communicating Now, as the section states, we are going to see how you can connect to the WebSocket server as the client. To move further, ensure that you have a server to connect to, which you can create as shown in the above section in Node-RED. ### Connecting to the WebSocket Server for Incoming Messages 1. Drag the **WebSocket In** node onto the canvas. 2. Double-click on it and select the type as "Connect To." 3. Click on the "+" icon to add the WebSocket configuration. 4. Enter the WebSocket server URL you want to connect to. 5. Select 'payload' in the Send/Receive. 6. Drag the **Debug** node onto the canvas and connect it to the WebSocket In node. ### Making the Client Able to Send Messages 1. Drag the **Inject** node onto the canvas. This node will be used to send example data. 2. Drag the **WebSocket Out** node onto the canvas. Double-click it and set the type to "Connect To." 3. In the URL field, select the configuration you added while connecting to the server for listening to incoming messages. 4. Click the **Deploy** button. ### Testing the WebSocket Client :video{ariaLabel="Testing Client with Server" autoPlay="true" height="832" loop="true" muted="true" playsInline="true" preload="none" width="1920"}*Testing Websocket Client with Websocket Server* Now, to test the client, you can send messages from the server and see the debug window for that message in the client instance. Similarly, you can send messages from the client Instance to the server and observe the responses in the debug window of server instance. For more information on the advaced websocket node configuration refer to the [Websocket Node Documentation](https://flowfuse.com/docs/node-red/core-nodes/network/websocket/) # Node-RED Terminology Here are the key terms and concepts used within the Node-RED community to ensure clarity and consistency when communicating across projects. ## Flow A flow is represented as a tab within the editor workspace which provides a new workspace for building applications by connecting nodes. The term "flow" is also used to informally describe a single set of connected nodes. So a flow (tab) can contain multiple flows (sets of connected nodes), but formally we can say a flow is a parent group of multiple connected nodes. ![Image displaying flow tab](https://flowfuse.com/docs/node-red/terminology/images/editor-flow-tabs.png "Image displaying flow tab"){dataZoomable=""} ## Subflow A subflow in Node-RED is a collection of nodes that are collapsed into a single node in the workspace. It allows you to group a set of nodes together into a reusable unit. This helps in organizing flows, promoting reusability, and simplifying complex flow designs by encapsulating multiple nodes into a single, higher-level node representation. ![An image displaying two sections: one showing the nodes selected to create a subflow, and the second showing the subflow created for those selected nodes](https://flowfuse.com/docs/node-red/terminology/images/node-red-subflow.png "An image displaying two sections: one showing the nodes selected to create a subflow, and the second showing the subflow created for those selected nodes"){dataZoomable=""} ## Workspace The workspace is where flows (groups of nodes) are developed by dragging nodes from the palette and wiring them together. Adding a new flow tab gives you a new workspace. ![Image displaying workspace](https://flowfuse.com/docs/node-red/terminology/images/editor-workspace.png "Image displaying workspace"){dataZoomable=""} ## Node A Node is a fundamental building block used to create flows. Each node represents a distinct piece of functionality or a specific action that can be performed within a flow. These nodes can be third-party additions using the palette manager or core nodes. ![Image displaying node](https://flowfuse.com/docs/node-red/terminology/images/node-red-node.png "Image displaying node"){dataZoomable=""} ## Core-Node The core nodes are the set of nodes that are included with the Node-RED runtime by default, without the need for a node installation procedure. For more information on core nodes, refer to the [Core node docs](https://flowfuse.com/docs/node-red/core-nodes/). ## Wires The "wires" refer to the connections that link nodes together to define the flow of data. These wires visually represent the direction and flow of information from one node to another within a Node-RED flow. ![Image displaying node's wire](https://flowfuse.com/docs/node-red/terminology/images/node-wire.png "Image displaying node's wire"){dataZoomable=""} ## Input and Output ports Nodes in Node-RED have input and output ports represented by small circles on the left (input) and right (output) sides of the node. These ports indicate where data enters or exits the node. This allows you to connect different nodes via wires. ![Image displaying node's input and ouput port](https://flowfuse.com/docs/node-red/terminology/images/node-input-ouput-port.png "Image displaying node's input and ouput port"){dataZoomable=""} ## Message A message is essentially a JavaScript object that carries data between nodes within a flow. This message contains both the main data payload and additional metadata, allowing nodes to communicate and process information effectively. ![Simple message object printed on debug panel by debug node](https://flowfuse.com/docs/node-red/terminology/images/node-red-message-object.png "Simple message object printed on debug panel by debug node"){dataZoomable=""} ## Payload The primary property within a message. This is the default property that most nodes will work with. This property holds the main data that nodes in the flow process and manipulate. ## Context Context refers to a storage mechanism that allows nodes to store data between invocations. It provides a way for nodes to share data within the same instance or flow, across different flows in Node-RED. For more information on Context, refer to the [Understanding Node, Flow, Global, and Environment Variables in Node-RED](https://flowfuse.com/blog/2024/05/understanding-node-flow-global-environment-variables-in-node-red/). ## Function Node The Function node in Node-RED allows you to write custom JavaScript functions to process and manipulate messages within your flows. ## Node Palette The palette is a sidebar containing all of the nodes that are installed and available to use. ![Image displaying Node-RED Palette](https://flowfuse.com/docs/node-red/terminology/images/node-palette.png "Image displaying Node-RED Palette"){dataZoomable=""} ## Palette Manager The Palette Manager in Node-RED is a tool that allows users to manage the nodes available for use in their Node-RED instance. It provides a graphical interface for searching, installing third-party nodes using Node Package Manager from the palette. Additionally, it shows all installed nodes and allows users to update and uninstall them if needed. Installed nodes get automatically added to the Node Palette for easy access and use in flows. ![Image displaying Node-RED Palette Manager](https://flowfuse.com/docs/node-red/terminology/images/node-red-palette-manager.png "Image displaying Node-RED Palette Manager"){dataZoomable=""} ## Node Package Manager (npm) `npm` is a command-line tool used to manage additional nodes and their dependencies in Node-RED. It allows users to install, update, and remove nodes that are contributed or added by Node-RED community members. It's provided by Node.JS, that's the runtime for Node-RED. ## Editor The editor in Node-RED is the graphical interface where you create and manage flows. It includes all the components: workspace, palette for nodes, tabs for organizing flows, and a sidebar for configuration and deployment. ![Image displaying Node-RED Editor](https://flowfuse.com/docs/node-red/terminology/images/node-red-editor.png "Image displaying Node-RED Editor"){dataZoomable=""} ## Instance In Node-RED, an instance refers to a running environment of the Node-RED runtime, which handles flows and node interactions. ## Node-RED Dashboard Node-RED Dashboard is a collection of nodes and UI components that allow you to create web-based dashboards in Node-RED. It provides widgets like buttons, charts, gauges, and text boxes to display and interact with data from your flows in real-time, for more information refer to the [Node-RED Dashboard 2.0 official documentation](https://dashboard.flowfuse.com/){rel=""nofollow""}. ## Deploying Flow Deploying a flow in Node-RED means making your flow changes active in the runtime environment. It's done by clicking the "Deploy" button in the editor, activating nodes to process data or execute tasks defined in the flow. ![Image displaying Node-RED Deploy button](https://flowfuse.com/docs/node-red/terminology/images/node-red-editor-deploy-button.png "Image displaying Node-RED Deploy button"){dataZoomable=""} ## Importing, Exporting, Grouping Flows Flows can be imported, exported, and grouped in Node-RED, which allows you to share your flows with others or import flows created by others. Flows are exported in JSON format and commonly named `flow.json`. Grouping flows allows you to organize related flows together for better management and sharing. This feature facilitates collaboration and sharing of Node-RED projects among the community. For more information, refer to [Importing, Exporting, and Grouping Flows](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/). # Customer Support Premium customers can get support by [filing a ticket](https://flowfuse.com/support). We offer support for the FlowFuse application and your account, any issues relating to Node-RED such as your flows or a 3rd party node should be raised in the [community forum](https://community.FlowFuse.com){rel=""nofollow""}. # Quick Start Guide This guide provides a streamlined process for setting up and running the FlowFuse platform using [Docker](https://docs.docker.com/get-started/){rel=""nofollow""} and [Docker Compose](https://docs.docker.com/compose/){rel=""nofollow""}. The Docker Compose file deploys the following services: - **FlowFuse Platform**: Includes the core application, MQTT broker, and file server for storage - **Database:** A pre-configured database for storing platform data - **Proxy Server:** A pre-configured proxy server for managing HTTP traffic For a full installation guide, including how to setup FlowFuse in a production environment, please refer to the dedicated page for [running FlowFuse on Docker](https://flowfuse.com/docs/install/docker). ## Prerequisites Before you begin, ensure you have [Docker](https://docs.docker.com/engine/install/){rel=""nofollow""} and [Docker Compose](https://docs.docker.com/compose/install/){rel=""nofollow""} (in `2.23.1` version or higher) installed on your system (either as a standalone binary or as a Docker plugin). ## Step 1: Prepare your domain FlowFuse requires a domain name to work properly — it uses subdomains to run each Node-RED instance separately, so `localhost` won't work here. If you own a domain (e.g., `example.com` or `flowfuse.example.com`), create an **A record** pointing to your server's IP address, and another **A record** for the wildcard subdomain (e.g., `*.example.com` or `*.flowfuse.example.com`) pointing to the same address. That's all the setup needed. If you don't have a domain yet and just want to try FlowFuse locally, see [setting up an alternative to DNS](https://flowfuse.com/docs/install/dns-setup#no-local-dns-server). ## Step 2: Download files ```bash curl -L -o docker-compose.yml https://github.com/FlowFuse/docker-compose/releases/latest/download/docker-compose.yml curl -L -o .env https://raw.githubusercontent.com/FlowFuse/docker-compose/refs/heads/main/.env.example ``` ## Step 3: Provide domain name Edit the downloaded `.env` file with the editor of your choice and update the `DOMAIN` variable with your domain. You can use `sed` to update the `DOMAIN` variable in the `.env` file: ```bash sed -i.bak 's/^DOMAIN=.*/DOMAIN=example.com/' .env ``` ## Step 4: Start the Application Run the following command to deploy FlowFuse: ```bash docker compose up -d ``` This downloads the necessary Docker images, runs initial setup, and starts all services in detached mode. ## Step 5: Complete the application Setup Open your web browser and navigate to `http://forge./setup` (e.g., `http://forge.example.com/setup`). You will be redirected to the setup page where you can create your admin account and set up your instance. For detailed information about first setup and configuration, follow [this guide](https://flowfuse.com/docs/install/first-run). ## Cleanup To stop and remove the FlowFuse application, run the following command: ```bash docker compose down -v ``` ## Troubleshooting If you encounter any issues, please check the following: 1. Ensure all prerequisites are correctly installed 2. Verify your DNS settings are correct and have propagated 3. Check the Docker logs for any error messages: ```bash docker compose logs ``` For more detailed information or advanced configuration options for running FlowFuse on Docker, please refer to our [full documentation](https://flowfuse.com/docs/install/docker). # Upgrading FlowFuse If you are upgrading an existing FlowFuse installation, this page will list any particular requirements needed to upgrade to a given level. If you are upgrading across multiple versions, make sure you check the requirements for each version you are upgrading across. Note that we do not support downgrading FlowFuse to previous levels once an upgrade has been performed. ## General guideline Details of how to upgrade can be found for each deployment model: - [LocalFS](https://flowfuse.com/docs/contribute/local#upgrade) - [Docker](https://flowfuse.com/docs/install/docker#upgrade) - [Kubernetes](https://flowfuse.com/docs/install/kubernetes#upgrade) To upgrade the version of Node-RED your Instances run (for example, moving to Node-RED 5.0), see [Upgrading the Node-RED version](https://flowfuse.com/docs/upgrade/nodered-version). ### Upgrading to 2.31.2 #### Kubernetes: MQTT broker is now EMQX From Helm chart v2.78.0 (FlowFuse 2.31.2), the chart deploys [EMQX](https://www.emqx.io/){rel=""nofollow""} as the platform MQTT broker, replacing Mosquitto. The migration happens automatically as part of upgrading the chart; it is not possible to stay on Mosquitto when using the Helm chart. If the broker is enabled, the [EMQX Operator](https://docs.emqx.com/en/emqx-operator/latest/getting-started/getting-started.html){rel=""nofollow""} must be installed on the cluster before upgrading, otherwise the upgrade will fail. See [MQTT Broker configuration](https://flowfuse.com/docs/install/configuration#mqtt-broker-configuration) for which platform features depend on the broker. ### Upgrading to 2.6.0 #### Required AWS EKS configuration change This release introduces the new Embedded Editor which integrates the Node-RED editor with the FlowFuse dashboard when using Node-RED 4.0. This has required some changes to be made on how certain HTTP headers are passed between the NGINX Ingress controller and AWS NLB. The following configuration change must be applied otherwise users will not be able to login to Node-RED 4.0 instances. The following configuration needs to be added in the values passed to the ingress-nginx helm chart. See [full configuration](https://flowfuse.com/docs/install/kubernetes/aws/#nginx-ingress){rel=""nofollow""} for the reference. ```text controller: config: use-proxy-protocol: true service: annotations: service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: proxy_protocol_v2.enabled=true externalTrafficPolicy: Cluster ``` The Proxy Protocol feature will be enabled only on newly created Target Groups. To enable the Proxy Protocol on an existing Target Group, manual intervention is required. For detailed instructions, please refer to the [official AWS documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#enable-proxy-protocol){rel=""nofollow""}. #### Persistent Storage As part of this release there is a new option for Persistent File Storage for Kubernetes based deployments. This change removes the need to use the customised File Nodes and the FlowFuse File Server by mounting a Persistent Volume into the Pods running the instances. To enable this feature the following needs to be created - A Kubernetes StorageClass that points to storage provider that can dynamically provision new Persistent Volumes. e.g. the [AWS EFS CSI driver](https://github.com/kubernetes-sigs/aws-efs-csi-driver){rel=""nofollow""} - Pass the following values to the FlowFuse Helm Chart ```text forge: persistentStorage: enabled: true storageClass: '' size: '5Gi' ``` Where size is the default size for the volume. Details for how to setup a AWS EFS backed StorageClass can be found on the aws-efs-csi-driver [site](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/efs-create-filesystem.md){rel=""nofollow""}. ### Upgrading to 2.0.0 > **⚠️** Breaking changes introduced! Together with new application features, this **release 2.0.0 introduces breaking changes** in Flowfuse Helm chart. If you are managing your local Flowfuse instance using our [Helm Chart](https://github.com/FlowFuse/helm/tree/main/helm/flowfuse){rel=""nofollow""}, please refer to the [upgrade](https://flowfuse.com/docs/install/kubernetes#upgrade) section of the Kubernetes installation guide or the Helm Chart [README.md](https://github.com/FlowFuse/helm/blob/main/helm/flowfuse/README.md#upgrading-chart){rel=""nofollow""} for more details. ### Upgrading to 1.10 Endpoint Rate Limiting is now available to FlowFuse. This is disabled by default, but can be enabled by setting the `rate_limits.enabled` config setting to `true`. The documentation for this is available [here](https://flowfuse.com/docs/install/configuration#rate-limiting-configuration). The [TeamType concept](https://flowfuse.com/docs/user/concepts#team-type) was expanded in this release. It is used to control what Instance Types are available for different teams, as well as any additional limits that should be applied. When creating new Instance Types, they must now be [manually enabled](https://flowfuse.com/docs/admin/introduction#managing-instance-types) for the Team Types on the platform. ### Upgrading to 1.5 The main change in this release was a change in our terminology around the individual Node-RED instances. We have introduced the [Application concept](https://flowfuse.com/docs/user/concepts#application) as a way to group individual [Node-RED instances](https://flowfuse.com/docs/user/concepts#instance) (what we previously called Projects). The term 'Project' is being phased out. You may still see it crop up, such as in some of the external APIs, but we're working our way through removing it. ### Upgrading to 1.3 To enable the Team Library and FlowFuse-based Authentication of HTTP routes each Node-RED instance will need to be updated to the [latest Stack](https://flowfuse.com/docs/user/changestack). #### Persistent Context added The new Persistent Context feature is available to projects when running with a [premium license](https://flowfuse.com/docs/upgrade/open-source-to-premium). This feature requires additional configuration to be added to the File Server component that was introduced in FlowFuse 1.1. Details of how to configure this can be found at the following links: - [LocalFS](https://flowfuse.com/docs/install/file-storage#localfs) - [Docker and Kubernetes](https://flowfuse.com/docs/install/file-storage#configuring) ### Upgrading to 1.1 #### File Server added This release introduces a system for supporting persistent file storage when running on Docker or Kubernetes (it will also work with LocalFS, but is not required as projects have access to the hosts filesystem). Details of how to configure this can be found at the following links: - [LocalFS](https://flowfuse.com/docs/install/file-storage#localfs) - [Docker and Kubernetes](https://flowfuse.com/docs/install/file-storage#configuring) ### Upgrading to 0.8 #### MQTT Broker added This release introduces an MQTT Broker into the FlowFuse platform used to communicate between devices and the core platform. For LocalFS users, they will need to manually setup the broker and ensure it is properly configured. The documentation for this is available [here](https://flowfuse.com/docs/contribute/local#setting-up-mosquitto-optional) #### LocalFS Users With the 0.8 release we have updated the version of the SQLite3 module used by the localfs container driver. We are moving from v5.0.2 to v5.0.8. There appears to be a clash with the bcrypt module when doing an in place upgrade of the SQLite3 module that gives an error similar to the following: ```bash npm ERR! path /opt/share/projects/flowforge/sqlite-test/node_modules/sqlite3 npm ERR! command failed npm ERR! command sh -c node-pre-gyp install --fallback-to-build npm ERR! sh: line 1: node-pre-gyp: command not found ``` If you see this then the simplest fix is to remove the `node_modules` directory and reinstall the modules. #### Project Nodes This release adds support for the new Project Link nodes that can be used to send messages between projects seamlessly. These nodes require the MQTT Broker to be properly configured. To deploy flows using these nodes to a Device will require the Device to be running the latest 0.2.0 release. They will also need to have their credentials regenerated once the MQTT Broker has been added. ### Upgrading to 0.7 The 0.7 release introduces the [ProjectType concept](https://flowfuse.com/docs/user/concepts#instance-type). After upgrading to 0.7, an administrator must perform the following tasks before users will be able to create new projects: 1. Create a Project Type. 1. On the Administrator Settings -> Project Types page, click 'Create project type'. 2. Provide a name and description. If you have billing enabled, copy in the default Stripe Product/Price IDs from your runtime settings file. 3. Click 'create' 2. Assign your existing stacks to that type 1. On the Administrator Settings -> Stacks page, edit each existing stack via the drop-down menu in the table. 2. As a one-time action, set its Project Type to the one just created. 3. Click 'save'. This will update the stack *and* all existing projects to be associated with the new Project Type # Upgrading the Node-RED version A new major version of Node-RED - such as Node-RED 5.0 - does **not** arrive through an in-editor or in-platform "update" button. The version of Node-RED an Instance runs is defined by its [Stack](https://flowfuse.com/docs/user/concepts#stack). To move to a new major version you add it as a new Stack, then point your Instances at it. This page uses the move to **Node-RED 5.0** as an example, but the same steps apply to any major Node-RED version upgrade. ## Why there's no update button The notification and one-click upgrade you may have seen applies to new *versions of an existing Stack* (see [Managing Stacks](https://flowfuse.com/docs/admin/introduction#managing-stacks)). Crossing a major Node-RED version is different: you create a new Stack that pins the new container, rather than upgrading the existing Stack in place. This keeps the two versions side by side so you can move Instances over at your own pace. It also ensures major version upgrades are not automatically applied by the Scheduled Maintenance feature. ## Before you start - You will need access to **Admin Settings**. - Add the new Node-RED version as a **distinct new Stack** rather than editing your existing Stack. Keeping them separate means your current Instances stay on their existing version until you choose to move them, and you can switch back from an Instance's settings if needed. ## Create the new Stack 1. Go to **Admin Settings → Stacks**. 2. Click **Create Stack**. 3. Set the **Container Location** to the image for the new Node-RED version. The exact value depends on your deployment model - see the deployment-specific guides below. For Node-RED 5.0 on Docker or Kubernetes this is `flowfuse/node-red:2.31.2-5.0.x`; the full list of pre-built tags is on [Docker Hub](https://hub.docker.com/r/flowfuse/node-red/tags){rel=""nofollow""}. (tags are of the form [FlowFuse Version] - [Node-RED Version] ) 4. Save the Stack. It is now available when you create or migrate Instances. For the details of how the Container Location is specified for each deployment model, see: - [Local Stacks](https://flowfuse.com/docs/contribute/local/stacks) - [Docker Stacks](https://flowfuse.com/docs/install/docker/stacks) - [Kubernetes Stacks](https://flowfuse.com/docs/install/kubernetes/stacks) ## Use the new Stack ### For a new Instance Select the new Stack when you create the Instance. It starts on the new Node-RED version right away. ### For an existing Instance Move the Instance onto the new Stack by following [Changing the Stack](https://flowfuse.com/docs/user/changestack): open the Instance's **Settings** tab and use **Change Instance Stack** to select the new Stack. The Instance restarts on the new Stack. # Upgrading to FlowFuse Enterprise For self-managed FlowFuse installations without a license you can unlock more features with a enterprise license. As an admin a license can be uploaded to FlowFuse in the admin panel, under the settings tab. When a license is uploaded a restart of the `forge` app is required. After the forge application has restarted, the Node-RED runtimes need to be updated to leverage these features. As restarting Node-RED might need to be coordinated, FlowFuse will not automatically restart all instances. ## Reusing FlowFuse licenses A single license may only be applied to one FlowFuse platform at any time. Running multiple FlowFuse platforms with the same license key is against the terms of the subscription. A license may be reused on another FlowFuse platform if the original platform is no longer running. For example, if the FlowFuse platform is reinstalled onto new hardware, the license from the original install can be reused. # Bill of Materials The Application Bill of Materials (BoM) provides a comprehensive overview of all instance dependencies for each application. This feature gives you visibility and control to monitor, manage, and analyze the components your applications rely on. ## Accessing the Bill of Materials The BoM is located in the **Dependencies** tab of each application. ![bom.png](https://flowfuse.com/docs/user/images/bom.png){dataZoomable=""} ## Search Functionality Use the integrated search to efficiently find devices and instances assigned to your application. Search by: - Instance or device name - Package name - Specific dependency - Dependency version ## Prerequisites ### FlowFuse Cloud - Instance Stack with launcher version **2.9.0 or higher** - **Enterprise Team Type** required ### Self-Hosted - Available only with **Enterprise license** # Changing the Stack [Stacks](https://flowfuse.com/docs/user/concepts#stack) define various aspects of how Node-RED instances run - including the version of Node-RED being used. FlowFuse allows you to change the stack an instance is using - providing a way to upgrade Node-RED. **Note:** Stacks are created by Administrators and made available to the teams and users of the platform. When an Administrator creates a new version of a Stack your instance is using, the platform will notify you that there is a new version available. To change an instance's stack: 1. Go to the instance's page and select the **Settings** tab. 2. Click the **Change Instance Stack** button. 3. You will be prompted to select the new stack. 4. Click **Change Stack** Your instance will now be restarted on the new stack. **Note:** Changing the stack causes Node-RED to be stopped and restarted. This will require a short downtime of the flows. # FlowFuse Concepts FlowFuse makes it easy to create, manage, and scale Node-RED instances. The platform introduces a few core concepts to help you organize and work with it effectively. Throughout the platform, you'll also see ⓘ icons that you can click on to get pop-up explanations for different features and terms. ## Table of Contents - [FlowFuse Concepts](https://flowfuse.com/#flowfuse-concepts) - [Table of Contents](https://flowfuse.com/#table-of-contents) - [Team](https://flowfuse.com/#team) - [Team Type](https://flowfuse.com/#team-type) - [Application](https://flowfuse.com/#application) - [DevOps Pipeline](https://flowfuse.com/#devops-pipeline) - [Instance](https://flowfuse.com/#instance) - [Hosted Instance](https://flowfuse.com/#hosted-instance) - [Remote Instance](https://flowfuse.com/#remote-instance) - [Instance Configuration](https://flowfuse.com/#instance-configuration) - [Instance Type](https://flowfuse.com/#instance-type) - [Stack](https://flowfuse.com/#stack) - [Blueprint](https://flowfuse.com/#blueprint) - [Template](https://flowfuse.com/#template) - [Snapshot](https://flowfuse.com/#snapshot) - [Hosted Instance Snapshot](https://flowfuse.com/#hosted-instance-snapshot) - [Remote Instance Snapshot](https://flowfuse.com/#remote-instance-snapshot) - [Assigning Remote Instances to Hosted Instances](https://flowfuse.com/#assigning-remote-instances-to-hosted-instances) - [How It Works](https://flowfuse.com/#how-it-works) - [Assignment Rules](https://flowfuse.com/#assignment-rules) - [When to Use Instance Assignment vs DevOps Pipelines](https://flowfuse.com/#when-to-use-instance-assignment-vs-devops-pipelines) - [Device](https://flowfuse.com/#device) - [Device Agent](https://flowfuse.com/#device-agent) - [Fleet Mode vs Developer Mode](https://flowfuse.com/#fleet-mode-vs-developer-mode) - [Provisioning Tokens](https://flowfuse.com/#provisioning-tokens) - [Device Groups](https://flowfuse.com/#device-groups) ## Team Teams are how FlowFuse organizes users on the platform. Each team can have multiple members and each user can be a member of multiple teams. The users in a team can have different roles that determine what they are [able to do](https://flowfuse.com/docs/user/team#role-based-access-control). In FlowFuse Cloud, each team has its own billing plan, managed via Stripe. ### Team Type The platform can be configured to provide different types of teams. These can be used to apply limits on what teams of a given type can do. For example, a particular team type may be restricted to certain types of [Node-RED instances](https://flowfuse.com/#instance), or how many members the team can have. ## Application **Introduced in FlowFuse 1.5** To organize your Node-RED instances, they are grouped within Applications. With the 1.5 release, each Application has a single Node-RED instance. With the 1.6 release, an application can have multiple Node-RED instances. Applications provide logical organization of related instances, support for DevOps pipeline workflows, simplified device group management, application-level audit logging, and clear organizational boundaries for managing multiple instances and devices. With FlowFuse's Granular RBAC, Applications can now also act as an authorization boundary. This means user roles and permissions can be managed at the application level, providing finer control over access to instances, snapshots, and devices within a given application. ### DevOps Pipeline **Introduced in FlowFuse 1.8** DevOps Pipelines allow you to manage staged development environments, pushing from your Development instances to Production once you have stable and well-tested flows. You can find out how to implement DevOps Pipelines [here](https://flowfuse.com/docs/user/devops-pipelines). ## Instance **This was called a Project before FlowFuse 1.5** Within your Application, you can have one or more instances of Node-RED. FlowFuse supports two types of instances based on where they run. | Feature | Hosted Instance | Remote Instance (Device) | | ------------- | --------------------------------------------------------- | ------------------------------------------------ | | Where it runs | On FlowFuse-managed infrastructure (cloud or self-hosted) | On user-provided hardware (Edge, VM, Pi, PLC) | | Provisioning | Automatic via the FlowFuse UI | Manual installation of the FlowFuse Device Agent | | Lifecycle | Fully managed by FlowFuse | Managed by FlowFuse via the Device Agent | | Common Use | Core logic, APIs, dashboards, development, testing | Local data processing, IO, edge control | ### Hosted Instance A **Hosted Instance** is a Node-RED environment that runs within the FlowFuse infrastructure, whether in the cloud or on a self-hosted FlowFuse platform. These instances run on FlowFuse-managed infrastructure and benefit from automatic scaling and high availability. They are accessed via HTTPS with TLS/SSL encryption, making them ideal for centralized data processing and transformation, dashboard hosting, cloud service integration, and development or testing environments. When running in Docker or Kubernetes (such as FlowFuse Cloud), the instance names are used as the hostname to access the instance. This means that the names must be DNS safe (made up of a-z, 0-9 and -) and not start with a number. Currently, it is not possible to change an instance name after it has been created. ### Remote Instance A **Remote Instance** refers to a Node-RED environment that is managed by FlowFuse but runs on external infrastructure—be it a PLC, Gateway, local PC, or even a server in a different network. Remote instances used to be referred to as Devices within the FlowFuse platform, and in some parts of the platform this terminology is still used. See the [Device](https://flowfuse.com/#device) section for more details. These instances run on user-provided infrastructure such as edge devices, on-premises servers, or industrial equipment while being managed centrally through the FlowFuse platform. They are ideal for edge computing, local data processing, and environments with limited connectivity. Remote instances require FlowFuse Device Agent installation to connect to the platform. ### Instance Configuration Both hosted and remote instances are customized versions of Node-RED that include various FlowFuse plugins to integrate with the platform. A number of the standard Node-RED settings are exposed for customization, and they can be preset by applying a Template when creating the instance. When an instance is being created, the user can select a blueprint to use. ### Instance Type When you create a Node-RED instance, you can pick its type from the list the platform Administrator has made available. For example, each type could provide a different amount of memory or CPU allocation. If the platform has billing enabled, each type may have a different monthly price associated with it. ### Stack A Stack describes the properties of the Node-RED runtime. This can include the version of Node-RED, memory, and CPU allocation. The specific details will depend on how and where FlowFuse is running. Stacks are created and owned by the platform Administrator. When a User comes to create a new Node-RED instance, they choose from the available stacks associated with the chosen Instance Type. The stack determines the Node-RED version, memory, and CPU usage for your instance, ensuring compatibility and optimal performance for your use case. For details on how to administer and manage Stacks, please see the [Administering FlowFuse](https://flowfuse.com/docs/admin/introduction#managing-stacks) docs. ### Blueprint Blueprints are pre-built Node-RED instances designed for industrial applications. They include ready-to-use flows along with all node configurations and the required nodes, as well as environment settings, allowing the instance to be deployed with minimal modification. ### Template A Template describes the properties of Node-RED itself. It is how many of the settings a user familiar with Node-RED would be used to modifying in their settings file. But it can also be used to customize the palette of nodes that are pre-installed, provide a set of default flows and change the look and feel of the editor. A template can also specify [Environment Variables](https://flowfuse.com/docs/user/envvar) which can then have their values customized for each Node-RED instance, or have their values locked to prevent any changes. In the current release of FlowFuse, Templates are created by the Administrator. As well as defining the values, they also get to choose whether instances can override any of the settings for themselves. ### Snapshot Snapshots are point-in-time backups that work differently depending on the instance type. #### Hosted Instance Snapshot A snapshot is a point-in-time backup of a hosted Node-RED instance. It captures the flows, credentials, and runtime settings. Snapshots can be created and deleted on the FlowFuse Platform, or using the [FlowFuse Node-RED Tools plugin](https://flowfuse.com/docs/migration/node-red-tools). The platform also allows you to roll an instance back to a previous snapshot. A user can create an hosted instance snapshot and then mark it as the *target* snapshot for remote instance. The platform will then deploy that snapshot to all of the devices assigned to the instance. Snapshots can be set as targets for DevOps pipeline stages. Auto snapshots are available for automatic backup scheduling. If an auto snapshot is set as a target or assigned to a pipeline stage, it will not be automatically cleaned up, so you may have more than 10 auto snapshots in that case. #### Remote Instance Snapshot Similar to instance snapshots, a device snapshot is a point-in-time backup of a Node-RED instance running on a remote device. It captures the flows, credentials, and runtime settings. The difference is that local changes made on a device during developer mode are pulled into the FlowFuse platform and stored as a snapshot. The dashboard also allows you to see and manage these snapshots. With devices assigned to an application, a user can create a device snapshot from the remote device. ### Assigning Remote Instances to Hosted Instances **Note:** This is a legacy feature that predates DevOps Pipelines. For new deployments, consider using [DevOps Pipelines](https://flowfuse.com/#devops-pipeline) instead, which provide more flexible and powerful deployment workflows. Instance assignment establishes a parent-child deployment relationship between a hosted instance and remote instances. When assigned, the hosted instance becomes the source of truth and automatically pushes snapshots to the remote instance. You can only push nodes and flows that are supported by both Hosted and Remote Instances. For more information on how this works, see [How Instance Assignment Works](https://flowfuse.com/docs/user/concepts#how-it-works). #### How It Works Instance assignment creates a parent-child deployment relationship with the following behavior: - **Hosted Instance (Parent)**: Acts as the deployment controller and source of truth for snapshots - **Remote Instance (Child)**: Receives and applies snapshot updates from the parent - **Deployment Flow**: When you set a [Target Snapshot](https://flowfuse.com/docs/user/snapshots/#instance-owned-devices) on the hosted instance, all assigned remote instances automatically restart on that snapshot - **One-Way Relationship**: Changes flow exclusively from the hosted instance to remote instances, ensuring consistent deployments across your fleet #### Assignment Rules FlowFuse enforces specific constraints on instance assignment: - Remote instances can be assigned to hosted instances - Remote instances cannot be assigned to other remote instances - Hosted instances cannot be assigned to any other instances #### When to Use Instance Assignment vs DevOps Pipelines Instance assignment suits straightforward scenarios where you need to push snapshots from a single hosted instance directly to one or more remote instances. It works well for maintaining existing deployments or when you simply need one hosted instance deploying to multiple remote instances without staging environments. DevOps Pipelines are recommended for most deployment workflows. They support staged environments for testing changes in development before promoting to production, enable deployment to device groups for easier fleet management, and offer more flexibility aligned with modern development practices. If you're setting up a new deployment process, use DevOps Pipelines. ## Device The FlowFuse platform can be used to manage Node-RED applications running on remote devices. A Device is essentially a **Remote Instance** that runs a software agent to connect back to the platform and receive updates. Users must [install the agent](https://flowfuse.com/docs/device-agent/install/overview) on the devices. Devices are registered to a Team and then assigned to an individual Node-RED instance or a FlowFuse application within that team. A user can create an [instance snapshot](https://flowfuse.com/#hosted-instance-snapshot) and then mark it as the *target* snapshot for devices. The platform will then deploy that snapshot to all of the devices assigned to the instance. With devices assigned to an application, a user can create a [device snapshot](https://flowfuse.com/#remote-instance-snapshot) from the remote device. ### Device Agent The FlowFuse Device Agent is the software that connects your hardware to FlowFuse and manages how Node-RED runs on it. It receives snapshots from FlowFuse, installs the required Node-RED version and nodes, and ensures the device always runs the assigned snapshot. It maintains a secure connection so FlowFuse can monitor the device and send updates remotely. Read more about the [Device Agent](https://flowfuse.com/docs/device-agent/introduction). ### Fleet Mode vs Developer Mode **Fleet Mode** When in this mode, the device runs only the assigned target snapshot. It automatically updates whenever the target snapshot changes, and the Node-RED editor is disabled to prevent any local modifications. This keeps all devices consistent across the fleet. **Developer Mode** When in this mode, the device ignores the target snapshot and stops receiving updates. A secure tunnel opens so you can access the Node-RED editor and make changes directly on the hardware. When you exit Developer Mode, all local edits are discarded and the device reloads the current target snapshot. ### Provisioning Tokens Provisioning tokens can be created to allow Remote Instances to automatically join a team and to be auto assigned to an application or an instance if required. ## Device Groups **Introduced in FlowFuse 1.15** Device groups allow you to organize your devices into logical groups. These groups can be the target of [DevOps Pipelines](https://flowfuse.com/#devops-pipeline), greatly simplifying the deployments to one or hundreds of devices. Device groups provide logical organization of devices by location, function, environment, or other criteria. They enable simplified mass deployments through DevOps pipelines, allow you to target specific device groups for staged rollouts, and let you manage subsets of devices independently. Read more [about Device Groups](https://flowfuse.com/docs/user/device-groups). # Custom Hostnames FlowFuse allows you to point a custom subdomain at your instances, such as `dashboard.example.com`. This feature is available to: - FlowFuse Cloud Enterprise teams - Self-hosted FlowFuse Enterprise, running version 2.5 or later with the kubernetes driver ## Configuring a custom hostname The Custom Hostname option is available under the General Settings tab of your Instance. Currently, FlowFuse only supports configuring a subdomain such as `dashboard.example.com`. Top level domains cannot be used. 1. Enter your custom subdomain and click save. 2. You will be shown a dialog to confirm the change, as this will require restarting your Instance to apply. It also provides information on how to configure your DNS provider. 3. Using your DNS provider, create a `CNAME` record for your chosen subdomain that points at the endpoint provided in the dialog. **Note**: it is important to configure the Instance's Custom Hostname *before* you make the DNS changes. Making the DNS changes without configuring your Instance may allow someone else to configure their instance to use your subdomain. For FlowFuse Cloud, the `CNAME` should point to `custom-loadbalancer.flowfuse.com`. For self-hosted users, use the information provided in the dialog. If you also use CAA DNS entries to control which Certificate Authorities can issue certificates for your domains, you will need to add a record allowing LetsEncrypt to issue certificate. Please see details [here](https://letsencrypt.org/docs/caa/){rel=""nofollow""}. The platform will issue certificates using the `http-01` validation method. # Custom Node Packages FlowFuse has access to the wide range of Node-RED nodes listed in the [public catalogue](https://flows.nodered.org){rel=""nofollow""}. But occasionally there will be the need for a custom node for a situation that is specific to your Team. If you decide to [develop](https://nodered.org/docs/creating-nodes/){rel=""nofollow""} your own nodes, you will need somewhere to host both the node and a Node-RED catalogue file. FlowFuse has two solutions for this: 1. FlowFuse Hosted Nodes - Use the private NPM registry hosted by FlowFuse to store and manage your custom npm packages. 2. Third-Party NPM Registries - If you already have a private npm registry, you can enable access to these in your Instance's settings. ## FlowFuse Hosted Nodes If you want to create a Node-RED node for private use by Instances in your FlowFuse Team then you can publish them to the FlowFuse Custom Node Registry (available to Enterprise level teams on FlowFuse Cloud). ### Publishing Nodes After developing your node you can publish it to your Teams Custom Nodes registry with the following steps #### Authenticating Before publishing to the registry you need to authenticate. This step should only need to be done once. The credentials can be found by navigating to the "Custom Nodes" tab under the Team Library and clicking on the "Publish" button. ![Publish Custom Package](https://flowfuse.com/docs/user/images/publish-custom-package.png){dataZoomable=""}{style="max-width: 600px;"} *Screenshot fo the "Publish Custom Package" dialog shown in the FlowFuse UI* ```text npm login --registry=https://registry.flowfuse.cloud ``` #### Packaging There are steps required to ensure your node is correctly packaged for the FlowFuse Custom Nodes registry 1. Make sure the package name contains the correct scope prefix e.g. `@flowfuse-[team id]/node-name`. The correct prefix will be shown on the in the FlowFuse application 2. Add a `publishConfig` section with a `registry` entry e.g. for a Team with ID `6Rag1kQj4k` ```json { "name": "@flowfuse-6Rag1kQj4k/bar", "version": "0.0.1", "description": "...", "publishConfig": { "registry": "https://registry.flowfuse.cloud" }, ... } ``` #### Publishing In the same directory as the `package.json` file run the following command ```text npm publish ``` Once published you should see the Node listed in the "Custom Nodes" section of the Team Library. ![Screenshot of the "Custom Nodes" view in the Team Library](https://flowfuse.com/docs/user/images/custom-node-library.png){dataZoomable=""}{style="max-width: 850px;"} *Screenshot of the "Custom Nodes" view in the Team Library* ### Installing Nodes Any packages uploaded to the Team Library will be published to your Instances under a custom catalogue with the name "FlowFuse Team [team name] Catalogue" ![Node-RED Custom Catalogue](https://flowfuse.com/docs/user/images/custom-catalogue.png){dataZoomable=""}{style="max-width: 600px;"} *Screenshot of the contents of a FlowFUse catalogue appearing in the "install" tab of the Node-RED Palette Manager* ## 3rd Party NPM Registries or Private npmjs.org packages The following features are available to Enterprise users of FlowFuse Cloud. ### NPM Registries If you have already published packages to an existing NPM Registry then you can enable access to this by adding the required values to a \`.npmrc" file in the Instance Settings. This can include authentication tokens to access private packages. ![.npmrc file](https://flowfuse.com/docs/user/images/instance-settings-npmrc.png){dataZoomable=""}{style="max-width: 600px;"} *Screenshot from the FlowFuse platform, showing the input for defining an .npmrc file* ### Node-RED Catalogues In order to be able to install packages in the Node-RED editor they need to in a Node-RED Catalogue file that is loaded from a HTTPS URL. You can supply a list of Catalogue URLs in the Instance Settings. ![Node Catalogues](https://flowfuse.com/docs/user/images/instance-settings-catalogues.png){dataZoomable=""}{style="max-width: 600px;"} *Screenshot of the listed Node Catalogues configured on an Instance* # Dashboards Any hosted instance running the [FlowFuse Dashboard](https://dashboard.flowfuse.com){rel=""nofollow""} can be viewed directly inside FlowFuse, without opening a separate browser tab. *This does not apply to Remote Instances (devices) - Dashboards are only listed and viewable here for hosted instances.* ## Team Dashboards Click **Dashboards** in the Team navigation to see every hosted instance in the Team that has a Dashboard installed, regardless of which Application it belongs to. ![The Team Dashboards page listing every instance with a Dashboard installed, across all Applications](https://flowfuse.com/docs/user/images/dashboard-team.png){dataZoomable=""} The list shows each instance's name, status, the Application it belongs to, and when its flows were last updated. Click a row to open that instance's Dashboard. Members with the **Dashboard Only** role land on this page, scoped to only the instances they have access to. ## Application Dashboards Each Application has its own **Dashboards** tab, showing only the Dashboards belonging to instances within that Application. ![The Dashboards tab on an Application page, listing Dashboards for instances in that Application](https://flowfuse.com/docs/user/images/application-dashboard.png){dataZoomable=""} ## Instance Dashboard Tab An individual hosted instance's page has a **Dashboard** tab, for viewing its Dashboard without leaving the instance's context. ![The Dashboard tab on an individual instance's page](https://flowfuse.com/docs/user/images/dashboard-instance-tab.png){dataZoomable=""} ## Viewing a Dashboard Opening a Dashboard displays it embedded inside FlowFuse. A drawer alongside it lists every other Dashboard available in the current scope (Team or Application), with a search box to find one by name. Click any entry in the drawer to switch to that Dashboard directly. ![The embedded Dashboard view with the switcher drawer open, listing other available Dashboards](https://flowfuse.com/docs/user/images/dashboard-drawer.png){dataZoomable=""} Use the icon in the top-right of the drawer to collapse or reopen it. To open a Dashboard in its own browser tab instead, use the **Dashboard** button's dropdown arrow on an instance's page and select **Open Direct URL**. ## Requirements - The instance must be **running** for its Dashboard to be viewable. - The instance must have the [FlowFuse Dashboard](https://dashboard.flowfuse.com){rel=""nofollow""} node installed and configured in its flows. Instances without a Dashboard configured do not appear in these lists. # Groups **Navigation**: Team > Application > Groups ## Overview ![Groups UI](https://flowfuse.com/docs/user/images/groups.png){dataZoomable=""} Groups help you organize and manage multiple devices that run the same [snapshot](https://flowfuse.com/docs/user/snapshots) configuration. By grouping devices logically, you can deploy updates to dozens or even hundreds of devices simultaneously through [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines). ### Key Features **Automatic Updates** - Devices added to an active group automatically receive the current pipeline snapshot - Devices removed from an active group have their snapshot cleared **Group-Level Environment Variables** - Set environment variables at the group level that apply to all member devices - Device-specific variables take precedence over group variables - Variables are merged at runtime without modifying device settings - Updates to group variables trigger automatic device restarts ### Requirements - FlowFuse 1.15+ (Enterprise Tier) - FlowFuse Cloud (Enterprise Tier) - FlowFuse 2.10+ for Group Environment Variables ## Creating a Group ![Create Group](https://flowfuse.com/docs/user/images/create-device-group.png){dataZoomable=""} 1. Navigate to your Application 2. Select the **Groups** tab 3. Click **Add Group** 4. Enter a descriptive name for your group 5. (Optional) Add a description to help distinguish between groups 6. Click **Create** ## Managing Group Membership ### Adding and Removing Devices ![Edit Group Members](https://flowfuse.com/docs/user/images/ui-device-group-member-edit.png){dataZoomable=""} 1. Click the group you want to modify from the table 2. Click **Edit** 3. Review the two lists: - **Available Devices** (left): Devices assigned to your application that can be added - **Group** (right): Devices currently in the group 4. To add devices: Check the boxes next to devices in the Available list, then click **Add Devices** 5. To remove devices: Check the boxes next to devices in the Group list, then click **Remove Devices** 6. Click **Save** 7. Review the confirmation prompt and click **Confirm** to apply changes **Note**: If a device doesn't appear in the Available list, it's likely already assigned to another group. ### How Snapshots Are Affected **When Adding a Device** If the group has an active pipeline snapshot, newly added devices will automatically be updated to that snapshot. **When Removing a Device** If the group has an active pipeline snapshot and the device is currently running it, removing the device will clear its snapshot, effectively resetting it to a blank state. **Clearing Group Snapshots** You can remove the target snapshot from a group in the group settings. This will also clear the snapshot from all devices in the group. ## Environment Variables Group-level environment variables follow these rules: - **Precedence**: Device variables override group variables - **Scope**: Variables are merged at runtime only; device settings remain unchanged - **Removal**: Removing a device from a group removes the group's variables from that device - **Updates**: Changing group variables triggers a restart of all devices in the group # DevOps Pipelines **Navigation**: `Team > Application > Pipelines` ![Overview of a DevOps Pipeline in FlowFuse](https://flowfuse.com/docs/user/images/ui-devops-pipelines.png){dataZoomable=""} In FlowFuse it is possible to configure a DevOps pipeline for your Node-RED instances. DevOps Pipelines allow you to easily deploy from one instance to another, most commonly used for having an unstable/experimental "Development" instance, and a more stable "Production" instance. The pipeline then allows you to move your full flow and configuration along from "Development" to "Production" once it's ready. You can configure this in FlowFuse from the Application screen. Note you will need to have created any Instances you wish to include in the Pipeline before being able to add them to a Pipeline. ## Creating a Pipeline 1. Select the Application you want to configure a Pipeline for. 2. Ensure an instance is created for each stage you plan to create, e.g. development, QA, and production. 1. For the instance you want to duplicate go to the **Settings** tab 2. Click **Duplicate Instance** and provide the necessary details 3. Select the "DevOps Pipelines" tab 4. Select "Add Pipeline" 5. Name your pipeline appropriately (this can be changed later) 6. Select "Add Stage" 7. Define your Stage's name, select the [Stage Type](https://flowfuse.com/#stage-types) and select an [Action](https://flowfuse.com/#actions). 8. Click "Add Stage" 9. Repeat 5. - 7. for as many stages as you need. See [Pipeline Stage details](https://flowfuse.com/#pipeline-stage-details) below for more info. ## Running a Pipeline Stage ![Running a pipeline stage by clicking the play icon on the source stage](https://flowfuse.com/docs/user/images/ui-devops-run.png){dataZoomable=""} Each stage currently is deployed manually. To do so, click the "play" icon on the source stage. In the example above, it will push from the "Development" stage to the "Production" stage. **Environment Variables** - When pushing to a next stage, ***only your environment variable keys will be copied over***. Values must be set on the next Stage's Instance explicitly. **Instance Settings** - None of your Instance Settings will be copied over (e.g. Editor, Palette or Security Settings). This ensures a split between your staging environments. ## Pipeline Stage details - Stages of a pipeline are executed from left to right. - Actionable stages have a play button that will push from that stage to the next stage. - Every stage, except the last one, is effectively a source stage that can be pulled *from*. - Every stage, except the first one, is a target stage that can be pushed *to*. - You cannot currently insert a Stage into the middle of a Pipeline, only at the end. ### Stage Types There are four types of stage to chose from: 1. **[Instance](https://flowfuse.com/docs/user/concepts#hosted-instance)** - a single Node-RED instance. 2. **[Device](https://flowfuse.com/docs/user/concepts#remote-instance)** - a single remote instance. 3. **[Device Group](https://flowfuse.com/docs/user/concepts#device-groups)** - a group of remote instances. 4. **Git Repository**- a remote Git repository. - This stage supports GitHub, Azure DevOps, and any other Git server accessible over HTTPS (for example GitLab, Bitbucket, Gitea, or a self-hosted instance). ### Actions ![Selecting an action for a pipeline stage](https://flowfuse.com/docs/user/images/ui-devops-select-action.png){dataZoomable=""} The action defines what happens when the stage is deployed. The available actions depend on the stage type selected for the stage. These are listed below. #### Instance stage actions - **Create new instance snapshot** - A new snapshot of the instance will be created and pushed to the next stage. - **Use latest instance snapshot** - The latest existing snapshot of the instance will be pushed to the next stage. - **Prompt to select instance snapshot** - You will be prompted to choose which snapshot to push to the next stage. #### Device stage action - **Use active snapshot** - The active snapshot of the device will be pushed to the next stage. - **Use latest device snapshot** - The latest snapshot of the device will be pushed to the next stage. - **Prompt to select device snapshot** - You will be prompted to choose which snapshot to push to the next stage. #### Device Group stage When a Device Group stage is triggered, it will push the current active snapshot of the group to the next stage. #### Git Repository stage Git Repository stages can be used to push and pull snapshots to and from a remote Git repository. FlowFuse supports GitHub, Azure DevOps, and any other Git server that is accessible over HTTPS, for example GitLab, Bitbucket, Gitea, or a self-hosted instance. > This feature is available to Enterprise tier teams on FlowFuse Cloud, and to Enterprise licensed self-hosted installations from version 2.32 onwards. Support for generic HTTPS Git servers (anything other than GitHub and Azure DevOps) requires version 2.32 or later. ##### Adding a Git token Before you can create a Git Repository stage, you need to add a token that grants FlowFuse access to your repository. Tokens are managed per team, under `Team > Team Settings > Integrations`. 1. Go to **Team Settings** and open the **Integrations** tab. 2. In the Git integration section, click **Add Token**. 3. In the **Add Git Personal Access Token** popup, choose the tab that matches your Git server (**GitHub**, **Azure DevOps**, or **Other**) and follow the instructions for that provider (below). 4. Give the token a **Name**, complete the required fields, and click **Add**. ![Add Git Personal Access Token popup with tabs for GitHub, Azure DevOps, and Other providers](https://flowfuse.com/docs/user/images/generic-git-provider.png){dataZoomable=""} ###### GitHub 1. Open GitHub Personal Access Tokens Settings. 2. Click on **Generate a new token**. 3. Select the **Only select repositories** option and pick which repositories to grant access to. 4. Expand the **Repository permissions** section and ensure the **Contents** option is set to **Read and write**. 5. Click on **Generate token**. 6. This will be the only time GitHub shows you the token value. Copy the token into the **Token** field. ###### Azure DevOps 1. Open `https://dev.azure.com/[org-name]/_usersSettings/tokens` to create a new Personal Access Token. 2. Click the **+ New Token** button. 3. Give the token a name and set the **Expiry** date. 4. Select **Custom defined** for the Scopes. 5. Check **Read & Write** in the **Code** section. 6. Hit **Save**. 7. This will be the only time Azure shows you the token value. Copy the token into the **Token** field. ###### Other Use this tab to connect to any Git server (GitLab, Bitbucket, Gitea, or a self-hosted instance) over HTTPS. 1. Create a Personal Access Token (or App Password) on your Git server with read & write access to the repository. 2. Enter the **Username** associated with that token. 3. Paste the token value into the **Token** field. 4. **CA Certificate (optional)**: only needed for self-hosted servers that use a private certificate authority. Paste the certificate (in PEM format, including the `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines) into this field so that FlowFuse can trust the connection. No changes to your infrastructure are required. ##### Configuring the stage When adding a Git Repository stage, select the token you created and enter the repository URL (for example `https://github.com/your-org/nodered-flows.git`). The stage can be configured with the branch to push/pull from as well as the filename to use for the snapshot. If a filename is not configured, it will generate the filename when pushing to the repository based on the name of Instance, Device or Device Group that provided the snapshot. The provided filename can include directory structures, allowing the snapshot to be stored in a subdirectory of the repository. When pulling from a repository, if the stage has not previously been used to push to the repository, the filename is a required property. It is also possible to configure the stage with different branches for the push and pull actions. This enables a Git-based review process as part of the pipeline; using a Pull Request process to review and approve the changes before merging between the two branches. #### Deploy to Devices This option is only applicable when the Stage Type is an Instance. When a pipeline stage with this action is deployed to, the same snapshot will be deployed to all devices connected to the defined instance. ## Protected Instances It is now possible to mark an instance as Protected. This means that all team members (including Owners) only have Read Only access to the Node-RED Editor and updates to the flows can only be made by a Team Owner running a DevOps pipeline that targets the instance. Protected mode is activated under Instance > Settings > Protect Instance ![The Protect Instance setting under Instance Settings](https://flowfuse.com/docs/user/images/protected-instance.png){dataZoomable=""} A Protected Instance will be marked by a status badge next to it's running state. Click on this badge will take you to the Settings page. ![Protected status badge shown next to the instance running state](https://flowfuse.com/docs/user/images/protected-instance-pill.png){dataZoomable=""} # Environment Variables Environment Variables allow you to manage variables used in your Node-RED flows from the FlowFuse application, you can read more on how to access environment variables inside Node-RED [in the Node-RED Docs](https://nodered.org/docs/user-guide/environment-variables){rel=""nofollow""}. An Environment Variable consists of a name and a value. ## Editing You can edit the environment variables from the `Settings` tab of an instance, select the `Environment` option from the side menu. Changes will only take effect when the Node-RED instance is restarted. ## Template provided variables The [Template](https://flowfuse.com/docs/user/concepts#template) may include some predefined environment variables that are automatically applied. The template may lock some of those variables to prevent an individual instance from changing them. Variables provided by the template cannot be deleted, however if they are editable, their value can be set to blank. ## Node-RED instance variables You can create additional variables for an individual Node-RED instance clicking the `Add variable` button. You can import variables from a `.env` file using the `Import .env` button. You can delete a variable using the trash can icon. The image below shows an instance with the following environment variables: - `policy_item_locked` - added by the template, locked - `policy_item_editable` - added by the template, editable - `FF_INSTANCE_ID` - provided by the platform, locked - `FF_INSTANCE_NAME` - provided by the platform, locked - `FF_PROJECT_ID` - provided by the platform, locked, depreciated - `FF_PROJECT_NAME` - provided by the platform, locked, depreciated - `INSTANCE_VAR` - added to the instance, editable ![](https://flowfuse.com/docs/user/images/project-envvar.png){width="500"} ## Standard environment variables Standard environment variables are set for all Node-RED instances running within the platform: - `FF_INSTANCE_ID` - The unique identifier of the instance - `FF_INSTANCE_NAME` - The name of the instance as set in FlowFuse - `FF_INSTANCE_URL` - The full URL of the instance (e.g. `https://my-instance.flowfuse.cloud`) In addition, the following variables are set when running on a device: - `FF_DEVICE_ID` - The unique identifier of the device - `FF_DEVICE_NAME` - The name of the device as set in FlowFuse - `FF_DEVICE_TYPE` - The device type label assigned in FlowFuse - `FF_SNAPSHOT_ID` - The unique identifier of the snapshot currently deployed to the device - `FF_SNAPSHOT_NAME` - The name of the snapshot currently deployed to the device When deploying the same set of flows out to multiple devices, these variables can be used by the flows to identify the specific device being run on. NOTE: `FF_SNAPSHOT_NAME` will not be immediately updated when the current snapshot is edited. It will only be updated when the snapshot is changed or a setting that causes the device to be restarted is changed. # Chat Interface The FlowFuse Expert chat interface is where you actively engage the AI: asking questions, building flows directly on your canvas, and querying live operational data. While other FlowFuse Expert features (like inline code completions and next-node prediction) work passively in the Node-RED editor, the chat interface is where you direct Expert and see it act on your behalf. > Team owners can disable all AI features, including the chat interface, from the team settings page. If AI has been disabled for your team, the Expert panel will not be available. ## Opening the Chat Interface To open the chat, first open your Node-RED instance using the **Open Editor** button. This launches Immersive Mode, where the Expert panel is available alongside your canvas. While in Immersive Mode you also have access to all [instance settings](https://flowfuse.com/docs/user/instance-settings/) from a drawer that sits beside the canvas. You can manage environment variables, snapshots, the palette, and other settings without leaving the editor. Use the eye icon at the top of the drawer to move it left or right, pin or unpin it, or toggle fullscreen mode. > **Note:** Note: As of v2.29, the FlowFuse Expert panel opens automatically whenever you enter the FlowFuse platform or an instance. If you close the panel, your preference is saved — Expert will remain closed on your next visit so it doesn’t get in your way. To open or close the panel while within the editor, click the FlowFuse drawer button in the top-left corner of the editor. !["Chat interface of FlowFuse Expert alongside the Node-RED editor"](https://flowfuse.com/docs/user/images/assistant/flowfuse-expert-drawer.png){dataZoomable=""} ## Chat Modes The chat interface operates in two distinct modes. You can switch between them using the **Mode Selector** at the top of the Expert panel. ::div --- style: "display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 20px 0;" --- [ Support Mode ](https://flowfuse.com/#support-mode){.assistant-feature}[ Insights Mode](https://flowfuse.com/#insights-mode){.assistant-feature} :: ### Support Mode **Support mode** is for flow-building assistance. Use it when you need help understanding, building, or debugging your Node-RED flows. Expert draws on its knowledge of Node-RED, your installed palette, and the context of your current flows to answer questions, explain and debug flows, and, when agentic flow building is enabled, build flows directly on your canvas. Typical use cases in Support mode: - "How do I convert data to CSV for writing to file & do you have any flow examples?" - "Explain what this flow is doing" - "Why is this node outputting a number instead of a string?" - "Is `node-red-contrib-string` node installed on this instance?" - "Build a flow that reads from Modbus and publishes each tag value to MQTT" #### Building Flows on the Canvas Expert can act on your requests directly by adding tabs, placing and wiring nodes, and configuring node properties on the canvas, rather than only describing what you should do yourself. To use it, describe what you want to build in the chat input, the same way you would ask a question. Expert will start working immediately, and you can follow along via real-time status updates in the chat panel as each step completes. When it finishes, the result is live on your canvas. You can continue refining it through chat by asking Expert to adjust a configuration, add a node, or change a topic path, or you can edit the canvas directly as normal. ![FlowFuse Expert building and configuring a flow directly on the Node-RED canvas](https://flowfuse.com/docs/user/images/assistant/flowfuse-expert-building-flow.gif){dataZoomable=""} Some prompts that work well: - "Create a flow that reads Modbus registers from `192.168.1.10` every 5 seconds and publishes the values to MQTT on `factory/line3/temperature`" - "Build an OEE dashboard tab with downtime reason buttons and a shift target gauge" - "Add a shift handover screen that shows the last 5 alarms and a notes input field" - "Set up a flow that polls an HTTP endpoint every minute and sends an alert if the response status is not 200" The more specific your prompt, the closer the result will be to what you need. Include node names, topic paths, endpoints, or field names where you have them. Expert will use them directly rather than substituting placeholders. > **Availability:** This feature is available on **FlowFuse Cloud** for all tiers, and from v2.32, to Self-Hosted Enterprise tiers. For Self-Hosted Enterprise tiers it requires a configuration for your instance. [Contact us](https://flowfuse.com/contact-us/?subject=FlowFuse%20Expert%20Application%20Building){rel=""nofollow""} to get access. #### Staying in Control of What Expert Does When Expert builds on your canvas, you stay in control of what it does and when. Three features let you steer it before and during a build: it can ask clarifying questions, propose a plan for your approval, and request permission before running individual actions. > **Availability:** Clarifying questions, plan mode, and tool permissions are available to FlowFuse Cloud and self-hosted users from v2.32. ##### Clarifying Questions Rather than guessing when a request is ambiguous, Expert can ask you a few questions before it starts. It presents up to four questions in a single turn, each as its own option group, either single-select or multi-select depending on the question. You answer all of them together and submit once, and Expert uses your answers to build exactly what you intended. You can edit an answered question and resubmit if you change your mind, up until you send a new message. ![The Expert asking grouped clarifying questions before building, each with selectable options](https://flowfuse.com/docs/user/images/assistant/follow-up-questions.png){dataZoomable=""} You can control how often Expert asks from the **Follow-up questions** setting in the Expert settings dialog, choosing whether it asks all its questions **all at once** or **one at a time**. ##### Plan Mode Plan mode lets you review Expert's approach before it changes anything on your canvas. Turn it on using the **plan mode** toggle in the composer. While it is enabled, Expert responds to your request with a proposed plan instead of acting on it. The plan is shown as its own card with four actions: - **Approve:** Expert exits plan mode and carries out the plan. - **Edit:** the plan text is loaded into the composer so you can adjust it directly and resubmit. - **Request changes:** describe what you'd like changed in your own words, and Expert proposes an updated plan. - **Reject:** the plan is abandoned and nothing is built. ![A plan card in the Expert, showing the proposed plan with Approve, Edit, Request changes, and Reject actions](https://flowfuse.com/docs/user/images/assistant/plan-mode.png){dataZoomable=""} Once you send a newer message, an earlier plan card is disabled so you can't act on a stale plan. ##### Controlling What Expert Can Do You decide which actions Expert is allowed to take on your behalf, and which require your approval first. These cover both the flow-building actions Expert runs on your canvas and the platform actions it can take across FlowFuse outside the editor. Each action, such as reading a flow, writing nodes to the canvas, or creating an instance, carries an action type of **Read**, **Write**, or **Delete**, and each is governed by a permission. **Approving actions in chat.** When an action is set to require approval, Expert pauses and shows an approval card in the chat before running it. The card shows the friendly name of the action, its type (Read / Write / Delete), and the exact parameters of the call as formatted JSON, so you can see precisely what Expert is about to do. You can then choose: - **Allow:** run this action once. - **Always allow:** run this action, and don't ask again for it for the rest of this chat. - **Deny:** skip this action; Expert adapts and explains what it did instead. - **Always deny:** skip it and don't ask again for it for the rest of this chat. ![FlowFuse Expert pausing for approval mid-build, showing the Adding Nodes action awaiting your decision](https://flowfuse.com/docs/user/images/assistant/expert-tool-approval-card.png){dataZoomable=""} Expert waits as long as you need, with no timeout on the decision. Stopping the chat while a card is open cancels the pending action (treated as a denial). "Always allow" and "Always deny" choices apply only to the current conversation and reset when you use **Start Over** or refresh. If you want to keep one, click **Make permanent** to save that choice for future chats. Once you approve an action, the card collapses to show what was decided, and the build continues. ![The chat after actions have been approved, each card showing its action type and that it is allowed for the current chat](https://flowfuse.com/docs/user/images/assistant/expert-tool-approval-chat.png){dataZoomable=""} **Configuring permissions in settings.** You can set your team's permissions ahead of time from the Expert settings dialog. To open it: 1. Open the FlowFuse Expert panel and select **Support** mode. 2. Click the settings (gear) icon at the top-right of the composer. Permissions are saved per user, for the team you currently have active. If you switch to another team, your settings there are kept separately, so you can maintain a different policy per team without losing your choices when you switch back. ![The Expert settings dialog showing follow-up question cadence and the tool permission defaults for each action type](https://flowfuse.com/docs/user/images/assistant/expert-tool-permissions-settings.png){dataZoomable=""} For each action type you set a **default** for all Read actions, all Write actions, and all Delete actions, choosing between **Always allow**, **Ask**, and **Always deny**. You can then override individual actions where you want different behaviour; an override stays in place until you reset it. Each default shows how many actions are "set individually", with a **Reset** control to return those actions to the default. The settings split actions into two groups: **Flow Building Tools**, the actions Expert takes on your canvas inside the editor, and **FlowFuse Platform Tools**, the actions it takes across the wider FlowFuse platform, such as listing your applications and instances, checking an instance's status or logs, and creating a new instance. Which group is shown first depends on where you are: flow-building tools lead inside the editor, and platform tools lead in the app. Flow-building tools only run inside an instance editor, so open one to let the Expert use them, though you can set their permissions here at any time. **Role-based limits.** Permissions respect your team role. Read-only team members cannot enable or trigger actions that write or delete, and will see why they are unavailable. This is enforced by Expert itself, not just hidden in the interface. **Auditability.** Every action Expert takes on the platform is recorded in the [Audit Log](https://flowfuse.com/docs/user/logs#ai-agents-and-api-activity), alongside the tool it called and the user it was acting on behalf of. **Example permission setups.** A few common ways to configure this: - **Balanced (the default):** Read is set to *Always allow*, Write and Delete to *Ask*. Expert reads your flows freely and checks with you before changing or removing anything. - **Fast building session:** set Write to *Always allow* so Expert builds without interrupting you, while leaving Delete on *Ask* so removals still need a nod. You can also grant a single tool *Always allow* from its approval card to skip repeat prompts for just that action for the rest of the chat. - **Review-only / locked down:** set Delete to *Always deny* and Write to *Ask*, so Expert can propose and build step by step but can never remove anything. For example, if you ask Expert to "add three nodes" with Write set to *Ask*, it pauses on the first *Adding Nodes* call and shows you the payload. Choose **Allow** to let just that call through, or **Always allow** so the remaining node additions in this chat don't prompt you again. **Flow-building actions reference.** The flow-building actions available today, grouped by action type. New actions are added over time and some are gated to your instance's Expert version, so your list may differ slightly. | Action type | What it covers | Actions | | ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Read** | View only, no changes | Describing Node Type, Describing Property, Getting Flow, Getting Nodes, Getting Palette, Listing Config Nodes, Listing Node Types, Listing Nodes, Listing Subflows, Listing Tabs, Searching Canvas, Selecting Nodes, Showing Workspace | | **Write** | Create or change resources | Adding Nodes, Adding Subflow, Adding Tab, Arranging Nodes, Creating Subroutine, Managing Groups, Opening Palette Manager, Setting Links, Setting Wires, Updating Nodes, Updating Tab | | **Delete** | Remove resources | Removing Nodes, Removing Tab | **Platform actions reference.** The actions in the FlowFuse Platform Tools group, for working with your platform outside the editor. These are read and write only. There are no delete actions. | Action type | What it covers | Actions | | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Read** | View only, no changes | List Applications, Get Application, Get Application Hosted Instances, Get Application Remote Instances, Get Application Instances Status, Get Application Audit Log, List Teams, Get Team, Get Hosted Instance, Get Hosted Instance Status, Get Hosted Instance Logs, Check Hosted Instance Name Availability, List Team Remote Instances, Get Remote Instance, Get Remote Instance Status, List Hosted Instance Snapshots, List Remote Instance Snapshots, List Hosted Instance Types, List Stacks, List Templates, List Blueprints, Open Hosted Instance, Open Hosted Instance Editor | | **Write** | Create or change resources | Create Application, Create Hosted Instance, Create Remote Instance, Assign Remote Instance To Application, Create Hosted Instance Snapshot, Create Remote Instance Snapshot | #### Context: What the Expert Can See Support mode becomes significantly more helpful when the Expert has context about your environment. Context is not automatic, you choose what to share with the Expert depending on what you need help with. ::div --- style: "display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 20px 0;" --- [ Palette Context ](https://flowfuse.com/#palette-context){.assistant-feature}[ Flow Context ](https://flowfuse.com/#flow-context){.assistant-feature}[ Debug Context](https://flowfuse.com/#debug-context){.assistant-feature} :: ##### Palette Context To add Palette Context, click the **Resource Selector** button (paperclip icon) in the chat interface and select **Palette**. Once added, the Expert has access to information about the nodes installed in your Node-RED instance - including installed packages and their versions. This allows you to ask questions like: - "Is my palette up to date?" - "What version of node-red-dashboard is installed?" - "Do I have a node available for reading from a PostgreSQL database?" The Expert can use palette context to tailor its suggestions - for example, recommending nodes you actually have installed rather than suggesting ones that are not available. ![Palette Context discussion with the FlowFuse Expert.](https://flowfuse.com/docs/user/images/assistant/palette-context.gif){dataZoomable=""} ##### Flow Context To add Flow Context, select the flow you want the Expert to reference from the flow tabs in the Node-RED editor. The selected flow is then added as context for the Expert to read and reason about. This makes it possible to ask questions directly about your flows without having to copy and paste JSON or describe your configuration manually. This allows you to ask questions like: - "What does this flow do?" - "Why does this flow output a number instead of a string?" - "Is there something wrong with this flow? I don't seem to get an output from the debug node!" Flow Context is what makes the Expert genuinely useful as a debugging and code review tool - it can see the same flow you're looking at and reason about it directly. ![FlowFuse Context discussion with the FlowFuse Expert.](https://flowfuse.com/docs/user/images/assistant/flow-context.gif){dataZoomable=""} ##### Debug Context To add Debug Context, you have 2 options: 1. Add individual log entries by clicking the ➕ button that appears over your debug message 2. Click the **Resource Selector** button (paperclip icon) in the chat interface and select **Add Debug Logs**. Once added, the Expert has access to the messages and output currently captured in your Node-RED debug panel. This allows the Expert to reason directly about the data or errors you are getting from your nodes at runtime, not just the structure of the flow itself. This allows you to ask questions like: - "Why is this debug output a number instead of a string?" - "What does this error message in the debug panel mean?" - "Is the payload structure here what my downstream node expects?" Debug Context is especially useful in combination with Flow Context - together they give the Expert both the structure of your flow and the actual data it is producing, making debugging significantly more effective. ![Debug Context discussion with the FlowFuse Expert.](https://flowfuse.com/docs/user/images/assistant/expert-debug-context.gif){dataZoomable=""} #### Resetting Context If the Expert starts giving unexpected or inconsistent answers, it may be due to accumulated context from earlier in the conversation influencing its responses. Use the **Start Over** button to clear the conversation and start fresh with a clean context. ### Insights Mode **Insights mode** connects the Expert to your live data via **Model Context Protocol (MCP)**. Use it when you want to query, analyze, or interact with real-world data - not just your Node-RED flows. In Insights mode, you first select an MCP Server that you've built using [FlowFuse MCP Server Nodes](https://flowfuse.com/node-red/flowfuse/mcp/){rel=""nofollow""}. The Expert can then use the tools and resources exposed by that server to answer questions against your live operational data. If you haven't built an MCP Server yet, see the guide on [building an MCP Server using FlowFuse](https://flowfuse.com/blog/2025/10/building-mcp-server-using-flowfuse/){rel=""nofollow""}. As of v2.32, Insights mode also reaches your **remote instances** at the edge, not just hosted instances. Point the Expert at a remote instance and ask about its live machine or operational data in plain language, with no dashboard to build and no query to write. > **Note:** Insights on remote and self-hosted instances relies on a change to how data is routed through the platform in v2.32. Remote instances need Device Agent 4.0.0 or newer, and existing hosted instances on FlowFuse Cloud need to update to Launcher 2.23.0 or newer to keep working. Typical use cases in Insights mode: - You have an MCP Resource named `production_lines_facilities_list`that returns a list of your production lines, their facility names and the facility types (stamping, assembly, packaging etc) - You can ask: "List all stamping facilities on our production lines" - You have added an MCP Tool named `get_production_live_state` -You can ask: "Tell me which of any of my assembly facilities are running and at what speed" - You have added an MCP tool named `get_production_oee`that - You can ask: "Show me the worst 3 OEE results for all production line facilities" > **Note:** Insights mode is currently in Beta. Capabilities are actively being expanded. **To switch to Insights mode:** 1. Open the FlowFuse Expert panel 2. Use the **Mode Selector** to switch from "Chat" to "Insights" 3. Select the MCP Server you want to query 4. Ask your question ## Writing Better Queries The quality of Expert's responses depends heavily on how your prompt is phrased, whether you are asking a question or requesting Expert to build something on the canvas. The more specific and contextual your prompt, the more accurate and actionable the result. Here are some common patterns to improve your prompts: ### Be specific about what you're referring to Vague references like "it", "this", or "that" require the Expert to guess what you mean. Name the thing explicitly. | Less effective | More effective | | ---------------------- | -------------------------------- | | "Is it up to date?" | "Is my palette up to date?" | | "What does this mean?" | "What does this log entry mean?" | | "Why did this happen?" | "Why did this error log occur?" | ### Describe the actual problem, not just a symptom If something isn't behaving as expected, describe what you expected versus what you got. | Less effective | More effective | | -------------------- | -------------------------------------------------------------- | | "This doesn't work" | "My flow should output a string but it is outputting a number" | | "The node is broken" | "The HTTP Request node is returning a 401 status code" | ### Include relevant details upfront The Expert works best when it doesn't have to ask clarifying questions. Include relevant context - the node type, the message property, the protocol, or the error - in your initial query. | Less effective | More effective | | --------------------------------- | --------------------------------------------------------------------------------------- | | "How do I connect to a database?" | "How do I connect to a PostgreSQL database using the node-red-contrib-postgresql node?" | | "How do I format this?" | "How do I format a Unix timestamp as an ISO 8601 string in a Function node?" | ### Ask one question at a time for complex topics If you have multiple questions, consider asking them separately so the Expert can give a focused answer to each rather than a broad response that covers everything superficially. *See also: [Node-RED Embedded AI](https://flowfuse.com/docs/user/expert/node-red-embedded-ai/) for AI features built directly into the Node-RED editor.* ## Keeping Expert Up to Date When a newer version of FlowFuse Expert is available, a banner appears in the chat area to let you know. You can update with a single click directly from the notification, without leaving your current workflow. ![FlowFuse Expert Update Banner](https://flowfuse.com/docs/user/images/assistant/ff-expert-update-banner.gif){dataZoomable=""} # FlowFuse Expert FlowFuse Expert is the AI built into FlowFuse and the Node-RED editor. It is not a generic AI assistant bolted onto the side of your workflow, it understands your flows, your installed nodes, your live data, and your environment in real time. **FlowFuse Expert is automatically installed and available in all hosted and remote instances running within or connected to FlowFuse**, no manual installation or configuration required. For self-hosted Enterprise customers, FlowFuse Expert can be enabled on request. [Contact us](https://flowfuse.com/contact-us/){rel=""nofollow""} to get it set up on your infrastructure. > **Note:** On self-hosted installations, FlowFuse Expert requires an Enterprise license and the platform's EMQX-based MQTT broker with the Team Broker capability enabled in the platform configuration (see [MQTT Broker configuration](https://flowfuse.com/docs/install/configuration#mqtt-broker-configuration)); installations running without EMQX cannot enable Expert. ## Managing AI Features **Team owners** can enable or disable all AI features for their team from the team settings page. When disabled, the Expert chat panel with all AI feature will get removed for that team. Running instances need to be restarted for the change to take full effect. **Self-Hosted Enterprise admins** have two additional controls: - Disable AI across the entire platform via the `ai.enabled` [configuration option](https://flowfuse.com/docs/install/configuration/#ai-configuration). This overrides all team-level settings. - Configure which AI features are available on a per-team-type basis from the Admin Panel. See [Managing Team Types](https://flowfuse.com/docs/admin/introduction/#managing-team-types) for details. ## What FlowFuse Expert Can Do FlowFuse Expert works in two distinct ways inside your environment. ### Chat Interface The Chat Interface is a conversational AI panel built into the FlowFuse Platform and accessible directly within the Node-RED editor. With agentic flow building enabled, you can describe what you want to build and Expert will build it on your canvas for you. It can also answer questions, debug flows, and query live operational data via MCP. The Chat Interface supports two modes: - **Support**: flow-building assistance, including asking questions, debugging, and building flows on the canvas. Expert can ask clarifying questions, propose a plan before it acts, and ask for your approval before running actions, and it can also take actions across the FlowFuse platform such as looking up your instances and creating new ones - **Insights**: query live operational data via MCP tools and resources exposed by your own MCP servers, on both hosted and remote instances [Learn more about the Chat Interface](https://flowfuse.com/docs/user/expert/chat/) ### AI in Node-RED FlowFuse Expert brings AI assistance directly into the Node-RED editor itself. It works where you already are - inside node editors, on the canvas, without requiring you to open a separate panel. AI features within the Node-RED editor include inline code completions, flow autocomplete, function builder, flow explainer, JSON generation, and CSS and HTML generation for FlowFuse Dashboard. > **Note:** FlowFuse Expert's in-editor AI features can also be installed as a plugin into Node-RED instances running outside of FlowFuse, using the `@flowfuse/nr-assistant` package from the Node-RED Palette Manager. This requires a FlowFuse Cloud account but does not require a paid subscription for the current release. The Chat Interface is exclusive to FlowFuse and cannot be installed externally. [Learn more about AI in Node-RED](https://flowfuse.com/docs/user/expert/node-red-embedded-ai/) ## Using Your Own AI Agent Connect your own AI agent, such as Microsoft Copilot, ChatGPT or Claude, to manage your platform and build and edit flows in your Node-RED instances. [Learn more about connecting your own agent](https://flowfuse.com/docs/user/expert/third-party-agents/) ## Data Privacy No data from FlowFuse is used by third-party AI service providers for training models. Some features utilize the OpenAI API, and as such some data is sent to OpenAI to process requests. In accordance with the [OpenAI Terms of Service](https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance){rel=""nofollow""} no data is used for training of future models. OpenAI will retain data sent via its APIs for 30 days for abuse monitoring, after which it is permanently deleted. # AI in Node-RED FlowFuse Expert brings AI assistance directly into the Node-RED editor itself. Unlike the [Chat Interface](https://flowfuse.com/docs/user/expert/chat/), which is a conversational panel you open separately, FlowFuse Expert's in-editor AI works where you already are - inside node editors, on the canvas, and in the palette. > **Note:** FlowFuse Expert can also be installed as a plugin into Node-RED instances running outside of FlowFuse, using the `@flowfuse/nr-assistant` package from the Node-RED Palette Manager. This requires a FlowFuse Cloud account but does not require a paid subscription for the current release. To enable the latest features, ensure your instance is running the latest Stack. **For FlowFuse Cloud instances, FlowFuse Expert (`@flowfuse/nr-assistant`) is automatically updated to the latest version.** Team owners can disable all AI features, including the in-editor AI, from the team settings page. When AI is disabled, the `@flowfuse/nr-assistant` plugin will not be loaded on new deployments. Running instances will need to be restarted for the change to take full effect. ## Features ::div --- style: "display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 20px 0;" --- [ Flow Autocomplete ](https://flowfuse.com/#flow-autocomplete){.assistant-feature}[ Inline Code Completions ](https://flowfuse.com/#inline-code-completions){.assistant-feature}[ Flow Explainer ](https://flowfuse.com/#flow-explainer){.assistant-feature}[ Function Builder ](https://flowfuse.com/#function-node-creation){.assistant-feature}[ Function Code Generation ](https://flowfuse.com/#function-code-generation){.assistant-feature}[ JSON Generation ](https://flowfuse.com/#json-generation){.assistant-feature}[ CSS & HTML Generation](https://flowfuse.com/#css-and-html-generation-for-flowfuse-dashboard){.assistant-feature} :: 1. **Flow Autocomplete:** Automated, intelligent suggestions for which node should be added next in your flow 2. **Inline Code Completions:** Inline code completions for Function node, Tables Query node and FlowFuse Dashboard `ui-template` node 3. **Flow Explainer:** Get detailed explanations of the selected nodes in your flow 4. **Function Node Creation:** Create a new function node directly, driven by natural language 5. **Function Code Generation:** Within the scope of an existing function node, ask the assistant to write code for you 6. **JSON Generation:** In-editor JSON generation within the JSON editor for all typed inputs and JSON editors 7. **CSS and HTML Generation:** In-editor CSS and HTML generation for FlowFuse Dashboard `ui-template` nodes ### Flow Autocomplete ![Recording of the flow autocomplete in-action, with up/down keys used to toggle suggestions and tab to move to the next suggestion](https://flowfuse.com/docs/user/images/assistant/node-autocomplete.gif){dataZoomable="" width="700px"}*Recording of the flow autocomplete in-action, with up/down keys used to toggle suggestions and tab to move to the next suggestion* FlowFuse Expert runs a trained, in-browser machine learning model that provides intelligent suggestions for which node should be added next in your flow. You can accept the suggestion by clicking it or by pressing the `Tab` key. You can also toggle through suggestions by pressing the `Up` and `Down` keys. ### Inline Code Completions Mimicking the familiar code assistant in your IDE, FlowFuse Expert provides inline code completions for Function nodes, Tables Query nodes, and FlowFuse Dashboard `ui-template` nodes. ![inline code completions](https://flowfuse.com/docs/user/images/assistant/inline-completion.png){dataZoomable="" width="450px"}*A simple example of inline code completions for a Function node* This feature accelerates the writing of custom code and queries by providing intelligent suggestions without having to leave the editor, lowering the barrier to entry for non-technical users. Using comments is optional - the assistant will do its best to understand the context of the code and provide suggestions based on the surrounding code. However, writing comments does help to frame the request in a way that is more likely to produce accurate results. ### Flow Explainer FlowFuse Expert adds a button to the Assistant menu that will explain what the selected nodes do. To use this feature, select one or more nodes on the canvas and click the "Explain Flows" button in the Assistant menu. ### Function Node Creation ![FlowFuse Expert dialog](https://flowfuse.com/docs/user/images/assistant/dialog-function-node-builder.png)*Screenshot showing the FlowFuse Expert dialog for creating a function node* Use natural language to request a new function node be added to your Node-RED flow. This is useful when you want to quickly add a function node without having to drag it from the palette and write the code yourself. If your instance supports external modules, you can ask for a function node that uses them and they will be added to the function node setup automatically. If your function node requires multiple outputs, FlowFuse Expert will set the correct number of outputs accordingly. ### Function Code Generation FlowFuse Expert adds a code lens to the function node editor that allows you to generate code directly within the editor. ![inline code lens](https://flowfuse.com/docs/user/images/assistant/function-node-inline-code-lens.png){dataZoomable=""} This is useful when you want to quickly add or rewrite code within an existing function node without generating a full new function node from scratch. ### JSON Generation FlowFuse Expert adds a code lens to the JSON editor that allows you to generate JSON directly within the Monaco editor. ![json generation](https://flowfuse.com/docs/user/images/assistant/json-prompt.png){dataZoomable="" width="700px"}*Screenshot showing a FlowFuse Expert prompt for JSON generation* This is useful when you want to quickly generate JSON for a prototype or to test a piece of functionality in your flows. ![json generation](https://flowfuse.com/docs/user/images/assistant/json-results.png){dataZoomable="" width="700px"}*Screenshot showing the result of the above FlowFuse Expert prompt* ### CSS and HTML Generation for FlowFuse Dashboard FlowFuse Expert adds a code lens to the FlowFuse Dashboard `ui-template` node that allows you to generate CSS and HTML directly within the code editor. It is aware of the context of the node and will generate suitable CSS and HTML components for Vuetify and the FlowFuse Dashboard. ## Installing in External Node-RED Instances > **Note:** FlowFuse Cloud instances have FlowFuse Expert automatically installed. Manual installation is only needed for Node-RED instances running outside of FlowFuse. Only the in-editor AI features can be installed externally - the Chat Interface is exclusive to FlowFuse. To install the plugin in your own Node-RED instance: 1. Use the Node-RED Palette Manager to install the package `@flowfuse/nr-assistant` 2. Restart Node-RED 3. Click the FlowFuse Expert icon in the header 4. Follow the prompts to connect to your FlowFuse Cloud account 5. Once connected, you will be able to use all FlowFuse Expert features ### FlowFuse Expert for self-hosted customers If you are self-hosting FlowFuse with an Enterprise license, get in touch with [support](https://flowfuse.com/support) who will be able to help get you set up to use FlowFuse Expert locally. # Connect Your Own Agent **Introduced in FlowFuse 3.0** You can connect your own AI agent to FlowFuse. The agent your team already uses can manage your platform and build and edit the flows inside your Node-RED instances. Because the agent is yours, so is the model it runs on. ## Connect your agent Any MCP client that supports the HTTP transport can connect. That is the only requirement. Pick your agent for the address to copy and the steps that apply to it: ::agent-setup-tabs{exclude-expert :signup='false' surface="docs"} :: The same three steps, written out: 1. **Add the FlowFuse MCP address in your agent's connector settings.** See [where to add it, per agent](https://flowfuse.com/#where-to-add-it-per-agent) if you are not sure where yours lives. :br On FlowFuse Cloud: ```text https://app.flowfuse.com/mcp ``` :brSelf-hosted, substitute your own platform address: ```text https://flowfuse.example.com/mcp ``` 2. **Sign in.** FlowFuse uses OAuth, so your agent sends you to a FlowFuse login page to authenticate, in the same way as any other application you sign in to. If your client asks for an OAuth client ID or secret, leave them blank. FlowFuse registers your client for you. 3. **Choose what the agent may do.** As part of signing in you decide which teams the agent may act on, and whether it has editing rights or read access only. Your agent is now connected. OAuth lets you connect by signing in. If your MCP client does not support OAuth, use a token instead, covered in [clients without a sign-in flow](https://flowfuse.com/#clients-without-a-sign-in-flow). > **Note:** This is separate from [MCP server nodes](https://flowfuse.com/node-red/flowfuse/mcp/){rel=""nofollow""}. Those let you build MCP servers inside your flows, connected to anything you like, to give any AI a set of tools of your own design. This page is about operating FlowFuse itself through MCP, where FlowFuse is the server and your agent is the client. ## What your agent can do, and what you grant Ask your agent what it can do in a given team or instance if you want the current picture, since its tools reflect the instance it is connected to. **With read access**, an agent can see your teams and applications with their activity history, your hosted and remote instances with their live status and runtime logs, your snapshots, and your FlowFuse Tables databases including table schemas and row data. It can also see which instance types, templates and blueprints your team has available. **With editing rights**, it can additionally create applications and hosted instances, register remote instances and assign them to applications, take snapshots, and build and edit flows. An agent with read access has no ability to change anything. An agent can query your FlowFuse Tables data to answer questions. With editing rights it can go further and build a flow with a [Query Node](https://flowfuse.com/docs/user/ff-tables/#query-nodes) that reads and writes your tables, exactly like a flow you would build yourself. ### Deleting, and deploying Nothing an agent can do through FlowFuse deletes anything, for now. There is no tool for deleting an instance, an application, a snapshot or a team. Deploying is also done by you, for the same reason. We are focused on delivering AI in a meaningful way that can act as required both in production setups and in setups where experimentation is permitted, so expect this to develop. ## Editing flows Asking about your platform needs nothing open. Editing flows happens in a live Node-RED editor, so that you can see the work as it happens on the canvas rather than receiving a result you have to go and check. Working in the running editor also means the agent gets Node-RED's own validation back as it goes, so it catches and corrects its own mistakes rather than handing you a flow that will not load. When you ask for flow work, your agent will guide you to connect an editor session. In the platform header there is a control for indicating which of your current browser sessions the agent should work in, so if you have several open you can point it at the right one. Ending the session, or closing the tab, ends the agent's access to your editor. Switching team also ends it. ## Where to add it, per agent The agents below are the common ones and where their settings live. Every other AI Agent that supports MCP over HTTP connects the same way. ### Microsoft Copilot In **Copilot Studio**, open your agent's **Tools** page, select **Add a tool**, then **New tool**, then **Model Context Protocol**. Give the server a name and a description saying what it is for, since the orchestrator uses that description to decide when to call it, and enter the FlowFuse MCP address as the server URL. To make FlowFuse available across a Microsoft 365 tenant rather than in a single agent, a tenant administrator registers it in the Microsoft 365 admin center. Once approved it appears in Copilot Studio for everyone. Access through Copilot Studio runs over Power Platform connectors, so any Power Platform data policy your organisation has also governs it. ### ChatGPT Custom connectors live behind developer mode. Turn it on under **Settings**, then **Apps & Connectors**, then **Advanced settings**, then add FlowFuse by URL and sign in. Developer mode needs a paid plan, so it is not available on the free tier. ### Claude Where custom connectors are available on your plan, add one and enter the FlowFuse MCP address. On Team and Enterprise plans an owner adds the connector for the organisation first, and then each person connects and signs in individually. ### Command-line and editor agents Claude Code, Cursor, Visual Studio Code and Gemini CLI all connect to the same address. Where a client supports OAuth, sign in; otherwise use a token, see [clients without a sign-in flow](https://flowfuse.com/#clients-without-a-sign-in-flow). For Claude Code: ```bash claude mcp add --transport http flowfuse https://app.flowfuse.com/mcp ``` ### Local and self-hosted models Use any HTTP-capable MCP client, such as LM Studio, LibreChat or Open WebUI, pointed at your own model, and add the FlowFuse address as a server in that client's configuration. Note that Ollama is a model runtime rather than an agent, so it needs an MCP client in front of it. ## Clients without a sign-in flow Where your client does not support OAuth, give it a token in its configuration file instead. Both routes reach the same FlowFuse with the same enforcement. Create a [Personal Access Token](https://flowfuse.com/docs/user/user-settings/#personal-access-tokens) and [scope it](https://flowfuse.com/docs/user/user-settings/#scoping-a-token) the same way you would when signing in, to the team you want the agent working in rather than to everything you can reach. Then give the client the FlowFuse address together with that token as a bearer token in an `Authorization` header. How that is written down belongs to the client rather than to FlowFuse. Two JSON shapes are in common use, one keyed on `servers` and one keyed on `mcpServers`, and clients also differ on where the file lives and whether they accept headers at all, so follow your own client's configuration reference. For the two shapes, see the [`servers` reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration){rel=""nofollow""} and the [`mcpServers` reference](https://modelcontextprotocol.io/docs/develop/connect-local-servers){rel=""nofollow""}. ## Approvals and audit FlowFuse tools carry their recommended usage and permissions, so a connected agent knows what each one is for before it calls it. Most MCP clients then ask you to confirm before they run a tool. That prompt belongs to the client rather than to FlowFuse, so how it looks, and whether you can turn it off, differs between them. FlowFuse Expert's own approval cards are a first-party feature and do not apply here. What FlowFuse enforces on every call is what you granted: the teams, and read access or editing rights. That is the granularity. It is a boundary around what an agent can reach rather than a per-tool allow list, and it applies the same way whether the grant came from signing in or from the scope on an access token. Actions an agent takes appear in the [audit log](https://flowfuse.com/docs/user/logs/#ai-agents-and-api-activity), attributed to your account and marked as having come from a connected agent. ## If something is not working **A change was refused.** The agent has read access only. Re-connect it and grant editing rights. **The agent cannot reach a team.** That team was not included when you signed in. Re-connect and include it. **The agent cannot see the instance you mean.** Flow and editor work runs in a connected editor session. Ask your agent to list your sessions and connect to the right one. ## Getting the best out of it None of this is something to set up before you start. Your agent will tell you when something is in the way, and can help resolve it. For the smoothest experience, an instance the agent works in should be on a current launcher or Device Agent, with a current in-editor assistant. These update when an instance restarts, so a long-running instance may be behind. If an agent cannot do something you expected in a particular instance, this is usually why, and asking the agent about it is the quickest route. On self-hosted, platform messaging runs over the MQTT broker, so the Team Broker needs to be available. Whether anything is needed from you depends on how your platform was installed; see [MQTT Broker configuration](https://flowfuse.com/docs/install/configuration/#mqtt-broker-configuration). AI features also require an Enterprise licence with AI enabled. # FlowFuse Tables ::div{.ff-callout.ff-callout--warning} Warning :::div{.ff-callout__content} This feature is currently in [the beta state](https://flowfuse.com/handbook/engineering/releases/#beta-release){rel=""nofollow""}. ::: :: From FlowFuse v2.20.0 Teams (Enterprise teams only) can create a relational database to use to store data. You can create a database by selecting the Tables entry in the left hand menu ![Screenshot of FF Tables create database menu](https://flowfuse.com/docs/user/images/tables/create-database.png){dataZoomable=""}*Screenshot of FF Tables create database menu* Once you have created a database you can use the wizard to create some tables. The wizard offers a subset of the most used column types and has the option to set default values and generate sequences for ids. ![Screenshot of create tables wizard](https://flowfuse.com/docs/user/images/tables/create-table-wizard.png){dataZoomable=""}*Screenshot of create tables wizard* Once the tables have data in them then the first 10 rows will be displayed in the Explorer tab. ![Screenshot of Tables Explorer](https://flowfuse.com/docs/user/images/tables/tables-ui-screenshot.png){dataZoomable=""}*Screenshot of Tables Explorer* ## Query Nodes FlowFuse Node-RED instances come with a Query Node that will be enabled if running in a Team with a FlowFuse Tables database enabled. This node will automatically import the required credentials to connect to the database. ![Screenshot of Node-RED query node](https://flowfuse.com/docs/user/images/tables/tables-query-node.png){dataZoomable=""}*Screenshot of Node-RED query node* ## Postgres Clients You can connect any Postgres Client to the FlowFuse Tables database using the information on the Credentials tab ![Screenshot of credentials](https://flowfuse.com/docs/user/images/tables/credentials.png){dataZoomable=""}*Screenshot of credentials* # FlowFuse File Nodes Node-RED instances running within FlowFuse Cloud with Launcher version before 2.7.0 include a modified set of nodes that make it possible to store files safely regardless of the environment. Cloud based instances can read and write to persistent storage using these nodes. Edge devices will store files on its local filesystem. There are two nodes in the File Node collection: - `file` - A file node for writing to persistent storage - `file in` - A file node for reading from persistent storage ## FlowFuse 2.7.0 and later With the release of FlowFuse v2.7.0 a new Persistent Storage feature was enabled. This allows the default Node-RED File Nodes to work safely by mounting a volume to ensure files are persisted across all restarts of the Instance. On Docker, Kubernetes self hosted instances and FlowFuse Cloud the volume is mounted on `/data/storage`. The Current Working Directory for the Node-RED process is set to this directory, this means that if you do not specify a path in a node it will created or read from this directory. For LocalFS builds a `storage` directory is created in the Instance User Directory, this means that if FlowFuse is installed in `/opt/flowfuse` the directory will be at `/opt/flowfuse/var/projects//storage` ## Templates On FlowFuse Cloud before v2.7.0 the Default Template had an explicit exclusion for `10-file.js` to ensure that the replacement FlowFuse File Nodes were loaded. This Template was renamed to "Default v1" to differentiate it. If your instance is using this version of the template then it will need modifying to allow access to the default Node-RED File Nodes to make use of the new Persistent Storage. Please contact FlowFuse support to arrange this, they will also be able to migrate any files stored in the old service. ## Usage Simply drop the file nodes into your flows as you would with the regular file nodes in Node-RED. **Example:** Write string to a file, then read from the file :iframe{allow="clipboard-read; clipboard-write" height="100%" src="https://flows.nodered.org/flow/7f93fbbf67f9dc4e81bfbeb2b921881e/share" style="border: none;" width="100%"} There are more helpful built-in examples on the **Import Examples** dialog in Node-RED. ## Deployment Considerations When a snapshot is deployed to a device, the original Node-RED file nodes are used and any files will be stored on the device's local filesystem. # High Availability mode High Availability mode allows you to run multiple copies of your Node-RED instance, with incoming work distributed between them. The following requirements apply: - FlowFuse 1.8+ running with an EE license and the kubernetes driver - FlowFuse Cloud Within FlowFuse Cloud it is currently free to use for all teams, but will become a chargeable feature in a future release. ## Restrictions - Enabling or disabling HA mode requires a restart of the Instance. - When in HA mode, two copies of the flows are run. - Flows cannot be directly modified in an HA Instance; the editor is disabled. A [DevOps Pipeline](https://flowfuse.com/docs/user/devops-pipelines) should be created to deploy new flows to the instance. - Any internal state of the flows is not shared between the HA copies. - The [FlowFuse Persistent Context](https://flowfuse.com/docs/user/persistent-context) is not synchronised between the HA copies - The logs view show combined logs of all copies. An identifier indicates which replica the log message originates from. You have the possibility to filter the messages. More details of these restrictions are available below. ## Setting up High Availability mode To make full use of the HA mode you will need two separate Node-RED instances. The first will be your development instance. This is where you can build and test your flows. The second will be your HA 'production' instance. This instance will run multiple copies of the flows and get updated from the development instance using a DevOps Pipeline between the two instances. To enable HA mode on your production instance, open the [settings page](https://flowfuse.com/docs/user/instance-settings) for your instance and go to the High Availability section. Click the 'Enable HA mode' button and confirm the choice in the dialog. Once enabled the instance will be restarted to apply the settings. Once enabled, the editor will no longer be available. ## Editing an HA instance As the editor is disabled for an HA instance the flows cannot be directly modified. There are two options for updating the flows on an HA instance. ### Disabling HA mode If you disable HA mode via the setting page, after the instance has been restarted the editor will be available and changes can be made. However, this will mean a period of downtime whilst the instance is brought in and out of HA mode. ### DevOps Pipeline Create a [DevOps pipeline](https://flowfuse.com/docs/user/devops-pipelines) from a 'development' Instance to push updates to your HA instance. This allows you to free develop your flows in one instance and when you are happy with the results, use the pipeline to push the changes over to the HA instance. ## Building HA-ready flows Whilst FlowFuse can help run multiple copies of your flows, and provide the necessary load balancing between those copies, it still requires the flows to have been created with HA in mind. This guide provides some things to consider when building HA-ready flows. We'll continue to expand on this as we gather more feedback from our users. There are two things to consider - how state is managed and how work is distributed between the copies. ### Managing state The most well-suited flows for HA are those that do not depend on local state being maintain between individual messages coming into the flow. This is because, in an HA instance, the messages are distributed between the copies - none of them get to see every message. When state is required, it needs to be stored somewhere that all instances can access it - for example an external database service. FlowFuse provides a [persistent context](https://flowfuse.com/docs/user/persistent-context) option - however this includes a local caching layer that means it doesn't fully synchronize between the instances in real time. This is something we'll be working on for a future iteration of the feature, as it will require some changes in Node-RED to unlock its full potential. There is also the state that is implicitly maintained within the nodes of a flow. For example, the Smooth node can be used to calculate a running average value of messages passing through it. The node does that by keeping in memory the recent values so it can recalculate the average with each update. In an HA instance, the node will only be calculating the average for the messages it sees. ### Routing work When HA is enabled, all HTTP traffic directed at the instance will be load-balanced between the copies automatically. It gets a little more complicated where flows connect out to external systems to pull in work - for example, when using MQTT. MQTT includes a feature called [Shared Subscriptions](https://www.hivemq.com/blog/mqtt5-essentials-part7-shared-subscriptions/){rel=""nofollow""} that allows a broker to distribute messages between a group of subscribers. This provides the load balancing needed for an HA instance - but the flow must be configured to use the appropriate shared subscription topics, as well as to not set a ClientID on the connection to avoid conflicts between the connections. The [Project Nodes](https://flowfuse.com/docs/user/projectnodes) have been updated to support shared subscriptions automatically when running in HA mode. There are lots of other nodes that can be used to trigger flows, whether by listening for events on an API, connecting to locally attached hardware and many things in between. Typically, those that are more cloud-aligned, such as messaging systems like Kafka and AMQP will have very well established ways of doing load balancing. # HTTP Access Tokens When a FlowFuse instance is secured with [FlowFuse User Authentication](https://flowfuse.com/docs/user/instance-settings#flowfuse-user-authentication), only users who are in the same FlowFuse team can access the dashboard and HTTP endpoints created within Node-RED. In some cases, it is also necessary to provide secure access to the endpoints for other applications to use. This is where the HTTP Access Tokens can be used. With the FlowFuse User Authentication option enabled on the Instance's Security settings tab, the table of access tokens is shown. ### Creating an access token To create a token, click the Add Token button. Given your token a name and an optional expiry date, then click create. You will be shown the token value - this is the only time it will be shown so make a note of it before closing the dialog. ![](https://flowfuse.com/docs/user/images/bearer-token-dialog.png) ### Using an access token The token can be used when making an HTTP request to the Node-RED instance by providing it in the `Authorization` http header as a bearer token. For example, if a Node-RED instance has a flow deployed that includes an `HTTP In` node listening on `/token-demo`, the following `curl` command can be used to access it: ```shell curl -H "Authorization: Bearer ffhttp_FKc_S4qlTBV1H411hmhneHcSJ6F_FGNQLPYbnoD3-B0" \ https://example.flowfuse.cloud/token-demo ``` To access it from another Node-RED instance, you can use the `HTTP Request` node by enabling the 'Use authentication' option, selecting 'bearer authentication' and providing the token: ![](https://flowfuse.com/docs/user/images/bearer-token-nr-request.png) ### Remote Instances HTTP Access Tokens can also be used to secure HTTP endpoints served by Node-RED flows running on Remote Instances (devices). This requires [Device Agent v4](https://flowfuse.com/docs/device-agent/introduction) or later. On self-hosted FlowFuse, it also requires FlowFuse v2.32 or later. # Instance Settings The Instance Settings allow you to customize many aspects of your Node-RED runtime. To access them, click on an instance from the FlowFuse platform and select the **Settings** tab. The instance view also includes other tabs such as Overview, Devices, Version History, Assets, and Node-RED Logs. You can also access all the same settings from within the immersive Node-RED editor. The settings drawer sits alongside the canvas without overlapping it, so you can adjust environment variables, manage snapshots, update the palette, and make other changes without leaving your flow. You can customize the drawer layout using the eye icon at the top of the drawer, which lets you move it from right to left, pin or unpin it, and toggle fullscreen mode. ![Instance settings drawer](https://flowfuse.com/docs/user/images/instance-settings.png) Instance Settings are split into a number of sections: - [Instance Settings](https://flowfuse.com/#instance-settings) - [General](https://flowfuse.com/#general) - [Change Stack](https://flowfuse.com/#change-stack) - [Copy Instance](https://flowfuse.com/#copy-instance) - [Import Instance](https://flowfuse.com/#import-instance) - [Suspend Instance](https://flowfuse.com/#suspend-instance) - [Delete Instance](https://flowfuse.com/#delete-instance) - [Environment](https://flowfuse.com/#environment) - [High Availability](https://flowfuse.com/#high-availability) - [Editor](https://flowfuse.com/#editor) - [Security](https://flowfuse.com/#security) - [None](https://flowfuse.com/#none) - [Basic Authentication](https://flowfuse.com/#basic-authentication) - [FlowFuse User Authentication](https://flowfuse.com/#flowfuse-user-authentication) - [Palette](https://flowfuse.com/#palette) - [Alerts](https://flowfuse.com/#alerts) ## General This section includes several actions you can take on the instance: ### Change Stack The Stack determines the version of Node-RED being used. If a new stack is available, you can use this option to update your stack. ### Copy Instance This allows you to create a copy of the instance in your team. ### Import Instance This allows you to take existing Node-RED flow and credential files and import them into your instance. ### Suspend Instance This stops the instance entirely. ### Delete Instance If you're sure you don't want the instance anymore, this allows you to delete it. You cannot undo deleting an instance. Devices assigned to the instance will be unassigned from the instance and available to be reassigned to a new instance. ## Environment This allows you to manage the environment variables. More information on working with environment variables is available [here](https://flowfuse.com/docs/user/envvar). ## High Availability This allows you to manage the HA settings of the instance. High Availability is a Preview Feature. More information is available [here](https://flowfuse.com/docs/user/high-availability). ## Editor This covers many options to customize the Node-RED editor. This includes: - Disabling the editor entirely - Modifying the paths the editor and dashboard are served on - Choosing which code editor to use in your Node-RED nodes - Setting a custom title for the editor - Choosing a light or dark theme for the editor - Controlling the runtime timezone - Controlling the use of node modules in function nodes ## Security This allows you to modify the security settings of the runtime. In particular this covers the security applied to any HTTP routes served by the runtime. ### None The default option is not to apply any security - so any HTTP In nodes, or Node-RED Dashboard can be accessed by anyone. ### Basic Authentication You can optionally enable Basic Authentication, with a single hardcoded username and password. ### FlowFuse User Authentication Alternatively, with a licensed instance of FlowFuse, you can require anyone accessing those routes to be logged into FlowFuse. The hosted pages or API endpoints are only available for FlowFuse users who have access to the team on FlowFuse and the cloud instance. If using FlowFuse user Authentication you can also generate HTTP Bearer tokens that can be used to access APIs hosted in Instances with HTTP-in/HTTP-response nodes. More information on using HTTP Access Tokens can be found [here](https://flowfuse.com/docs/user/http-access-tokens). ## Palette This allows you to manage what extra nodes are installed inside Node-RED, as well as any restrictions you want to apply to the Palette Manager within Node-RED. It includes the option to add extra Node-RED Catalogue URLs and a `.npmrc` file that will be deployed to the instance. Details of the `.npmrc` format can be found [here](https://docs.npmjs.com/cli/v9/configuring-npm/npmrc){rel=""nofollow""} ## Alerts Alerts are a feature designed to provide email notifications based on specific Auditlog events. This functionality ensures prompt awareness and response to critical events. Users can configure alerts for the following Auditlog events: - Node-RED has crashed - Node-RED has been placed in Safe Mode When configuring alerts, you can choose the recipients of these notifications: - Team Owners - Team Members - Both Owners and Members # FlowFuse Node-RED Instance States The following list describes the possible states a hosted or remote Node-RED instance can find itself in. ## Stable States - **running**: Instance is fully operational. Flows are deployed and executing normally. - **suspended**: Instance resources are freed and flows are paused. Can be resumed without redeploying. - **stopped**: Instance is not executing any flows and is intentionally shut down. - **error**: Instance cannot operate normally due to a critical configuration or runtime issue. Requires user intervention. - **crashed**: Instance terminated unexpectedly due to repeated runtime errors or failures. - **rollback**: A previous working version of the instance has been restored due to deployment failure or manual revert. - **warning**: Instance is running but has non-critical issues (e.g., node failures, resource limits close to threshold). - **safe**: Running in safe mode after multiple crashes. Editor works, flows are not started until a deploy action is triggered. - **protected**: Editor is disabled. Deployment can only occur via pipeline or controlled automation. - **connected**: Instance has established a successful connection to its runtime environment and is reachable. ## Transitional States - **loading**: Instance UI or resources are being prepared (initial startup or page load). - **installing**: Required packages, dependencies, or container layers are being installed. - **starting**: Flows and runtime services are initializing. - **stopping**: Flows and runtime services are shutting down gracefully. - **restarting**: Instance is stopping and then immediately starting again, typically after a deployment or configuration update. - **suspending**: Instance flows are being paused and resources deallocated before entering suspended state. - **importing**: An external project or configuration is being applied to the instance. - **pushing**: Changes are being uploaded to the runtime or container registry. - **pulling**: Artifacts or project content are being fetched from a registry or pipeline source. # Getting Started with FlowFuse This guide will help you learn how to use the FlowFuse platform to quickly create new Node-RED applications after a successful [installation](https://flowfuse.com/docs/install/introduction) or [sign-up](https://app.flowforge.com/account/create){rel=""nofollow""} for FlowFuse Cloud. ## Creating a Node-RED Instance ### Your First Hosted Instance Your first Node-RED instance should be automatically created upon your initial login to FlowFuse. ![Instance created initial login](https://flowfuse.com/docs/user/images/getting-started/ff-home-initial-login.png){dataZoomable=""} **Accessing the Node-RED Editor:** To access the Node-RED Editor, simply click on the option shown in the image below marked with a red box: ![Open Editor Shortcut](https://flowfuse.com/docs/user/images/getting-started/open-editor-shortcut.png){dataZoomable=""} Alternatively, you can click on that instance and then you will find the "Open Editor" button at the top right: ![Open Editor](https://flowfuse.com/docs/user/images/getting-started/Open-Editor.png){dataZoomable=""} ### Creating Additional Instances For utilizing various other FlowFuse features (e.g., DevOps Pipelines), it's highly beneficial to create a second Node-RED instance. A second Node-RED instance is included in the Trial Phase of FlowFuse Cloud. **From the Home Page:** 1. Click on the "Add Instance" button shown in the image below 2. Enter the name you want for your instance 3. Select your application in which you want to create it 4. Select the instance type 5. Select the Node-RED version from the dropdown 6. Click "Next" and [select the blueprint](https://flowfuse.com/#selecting-a-blueprint) you want to use ![Add Instance](https://flowfuse.com/docs/user/images/getting-started/ff-home-after-initial-login-add-instance.png){dataZoomable=""} **From the Applications Page:** Alternatively, you can go to your applications from the left sidebar by clicking "Applications," select your application—in our example, "Demo's Application"—and click "Add Instance," then follow the same process. [Learn more about Instances](https://flowfuse.com/#working-with-instances) ### Your First Remote Instance A remote instance allows you to run Node-RED on your own hardware while managing it through FlowFuse. **Adding a Remote Instance:** 1. Click on "Remote Instances" from the left sidebar 2. Click "Add Remote Instance" 3. Enter a name for your instance and select the device type 4. Select your application and click "Add" ![Add Remote Instance](https://flowfuse.com/docs/user/images/getting-started/add-remote-instance.png){dataZoomable=""} ![Add Remote Instance Form](https://flowfuse.com/docs/user/images/getting-started/instance-add-form.png){dataZoomable=""} **Installing the Device Agent:** FlowFuse will show you a device configuration window with installation options: - **One-Line Install** (Recommended): Automatically installs Node.js (if needed), the device agent, and registers your device - **NPM Installation**: Manual installation instructions for Windows, Mac, or Linux ![Device Configuration Window](https://flowfuse.com/docs/user/images/getting-started/device-configuration-window-2.gif){dataZoomable=""} Follow the steps in the window to connect your device—it takes less than a minute. [Learn more about Device Agent](https://flowfuse.com/docs/device-agent/introduction) **Accessing Your Remote Instance:** Once registered, you can manage your remote Node-RED instance through FlowFuse: To start building flows: 1. Enable "Developer Mode" from the top right 2. Click "Open Editor" ![Developer Mode](https://flowfuse.com/docs/user/images/getting-started/developer-mode.png){dataZoomable=""} ![Open Editor](https://flowfuse.com/docs/user/images/getting-started/open-editor-remote-instance.png){dataZoomable=""} ### Selecting a Blueprint When creating a new Node-RED instance, you have the option to choose a blueprint tailored for specific use cases. For example, our "ANDON Operator Terminal" blueprint can be selected, and it will automatically configure the Node-RED instance, install necessary nodes, sparing you the need to start from scratch. Click "Create Instance" from the top-right to complete the process. While these templates are powerful out-of-the-box, they're also fully customizable, allowing you to tweak them to suit your unique requirements. Ultimately, blueprints speed up the learning curve for new users and expedite the solution-building process for experienced ones. ![Blueprint selection](https://flowfuse.com/docs/user/images/getting-started/blueprint-selection.png){dataZoomable=""} **NOTE**: *Some blueprints may only be available on certain tiers* ## Creating Your First Flow FlowFuse published an [eBook on Node-RED development](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/){rel=""nofollow""}, which is a great resources when you're new to Node-RED. You can also read our [blog post on creating your first flow](https://flowfuse.com/blog/2023/01/getting-started-with-node-red/){rel=""nofollow""}. ## Creating Your First DevOps Pipeline DevOps Pipelines enable you to link multiple Node-RED instances together in a deployment pipeline. 1. **Add a Pipeline**: Select your application and click `Add Pipeline`. :br![Add Pipeline](https://flowfuse.com/docs/user/images/getting-started/Add-Pipeline.png) 2. **Name Your Pipeline**: Enter a suitable name. 3. **Add Stages**: You can now add stages to your pipeline. In our example, we add a Development Stage and a Production Stage. 4. **Execute the Pipeline**: It is now easy to execute the pipeline with one click, promoting your recently created flow to your Production Node-RED instance. :br![Execute Pipeline](https://flowfuse.com/docs/user/images/getting-started/devops-pipeline-w-stage.png) [Learn more about DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines/) ## Working with Devices FlowFuse supports managing Node-RED on your own hardware. - [Getting started with Devices](https://flowfuse.com/docs/device-agent/introduction) ## Working with Teams - [Team management](https://flowfuse.com/docs/user/team/) - How to add and remove users from a team. - [Role based access control](https://flowfuse.com/docs/user/team/#role-based-access-control) - Which privileges are granted to different roles. ## Working with Files and Context FlowFuse supports reading and writing persistent files and persistent context. - [Working with Files](https://flowfuse.com/docs/user/filenodes) - [Working with the Static Asset Service](https://flowfuse.com/docs/user/static-asset-service) - [Working with Context](https://flowfuse.com/docs/user/persistent-context) ## Working with Instances - [Dashboards](https://flowfuse.com/docs/user/dashboards) - View and switch between your Node-RED Dashboards from one place. - [Instance States](https://flowfuse.com/docs/user/instance-states) - List of states an instance can be in. - [Snapshots](https://flowfuse.com/docs/user/snapshots) - Create point-in-time backups of your Node-RED instances. - [Environment Variables](https://flowfuse.com/docs/user/envvar) - How to manage Environment Variables in your Node-RED instances. - [Change Project Stack](https://flowfuse.com/docs/user/changestack) - How to change an instance stack, for example to upgrade Node-RED. - [Logs](https://flowfuse.com/docs/user/logs) - The Logs available in the FlowFuse application. - [Project Link Nodes](https://flowfuse.com/docs/user/projectnodes) - Custom nodes for sending messages between Node-RED instances and devices. - [MQTT Nodes](https://flowfuse.com/docs/user/mqtt-nodes) - Custom nodes for zero config MQTT integration with the team broker. - [Instance Settings](https://flowfuse.com/docs/user/instance-settings) - Settings available for Node-RED instances. - [Shared Team Library](https://flowfuse.com/docs/user/shared-library) - Share flows easily between different Node-RED instances in your team. - [Node-RED Tools Plugin](https://flowfuse.com/docs/migration/node-red-tools) - A plugin for Node-RED that lets you work with your flows outside of FlowFuse. - [High Availability mode](https://flowfuse.com/docs/user/high-availability) - Run multiple copies of your instance for scaling and availability. - [FlowFuse Expert](https://flowfuse.com/docs/user/expert/) - A Node-RED plugin powered by AI, trained on FlowFuse content, that helps you code faster, build flows, and debug with context-aware guidance. ## Working with MQTT - [Team Broker](https://flowfuse.com/docs/user/teambroker) - Working with the FlowFuse bundled MQTT Broker ## Working with Custom Nodes - [Custom Nodes](https://flowfuse.com/docs/user/custom-npm-packages) - Publishing Custom Node-RED Nodes ## Working with FF Tables - [FF Tables](https://flowfuse.com/docs/user/ff-tables) - Databases # Logs FlowFuse presents log information in several different places depending on what you are interested in. ## Node-RED Logs The Node-RED logs are available for all instances running within the platform. They will contain information such as nodes being added and errors relating to your flows. The log information is kept back to the last time the instance container was restarted, you can view older information on the `Load earlier...` link at the top of the log. ![](https://flowfuse.com/docs/user/images/projectlog.png){width="500"} Node-RED logs can also be output from the Containers/Pods that run Instances on Docker or Kubernetes. This is enabled by the `forge.logPassthrough` option. More details can be found in the [Configuration](https://flowfuse.com/docs/install/configuration) documentation. ## Audit Log The Audit Log tab on the application and instance views shows key events that have happened. The events include: - User logging into the editor - Flows being updated - Nodes installed - Snapshots being created - Resource utilization warnings This log contains all events since the instance was created. You can view older data using the `Load More...` link at the bottom of the log. ![](https://flowfuse.com/docs/user/images/projectactivity.png){width="500"} ### Resource utilization warnings If the CPU or memory usage exceeds 75% for more than 5 minutes, a warning will be displayed in the Audit Log, indicating that measures such as upgrading the instance are recommended. ## Team Audit Log From the Team page the Audit Log shows events relating to the management of the team. This includes: - Applications/Instances being created or deleted - Users being added/removed from the team This log contains all events since the team was created. Tou can view older data using the `Load More...` link at the bottom of the log. ![](https://flowfuse.com/docs/user/images/teamauditlog.png){width="500"} ### AI Agents and API Activity For actions performed by an AI agent or through the FlowFuse platform API, both this log and the instance/application-level [Audit Log](https://flowfuse.com/#audit-log) show an icon indicating how the action was performed: - A sparkle icon means an AI agent performed the action on the user's behalf. Hovering over the icon says which agent and names the tool that was called: "via Expert" for FlowFuse Expert, and "via MCP" for [your own connected agent](https://flowfuse.com/docs/user/expert/third-party-agents/). - A terminal icon means the action was performed through the FlowFuse API using a [Personal Access Token](https://flowfuse.com/docs/user/user-settings#personal-access-tokens). Actions performed directly through the FlowFuse UI do not show an icon. These logs provide visibility into supported AI agent and API actions only. # FlowFuse MQTT Nodes Node-RED instances running within FlowFuse include a set of nodes that make it simple to securely connect and send or receive messages between MQTT clients in your team or externally connected clients via the [Team Broker](https://flowfuse.com/docs/user/teambroker/#getting-started-with-team-broker). The nodes are very similar to the Node-RED MQTT nodes, but without the need to configure any settings for your broker, making integration seamless. ### Nodes There are two nodes in this collection: - `MQTT In` - subscribes to fixed or dynamic topics - `MQTT Out` - publishes messages to fixed or dynamic topics The `MQTT In` node receives messages from the topic defined in the node's configuration or the `msg.topic` property if it is set. The `MQTT Out` node sends the `msg.payload` value to the topic defined in the node's configuration or the `msg.topic` property if it is set. See the built-in help on the Node-RED sidebar for more information about using these nodes. ### GitHub The nodes are published under an Apache-2.0 license and available on [GitHub](https://github.com/FlowFuse/nr-mqtt-nodes){rel=""nofollow""}. # FlowFuse Persistent Context Some Node-RED flows require the ability to persist context values between restarts and FlowFuse stack updates. By default, context data in Node-RED is ephemeral, meaning it does not survive restarts or stack updates. With FlowFuse's paid plans, however, you can enable **persistent context storage**, ensuring that your context values persist across restarts, upgrades, and more. ## Usage In Node-RED with FlowFuse, you now have two context store options: 1. **Memory Context**: This is the default ephemeral context. Values stored in memory are not persistent and will be lost when Node-RED restarts or the Node-RED stack is updated. 2. **Persistent Context**: This allows context values to persist even when Node-RED restarts, updates. The amount of persistent storage available to you depends on the FlowFuse plan you're subscribed to. FlowFuse offers different storage sizes for each plan, allowing you to select the appropriate level of storage for your needs. For detailed information on storage options, please refer to the [pricing page](https://flowfuse.com/pricing/){rel=""nofollow""}. ### How to Use FlowFuse Persistent Context Using persistent context in Node-RED is similar to using memory context, with the key difference being that you specify the storage type for your context data. Here’s how to use persistent context it: #### Using Persistent Context in Nodes When configuring persistent context in different nodes (e.g., Change, Inject, or Switch nodes), you can select the type of context storage to use. By default, the context type is set to **Memory**, but you can change it to **Persistent** to store values across restarts. - **Change Node, Inject Node, Switch Node**: :br When you configure these nodes to store or access context data, you’ll notice a storage option at the right corner. By default, it will be set to **Memory**. To make the context **persistent**, simply switch the selection to **Persistent**. ![Persistent Store Option in Change Node](https://flowfuse.com/docs/user/images/variables-in-node-red-change-node-persistent-store-option.gif) #### Using Persistent Context in Function Nodes In **Function nodes**, you interact with context data using the `set` and `get` methods. These methods allow you to specify where the context data should be stored or accessed from. - **Setting Context**: To set a persistent context value, use the `set` method with three arguments. The third argument specifies the context store, which should be set to **persistent**. For example: ```javascript context.set('myKey', 'myValue', 'persistent'); ``` This argument is optional and defaults to memory. This means that if you leave it empty, it will use memory as the context store. - **Getting Context**: When retrieving persistent context, use the `get` method with two arguments. The second argument is optional and specifies the context store (either memory or persistent). Example: ```javascript var value = context.get('myKey', 'persistent'); ``` If you don't specify the store, it defaults to memory. For more detailed information, refer to the article [Understanding Node, Flow, Global, and Environment Variables in Node-RED](https://flowfuse.com/blog/2024/05/understanding-node-flow-global-environment-variables-in-node-red/){rel=""nofollow""}. # FlowFuse Project Nodes Node-RED instances running within FlowFuse include a set of nodes that make it very quick and easy to securely send and receive messages between different instances in a team. The nodes act in a similar way to the Node-RED Link nodes, but by allowing the links to extend between different instances, they open up a wide range of possibilities. Remote instances can take part too, with the limitations described below. For example, a single Node-RED instance may contain a set of utility flows that you want to reuse in other instances. Rather than copy the flows around, the Project Nodes allow you to easily call those flows and get the result back. The flows stay in one place, so a change to them applies to every caller at once, with the trade-offs covered in [Limitations](https://flowfuse.com/#limitations). The project nodes are only available in the Enterprise tier of FlowFuse. ### Nodes There are three nodes in this collection: - `Project In` - listens for messages being broadcast by other Node-RED instances, or for messages being sent just to this instance - `Project Out` - sends messages to other Node-RED instances - `Project Call` - sends messages to other Node-RED instances and waits for a response The nodes send the whole `msg` object. Due to the way the nodes encode messages, there are some data types that do not get sent. For example, the `msg.req`/`msg.res` properties used by the core HTTP nodes will not be sent. Instead, they are temporarily removed from the message and re-attached when the message is received back. Each node is configured with a topic on which it either sends or receives messages on. This is similar in concept to MQTT topics - although the nodes do not currently support using MQTT wildcards in their topics. The Project Out nodes can either broadcast messages on a topic to anyone listening, or they can send messages on a topic to a specific other instance. The Project In nodes do the opposite - they can either listen for messages being broadcast, or for messages sent directly to them. The Project Call node can be used to send a message to another Project In node and then wait for a response, with a built-in timeout if it doesn't arrive. The response is sent back using a Project Out node configured to respond to the call node. ### Limitations **Only hosted instances can be addressed directly.** The list of targets and sources offered by the nodes is the hosted instances in your team. This applies to the `Project Out` node when sending to a specific instance, the `Project In` node when listening to a specific source, and the `Project Call` node's target. **A remote instance can send, but cannot be sent to.** Flows on a remote instance can use `Project Out` and `Project Call` to reach a hosted instance, and can receive broadcasts. They cannot be named as a target, so shared logic being called by others has to live on a hosted instance. A remote instance assigned to an application cannot receive direct messages at all, and can only listen for broadcasts. **Calls need the target to be up.** The `Project Call` node waits for the default timeout of 30 seconds, then logs an error that can be caught with a Catch node. The target instance must be running and have a `Project In` node listening on the same topic. Because every call travels through the platform's broker, logic called this way is unavailable to a caller whose connection to the platform is down. **Load is shared only in HA mode.** The instance holding the called flows serves every caller from a single Node-RED runtime. In [High Availability mode](https://flowfuse.com/docs/user/high-availability) the nodes automatically switch to MQTT shared subscriptions, so calls are distributed between the copies. ### GitHub The nodes are published under an Apache-2.0 license and available on [GitHub](https://github.com/FlowFuse/nr-project-nodes){rel=""nofollow""}. # Role-Based Access Control Role-based access control (RBAC) determines what actions users can perform within FlowFuse. By assigning roles to team members, you control who can create, modify, view, or delete resources. ## RBAC Levels FlowFuse provides role-based access control at two levels: 1. **Team-Level RBAC** - Defines default permissions across all team resources 2. **Application-Level RBAC** - Overrides team-level permissions for specific applications ## Team-Level RBAC Team-level roles establish baseline permissions for all resources within a team. Every team member is assigned one of four roles. ### Roles **Owner**:br Full administrative control over the team, including managing settings, members, and all resources. **Member**:br Can develop and manage flows, create snapshots, and modify environment variables. Cannot manage team settings or create/delete applications and instances. **Viewer**:br Read-only access to view flows, instance details, and snapshots. Cannot make any modifications. **Dashboard Only**:br Restricted access limited to viewing dashboards and HTTP endpoints only. ### Permissions The table below shows which actions each role can perform. | Action | Owner | Member | Viewer | Dashboard Only | | --------------------------------- | ----- | ------ | ------ | -------------- | | **Team Management** | | | | | | Manage Team Settings | ✓ | - | - | - | | View Team Audit Log | ✓ | - | - | - | | Invite User | ✓ | - | - | - | | Change User Role | ✓ | - | - | - | | Remove User from Team | ✓ | §1 | §1 | §1 | | **Applications** | | | | | | Create Application | ✓ | - | - | - | | Delete Application | ✓ | - | - | - | | Modify Application Settings | ✓ | - | - | - | | View Application Logs | ✓ | ✓ | ✓ | - | | **Instances** | | | | | | Create Instance | ✓ | - | - | - | | Delete Instance | ✓ | - | - | - | | Copy Instance | ✓ | - | - | - | | View Instance Details | ✓ | ✓ | ✓ | - | | Start, Stop, Suspend Instance | ✓ | - | - | - | | Modify Instance Settings | ✓ | - | - | - | | Modify Environment Variables | ✓ | ✓ | - | - | | Manage Assets | ✓ | ✓ | - | - | | View Node-RED Logs | ✓ | ✓ | ✓ | - | | Access Dashboard or HTTP Endpoint | ✓ | ✓ | ✓ | ✓ | | **Flows** | | | | | | Access Flow Editor | ✓ | ✓ | ✓ | - | | Modify Flows | ✓ | ✓ | - | - | | **Snapshots** | | | | | | Create Snapshot | ✓ | ✓ | - | - | | Restore Snapshot | ✓ | ✓ | - | - | | Set as Device Target | ✓ | ✓ | - | - | | View Snapshots | ✓ | ✓ | ✓ | - | | Download Snapshot | ✓ | ✓ | - | - | | Upload Snapshot | ✓ | - | - | - | | Delete Snapshot | ✓ | - | - | - | | **Devices** | | | | | | View Devices | ✓ | ✓ | ✓ | - | | Modify Device Settings | ✓ | - | - | - | | Modify Environment Variables | ✓ | ✓ | - | - | | Assign to/Remove from Application | ✓ | - | - | - | | Assign to/Remove from Instance | ✓ | - | - | - | | Delete Device | ✓ | - | - | - | | Bulk Move Devices | ✓ | - | - | - | | Bulk Delete Devices | ✓ | - | - | - | | **Team Library** | | | | | | Add an Item | ✓ | ✓ | - | - | | Modify an Item | ✓ | ✓ | - | - | | Delete an Item | ✓ | ✓ | - | - | | **Team Broker** | | | | | | Create Client | ✓ | ✓ | - | - | | Delete Client | ✓ | ✓ | - | - | | List Clients | ✓ | ✓ | - | - | **Notes:** - §1 Users in any role can remove themselves from a team - Platform Administrators have owner-level access to all teams but cannot access the Flow Editor ### Managing Team-Level Roles Team Owners can manage member roles from the **Team Members** page. #### Setting Roles When Inviting Members When inviting a new team member: 1. Navigate to the Team Members 2. Click **Invite Member** ![Invite team member popup](https://flowfuse.com/docs/user/images/invite-members.png){dataZoomable=""} 3. Enter the user's username or email address 4. Select the initial role (Owner, Member, Viewer, or Dashboard Only) ![Select role while inviting](https://flowfuse.com/docs/user/images/invite-popup.png){dataZoomable=""} 5. Send the invitation The invited user will have the assigned role once they accept the invitation. #### Changing Existing Member Roles To change a team member's role: 1. Navigate to the Team Members page 2. Locate the user whose role you want to change 3. Click the three-dot icon next to their username 4. Select **Change Role** ![Change member role](https://flowfuse.com/docs/user/images/change-role.png){dataZoomable=""} 5. Choose the new role from the popup (similar to the invitation process) 6. Confirm the change **Note:** An Owner can only change their own role if at least one other Owner exists on the team. ## Application-Level RBAC Application-Level RBAC enables you to control permissions at the individual application level within a team. This allows different team members to have different permission levels for different applications without creating multiple teams. This is an Enterprise lisenced feature for Self Hosted Users and requires an Entprise Team on FlowFuse Cloud. ### Overview Team-level roles define default permissions across all resources. :br Application-level roles override these defaults for specific applications. When you assign an application-level role to a team member, it takes precedence over their team-level role **only** for that application. Their team-level role applies to all other applications. ### Available Roles Application-level roles follow the same structure: - **Owner** – Full control over the application - **Member** – Can develop and manage flows, create snapshots, modify environment variables - **Viewer** – Read-only access - **Dashboard Only** – Can only view dashboards and HTTP endpoints ### Permission Hierarchy 1. If a user has an application-level role, that determines their permissions for the application. 2. If not, their team-level role applies. 3. Team Owners always have full access to all applications. **Example:**:br A team-level *Member* is assigned *Viewer* permissions for one production application. They can only view flows in that application, but retain normal Member permissions for all others. ### Configuring Application-Level Roles Team Owners can configure application-level roles: 1. Navigate to the application 2. Open **Application Settings** → **User Access** 3. Click the three-dot icon next to the user and select **Edit Permission** ![Application user access settings](https://flowfuse.com/docs/user/images/application-rbac.png){dataZoomable=""} 4. In the popup, assign the desired application-level role ![Application RBAC popup](https://flowfuse.com/docs/user/images/application-rbac-popup.png){dataZoomable=""} 5. Changes apply immediately To remove an application-level assignment, simply clear the role. The user will fall back to their team-level role for that application. # Shared Team Library Node-RED allows you to import and export **flows** and **functions** to a local library. This is helpful for saving pieces of flow that you want to reuse. With FlowFuse Premium, each Node-RED instance has access to a Team Library that makes it very easy to share flows and functions without having to manually copy them around. For example, you may have a standard set of flows that you want each Node-RED instance to include. By exporting them to the Team Library, you can quickly import them wherever you want to use them. A short video is available on [how to use this feature](https://www.youtube.com/watch?v=B7XK3TUklUU){rel=""nofollow""}. ### Exporting flows To export flows to the library within the Node-RED editor: 1. Select the nodes to be included 2. Open the Export Dialog (`Main Menu -> Export`, or `Ctrl/Cmd-E`) 3. Select the `Team Library` tab 4. Enter a name for the library entry 5. Click Export ### Importing flows To import flows within the Node-RED editor: 1. Open the Import Dialog (`Main Menu -> Import`, or `Ctrl/Cmd-I`) 2. Select the 'Team Library' tab 3. Select the flow to import 4. Click Import 5. Place the imported nodes within your flows ![](https://flowfuse.com/docs/user/images/shared-lib-import.png) ### Viewing Team Library It is possible to explore your Team Library within FlowFuse by clicking "Library" in your Team options. You can inspect the contents of any `.json` flow file, or `.js` function file here too. # Snapshots ## Introduction A Snapshot is a point-in-time backup of a Node-RED instance. It captures: 1. The flows 2. Credentials 3. Environment variables 4. NPM packages, with locked versions 5. Runtime settings. ## In this document - [Application Snapshots Overview](https://flowfuse.com/#application-snapshots-overview) - An overview of all snapshots belonging to an application and the available actions - [Instance Snapshots Overview](https://flowfuse.com/#instance-snapshots-overview)- An overview of all snapshots belonging to an instance - [Snapshots](https://flowfuse.com/#snapshot-list) - A list of all snapshots belonging to an instance and the available actions - [Timeline](https://flowfuse.com/#timeline-view) - A visual timeline of instance changes and snapshots interleaved - [Device Snapshots Overview](https://flowfuse.com/#device-snapshots-overview) - An overview of all snapshots belonging to a device and the available actions - [Create a snapshot](https://flowfuse.com/#create-a-snapshot) - Create a snapshot of a device or an instance - [Restore Snapshot](https://flowfuse.com/#restore-a-snapshot) - Apply a snapshot to the runtime of a device or an instance - [Edit a snapshot](https://flowfuse.com/#edit-a-snapshot) - Edit the name and description of a snapshot - [Upload a snapshot](https://flowfuse.com/#upload-a-snapshot) - Upload a snapshot to a device or an instance - [Download a snapshot](https://flowfuse.com/#download-a-snapshot) - Download a snapshot to your local machine - [Delete a snapshot](https://flowfuse.com/#delete-a-snapshot) - Delete a snapshot - [Set Device Target (instance)](https://flowfuse.com/#instance-owned-devices) - Set a snapshot as the target for all devices belonging to an instance - [Creating a Snapshot from a device](https://flowfuse.com/#creating-a-snapshot-from-a-device) - Create a snapshot from the device overview page - [Creating a Snapshot from within a device](https://flowfuse.com/#creating-a-snapshot-locally) - Create a snapshot from within Node-RED - [Auto Snapshots](https://flowfuse.com/#auto-snapshots) - Automatically create snapshots when flows are deployed - [Previewing Snapshots](https://flowfuse.com/#previewing-snapshots) - Preview the flows of a snapshot ## Snapshot Views: ### Application Snapshots Overview All snapshots belonging to the instances and devices of an application are gathered and presented in a single list where you can perform the following actions: - [Edit a snapshot](https://flowfuse.com/#edit-a-snapshot) - Edit the name and description of a snapshot - [View Snapshot](https://flowfuse.com/#previewing-snapshots) - Preview the flows of a snapshot - [Compare Snapshot](https://flowfuse.com/#comparing-snapshots) - Compare the snapshot with another snapshot - [Download Snapshot](https://flowfuse.com/#download-a-snapshot) - Download the snapshot to your local machine - [Delete Snapshot](https://flowfuse.com/#delete-a-snapshot) - Delete the snapshot ![Application Snapshots](https://flowfuse.com/docs/user/images/snapshots/application-snapshots.png)*Screenshot showing Applications Snapshot list* ### Instance Snapshots Overview Snapshots belonging to an instance are presented as a list or a visual timeline: #### Snapshot List Snapshots belonging to an instance are gathered and presented in a single list where you can perform the following actions: - [Upload Snapshot](https://flowfuse.com/#upload-a-snapshot) - Upload a snapshot to the instance - [Create Snapshot](https://flowfuse.com/#create-a-snapshot) - Create a snapshot of the instance - [Restore Snapshot](https://flowfuse.com/#setting-a-device-target-snapshot) - Restore the snapshot to the instance - [Edit Snapshot](https://flowfuse.com/#edit-a-snapshot) - Edit the snapshot name and description - [View Snapshot](https://flowfuse.com/#previewing-snapshots) - Preview the snapshot flows - [Compare Snapshot](https://flowfuse.com/#comparing-snapshots) - Compare the snapshot with another snapshot - [Download Snapshot](https://flowfuse.com/#download-a-snapshot) - Download the snapshot to your local machine - [Set as Device Target](https://flowfuse.com/#setting-a-device-target-snapshot) - Set the snapshot as the device target snapshot - [Delete Snapshot](https://flowfuse.com/#delete-a-snapshot) - Delete the snapshot ![Instance Snapshots](https://flowfuse.com/docs/user/images/snapshots/instance-snapshots.png)*Screenshot showing Instance Snapshot list* #### Timeline View The timeline view shows a visual representation of changes made to an instance with available snapshots interleaved to signify when they were created. Any snapshots displayed inline on the timeline will have the same actions available as in the Snapshot List view above. ![Visual Timeline](https://flowfuse.com/docs/user/images/snapshots/instance-timeline.png)*Screenshot showing Instance Visual Timeline* ### Device Snapshots Overview Snapshots belonging to a device are presented in a single list where you can perform the following actions: - [Upload Snapshot](https://flowfuse.com/#upload-a-snapshot) - Upload a snapshot to the instance - [Create Snapshot](https://flowfuse.com/#create-a-snapshot) - Create a snapshot of the instance - [Restore Snapshot](https://flowfuse.com/#setting-a-device-target-snapshot) - Set the snapshot as the devices target snapshot - [Edit Snapshot](https://flowfuse.com/#edit-a-snapshot) - Edit the snapshot name and description - [View Snapshot](https://flowfuse.com/#previewing-snapshots) - Preview the snapshot flows - [Compare Snapshot](https://flowfuse.com/#comparing-snapshots) - Compare the snapshot with another snapshot - [Download Snapshot](https://flowfuse.com/#download-a-snapshot) - Download the snapshot to your local machine - [Delete Snapshot](https://flowfuse.com/#delete-a-snapshot) - Delete the snapshot ![Device Snapshots](https://flowfuse.com/docs/user/images/snapshots/device-snapshots.png)*Screenshot showing Device Snapshot list* ## Snapshot Actions ### Create a snapshot To create a snapshot: 1. Go to the device or instance's page and select the **Snapshots** tab. 2. Click the **Create Snapshot** button. 3. You will be prompted to give the snapshot a **name** and optional **description**. 4. Click **Create** The list of snapshots will update with the newly created entry at the top. ### Restore a snapshot To restore a snapshot: 1. Go to the desired device or instance page and select the **Snapshots** tab. 2. Open the dropdown menu to the right of the snapshot you want to restore and select the **Restore Snapshot** option. 3. You will be asked to confirm - click **Confirm** to continue. ### Edit a snapshot To edit a snapshot: 1. Go to the instance's page and select the **Snapshots** tab. 2. Open the dropdown menu to the right of the snapshot you want to edit and select the **Edit Snapshot** option. 3. Update the name and description as required. 4. Click **Update** NOTE: Changes made to a snapshot will not be immediately reflected in the Node-RED runtime already running this snapshot. ### Upload a snapshot A snapshot can be uploaded to an device or instance from your local machine. To upload a snapshot: 1. Go to the desired instance or device overview page and select the **Snapshots** tab. 2. Click the **Upload Snapshot** button. 3. Select the snapshot file from your local machine. 4. Update the name and description if required. 5. Select the components to upload: - **Flows**: Include the snapshots flows - **Credentials**: Include the snapshots flows credentials (visible only if the snapshot contains credentials) - **Environment Variables**: Include environment variables in the snapshot - **Keys and Values**: Include the keys and values of the environment variables - **Keys Only**: Include only the keys of the environment variables 6. If the snapshot contains credentials and the `Credentials` component is checked, you will be asked to enter a Secret. This will be used to later decrypt any credentials in the snapshots flows. 7. Click **Upload** ### Download a snapshot A snapshot can be downloaded to your local machine for backup or sharing. To download a snapshot: 1. Go to the desired application, instance or device overview page and select the **Snapshots** tab. 2. Open the dropdown menu to the right of the snapshot you want to download and click the **Download Snapshot** option to open the download dialog. 3. Select the required components to download. - **Flows**: Include the snapshot flows - **Credentials**: Include the snapshot flows credentials - **Environment Variables**: Include environment variables in the snapshot - **Keys and Values**: Include the keys and values of the environment variables - **Keys Only**: Include only the keys of the environment variables 4. Enter a secret to encrypt any credentials in the snapshot (optional, depends on components selected). 5. Click **Download** ### Delete a snapshot To delete a snapshot: 1. Go to the instance's page and select the **Snapshots** tab. 2. Open the dropdown menu to the right of the snapshot you want to delete and select the **Delete snapshot** option. 3. You will be asked to confirm - click **Delete** to continue. *Note:* If the snapshot is the current **Device Target** snapshot, this will cause any connected devices to stop running the snapshot when they next check in. ### Setting a Device Target snapshot Snapshots are used to identify a version of the Node-RED instance that should be pushed out to any connected devices. This allows you to develop your flows in FlowFuse and only push out to the devices when it is ready. #### Instance owned devices To set the **Device Target** of an instance owned device: 1. Go to the instance's page and select the **Snapshots** tab. 2. Open the dropdown menu to the right of the snapshot you want to set as the device target and select the **Set as Device Target** option. 3. You will be asked to confirm - click **Set Target** to continue. This will cause the snapshot to be pushed out to any connected devices the next time it checks in. #### Application owned devices To set the **Device Target** of an application owned device: 1. Go to the devices's page and select the **Snapshots** tab. 2. In the list of snapshots available, a "Restore Snapshot" button will be displayed for each snapshot as you hover over it. 3. You will be asked to confirm - click the **Confirm** button to set it as the target snapshot. This will cause the snapshot to be pushed out to the device the next time it checks in. ### Creating a Snapshot from a device It is possible to create a Snapshot from a device that is connected to the platform. The device must be set to Developer Mode for this to work. See [Working with Devices](https://flowfuse.com/docs/device-agent/deploy) for more information. ### Creating a Snapshot locally Using the [Node-RED Tools Plugin](https://flowfuse.com/docs/migration/node-red-tools) it is also possible to create Snapshots in a local copy of Node-RED and push them back into your FlowFuse managed Node-RED instances. For more information, see the [Node-RED Tools Plugin guide](https://flowfuse.com/docs/migration/node-red-tools). ### Auto Snapshots FlowFuse can automatically create snapshots whenever flows are deployed to the instance. This is useful for tracking changes, and rolling back. FlowFuse will label these snapshots as "Auto snapshot - yyyy-mm-dd hh\:mm\:ss". A limit of 10 auto snapshots will be kept, with the oldest being deleted when a new one is created. Devices can optionally disable auto snapshots, in the developer mode tab. This can be helpful to avoid excessive data usage when a device is in the field or on a cellular connection. NOTE: This feature is only available to Enterprise tier teams ### Previewing Snapshots From any Snapshots tab, you can preview the flows of a snapshot by selecting the Snapshot's actions, and selecting "View Snapshot". ![Screenshot to show the available "Actions" for a given Snapshot](https://flowfuse.com/docs/user/images/snapshots-actions.png)*Screenshot to show the available "Actions" for a given Snapshot* ![Screenshot to an example flow preview for a Snapshot in FlowFuse](https://flowfuse.com/docs/user/images/snapshots-preview.png)*Screenshot to an example flow preview for a Snapshot in FlowFuse* ### Comparing Snapshots From any Snapshots tab, you can compare two snapshots by selecting the Snapshot's action, then selecting "Compare Snapshots". This will open a dialog where you choose a second snapshot to compare with. The comparison view has three panels: - **Left sidebar**— lists every node that differs between the two snapshots. Each entry shows a node-type badge (config node, tab, or regular node) and one of three statuses: - **Added** — the node exists in the newer snapshot but not the older one - **Deleted** — the node existed in the older snapshot but has been removed - **Changed** — the node exists in both snapshots but one or more properties differ - **Flow canvas** — highlights the selected node and scrolls to it automatically, giving a visual indication of where the change is in your flow - **Right panel** — shows the property and code changes for the selected node. Use the **Prev / Next** buttons to step through changes one at a time Selecting an entry in the sidebar highlights the corresponding node on the canvas. The highlight clears automatically when you select a different entry. There are two types of diff shown in the right panel: - **Property diffs** — each changed property is shown with the old and new value side by side, with red `-` for removed and green `+` for added ![Screenshot showing property-level diff with old and new values displayed side by side](https://flowfuse.com/docs/user/images/snapshots/snapshot-diff-prop-change.png)*Screenshot showing property-level diff with old and new values displayed side by side* - **Code diffs** — for multiline properties such as function and template nodes, changes appear as a line-level diff with red `-` for removed lines and green `+` for added, in the same format as a git diff. Use the **Prettify** and **Wrap** toggles to make large or nested values easier to read ![Screenshot showing a code diff for a function node with red and green line-level changes](https://flowfuse.com/docs/user/images/snapshots/snapshot-diff-code-change.png)*Screenshot showing a code diff for a function node with red and green line-level changes* Not every difference between two snapshots is meaningful. Computed layout properties such as group node `w` and `h` values are automatically excluded because they are recalculated by Node-RED and do not reflect intentional edits. Position-only changes (nodes that moved on the canvas but had no property edits) can be hidden when you want to focus on substantive changes. # Static asset service Our platform now includes a Static Asset Service, enabling you to manage files seamlessly within your hosted Node-RED instances. ## What is Static asset service? The Static Asset Service allows you to store files permanently within your FlowFuse Instances. Files that you upload, generate, or modify will remain accessible even after your session ends or the application restarts. ## Prerequisites ### FlowFuse Cloud - A Instance Stack with a launcher version of 2.8.0 or greater. - Enterprise Team Type. ### Self-Hosted This feature is available only on self-hosted Enterprise licensed versions of FlowFuse. ### Limitations - Uploaded file sizes must not exceed 5MB. - Unsupported characters (dependent on the operating system): - empty paths (eg: `/`, `//` or `\`, `\\`) - special chars: `*`, `:` - Team permissions required: owner / member ## Getting Started If the prerequisites are met, you will be able to use the Static Asset Service capabilities of FlowFuse Instances from two locations: - Instance Assets tab within the **Instance Details** page. ![Instance Details Page](https://flowfuse.com/docs/user/images/assets-tab-instance.png){dataZoomable=""} - Instance Assets tab within the **Immersive Editor**. ![Immersive Editor](https://flowfuse.com/docs/user/images/assets-tab-editor.png){dataZoomable=""} The following steps assume that you have navigated to one of these locations and have the assets tab opened. ### Files #### Uploading a File To upload a file, click the 'Upload' button, select your desired file, and click confirm. This will upload the file to the current storage folder. *Note: Uploading a file with the same name as an existing one will overwrite the existing file.* #### Deleting a File Navigate to the file you want to delete, click on the kebab menu (three vertical dots) associated with the file, select 'Delete File,' and confirm when prompted. #### Renaming a File Currently, this feature is not supported. To rename a file, upload the file with the desired name and delete the old one from the instance's Persistent Storage. *Caution: Renaming a file will also affect any linked files or relative paths to that file in the Node-RED instance.* ### Folders #### Creating a Folder To create a folder, click the 'New Folder' button, enter your desired folder name, and click confirm. This will create the folder inside the current directory. #### Deleting a Folder Navigate to the folder you want to delete, click on the kebab menu associated with the folder, select 'Delete Folder,' and confirm when prompted. *Caution: Deleting a folder that contains files or other folders will permanently delete all nested files and folders.* #### Renaming a Folder Navigate to the folder you want to rename, click on the kebab menu, select 'Edit Folder,' and confirm the new name when prompted. *Caution: Renaming a folder will also affect any linked files or relative paths of nested files or directories in the Node-RED instance.* #### Folder Navigation You can navigate through folder structures by clicking on any folder and return by using the Working Directory Breadcrumbs located at the top of the Search Files input. #### Folder Visibility Following the 2.9.0 release, you can set the folder's visibility using the Visibility selector found in the Navigation section. This means that users can set the visibility of their uploaded files to public and make them accessible outside the node-red instance itself. When setting a folder's visibility to public you are required to set a static file path on which the files will be served by your instance. ![static-assets-visibility-selector.png](https://flowfuse.com/docs/user/images/static-assets-visibility-selector.png){dataZoomable=""} ![static-assets-select-static-path.png](https://flowfuse.com/docs/user/images/static-assets-select-static-path.png){dataZoomable=""} ![static-assets-public-visibility.png](https://flowfuse.com/docs/user/images/static-assets-public-visibility.png){dataZoomable=""} Considerations: - Visibility and static path maps can be set on folders only. - Any change in visibility settings require an instance restart in order for the changes to take effect. ### How to use The following video is a quick demonstration on how to use assets inside a FlowFuse Node RED Instance: :video[ Your browser does not support the video tag.]{controls="true" width="800"} # Teams Teams are groups of users that collaborate on their Node-RED applications. ## Managing Teams ### Creating a New Team Users can create new teams. Creating a team requires a name and a unique url. The user who creates the team is added as its "owner". **Note:** Administrators might not [allow all users to create teams](https://flowfuse.com/docs/admin/introduction#admin-settings). FlowFuse Cloud does allow any user to create a new team. ### Adding Team Members Each user with "owner" permissions on a team can invite new members on the "Members" tab of the team page. Invitations can be sent to existing users on the platform by their username, or via email. Users have up to 7 days to accept, or decline, the invitation before it will expire. When inviting a member, you can assign them an initial role (Owner, Member, Viewer, or Dashboard Only). This role can be changed later by team owners. For more details on managing roles, see [Role-Based Access Control](https://flowfuse.com/docs/user/role-based-access-control). ### Removing Team Members Owners can remove a member from a team by clicking the dropdown menu next to the username and selecting `Remove from team`. The team member will no longer have access to any team data. ## Role-Based Access Control FlowFuse uses role-based access control to manage what users can do within teams and applications. For detailed information about roles, permissions, and access control at both team and application levels, see the [Role-Based Access Control documentation](https://flowfuse.com/docs/user/role-based-access-control). # Getting Started with Team Broker When FlowFuse is deployed with an Enterprise license from v2.11.0 onwards comes with the option to enable a MQTT broker for each Team. This is a single shared MQTT broker, but each team has their own separate topic space and the ability to provision credentials for clients. It removes the need to install and manage a broker of your own. ## Foreword FlowFuse offers zero config MQTT integration with the Team Broker via the [FlowFuse MQTT Nodes](https://flowfuse.com/docs/user/mqtt-nodes/) that greatly simplifies the whole process by removing the need for manual configuration. If you wish to continue using traditional MQTT clients, the below sections will guide you through the process of creating clients and connecting to the broker. ## Creating Clients When creating clients you can specify a username, it will prepended to the the Team's id e.g. `alice` will become `alice@32E4NEO5pY`. This username should also be used as the MQTT Client ID in order to connect to the broker. Examples of how to do this are in the [next section](https://flowfuse.com/#connecting-to-the-broker). ![Create Broker Client](https://flowfuse.com/docs/cloud/images/create-broker-client.png) ## Connecting to the Broker The broker for FlowFuse Cloud is available on `broker.flowfuse.cloud` and supports the following connection types: - MQTT on port `1883` - MQTT over TLS on port `8883` - MQTT over secure WebSockets on port `443` For Self Hosted instances, please ask your Administrator for hostname and ports. You can connect to the broker using any MQTT client, for example `mosquitto_sub` ```text mosquitto_sub -u "alice@32E4NEO5pY" -i "alice@32E4NEO5pY" -P "password" -h broker.flowfuse.cloud -t "#" ``` Please note that username **must** also be used for the client id to connect to the team broker. This does mean that each username/password can only be used with a single MQTT client at a time. Or in Node-RED as follows ![Node-RED MQTT Client Connection](https://flowfuse.com/docs/cloud/images/node-red-mqtt-connection.png) ![Node-RED MQTT Client Security](https://flowfuse.com/docs/cloud/images/node-red-mqtt-security.png) # User Settings Access the FlowFuse User settings by clicking on your username in the top right corner. ![User Settings](https://flowfuse.com/docs/user/images/user-settings.png) ## Settings In the Settings section, you can modify various characteristics of your personal user account: - **Username** - **Name** - **E-Mail** (modifiable only if SSO is disabled) - **Theme** - choose **Light**, **Dark**, or **System** for the FlowFuse interface. **System** follows your operating system setting and updates live when it changes. Your choice is saved per browser. This applies to the FlowFuse application only; the Node-RED editor and Dashboard manage their own themes separately. Additionally, you can select a default Team, especially useful if your account is associated with multiple Teams. ## Teams The Teams tab provides an overview of all teams associated with your User account. Here, you can also view and respond to open invitations. ## Security ### Change Password Under the Security section, you have the option to change your password. For this, you need to enter your current and new password. ### Two-Factor Authentication Two-factor authentication adds an extra layer of security to your account. It requires a second form of identification when logging in. Note that when signing in via your SSO provider, you will not be prompted for a two-factor authentication code. To set up Two-factor authentication, click on `Enable two factor authentication` and follow the instructions. ### Personal Access Tokens Personal Access Tokens are useful for interacting with [FlowFuse APIs](https://flowfuse.com/docs/api/){rel=""nofollow""}. You can set these tokens to have a limited or unlimited lifespan. Tokens can be revoked at any time by removing them from your account. Remember, tokens with an expiry date will automatically delete upon reaching that date. It's important to note that the token value is only displayed once, at the time of creation. There is no way to retrieve the token value after this point. ### Creating a Token 1. Click **Add Token**. 2. Enter a name for the token. 3. Optionally, tick **Add Expiry Date** and choose a date. 4. Configure the token's scope (see below). 5. Click **Create**. ![Creating a new scoped Personal Access Token](https://flowfuse.com/docs/user/images/scoped-pat.png){dataZoomable=""} ### Scoping a Token - **Read Only** - restricts the token to read-only operations. Write operations, such as creating, updating, or deleting resources, are denied. - **Team Scope** - limits the token to specific Teams. If no Teams are selected, the token can access every Team you belong to, including Teams you join in the future. If specific Teams are selected, the token does not automatically gain access to any new Team you join later - you need to edit the token to add it. - **Admin Access** - only shown if your account has admin privileges. A token does not carry admin privileges by default, even if your account does; this must be enabled explicitly for that token. These restrictions can only narrow a token's access - a token can never do more than your own account is permitted to do. # Introducing FlowFuse Inc. When Dave and I first created [Node-RED](https://nodered.org){rel=""nofollow""}, it was a tool to solve a problem - allowing us to do our day job more effectively when building IoT solutions for clients. That gave us the means and purpose to create a truly useful platform. When it became an open source project, it quickly found an enthusiastic audience that has seen the community grow beyond our imagination. From both individual users, to a wide range of companies integrating it into their own products. But with that growth, the question in my mind has always been how to take it further and secure its long term future. I wrote on the [project blog](https://nodered.org/blog/2020/10/13/future-plans){rel=""nofollow""} last year about the future plans of the project. A key piece of that is its sustainability - how we can increase the commercial adoption of Node-RED and how we can get more people contributing back. An opportunity presented itself earlier this year that I believe will bring a step-change to what the Node-RED project is able to achieve. ### Introducing FlowFuse Inc. So today, I'm launching FlowFuse Inc - a new company whose mission is to build a low-coding development platform fit for the enterprise with Node-RED at its heart. Backed by [Sid Sijbrandij](https://www.linkedin.com/in/sijbrandij/){rel=""nofollow""}, we have funding in place to create a fully remote team dedicated to an open core model. In the short term, this means helping to accelerate the plans already in place for the Open Source project. Getting the 2.0 release done in the next few weeks, working on long-standing features such as the Test framework and Flow Debugger. Alongside that we'll also be building a platform around Node-RED that will make it easier to adopt at scale and integrate into existing enterprise environments. Node-RED remains a fully open source project, with its home at the OpenJS Foundation and an open governance model that allows anyone to have a say in its development. Our goal is to incorporate as much of our work directly into the core project as possible. Where we do create closed-source components, we will work with the community to ensure the right APIs and extension points are in the core for all to benefit from. We will only be successful if the whole Node-RED community is successful. For me personally, this is a really exciting next step. I never expected to turn my little side project into a full-time job and now into a company. ### Hiring soon! We'll be hiring soon - so keep an eye out if you're interesting in getting involved. # Welcome Ben I'm excited to share the news that Ben Hardill ([@hardillb](https://twitter.com/hardillb){rel=""nofollow""}) is joining FlowFuse as a Senior Engineer. I've known Ben for many years, having worked together at IBM. He's been an active member of the Node-RED community since the start of the project and he published one of the very first [3rd party nodes](https://flows.nodered.org/node/node-red-node-geofence){rel=""nofollow""}. He is ever-present on Stack Overflow, to the point where I've long since stopped rushing to respond to questions tagged with [`node-red`](https://stackoverflow.com/questions/tagged/node-red){rel=""nofollow""} in the full knowledge that he has usually beaten me to it. More recently he's been doing some really interesting work exploring [multi-tenant Node-RED systems](https://www.hardill.me.uk/wordpress/2020/10/01/multi-tenant-node-red/){rel=""nofollow""} - something we'll be continuing at FlowFuse. Welcome aboard Ben! # Community News January 2022 Welcome to the first FlowFuse newsletter, we’re going to publish this as a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community, if you want to receive it via email, sign up for updates at the bottom of the page. If you’ve got something that you’d like us to share please email . [FlowFuse 0.1 Released](https://flowfuse.com/blog/2022/01/flowforge-01-released/):br First up we are really pleased to ship the first version of our platform, this is a very early release but hopefully will give you an idea of the direction we’re going in. [Node-RED 2.2](https://nodered.org/blog/2022/01/27/version-2-2-released){rel=""nofollow""}:br The next version of Node-RED has been released with new editor features, predefined environment variables and improvements to some of the core nodes. Checkout the blog post and change log for more details. [Make your IoT data beautiful](https://blog.golioth.io/building-iot-dashboards-with-golioth-grafana-and-node-red){rel=""nofollow""}:br Ben Mawby wrote a guide on connecting Golioth's WebSocket endpoints to Grafana using Node-RED and InfluxDB [Alexa Voice Service on Node-RED](https://www.sammachin.com/posts/alexaweb-reborn){rel=""nofollow""}:br Sam Machin rebuilt an app on Node-RED allowing you to talk to Alexa through the browser. And he has published a new [node](https://flows.nodered.org/node/@sammachin/node-red-alexa-voice-service){rel=""nofollow""} to use the Alexa Voice Service within your own flows. [New Team Members](https://flowfuse.com/blog):br We welcomed 2 new members of the FlowFuse team this month [ZJ](https://flowfuse.com/blog/2022/01/welcome-zj/) joins as our CEO and [Steve](https://flowfuse.com/blog/2022/01/welcome-steve/) has come onboard to work on the Node-RED project [We are Hiring](https://boards.greenhouse.io/flowfuse/jobs/4312861004){rel=""nofollow""}:br We're looking for the next member of our team, If you're a Node.JS developer and want to work with us take a look at the link. # FlowFuse 0.1 released For an open core company, we haven't been very open with what we're doing. That all changes today with the release of FlowFuse 0.1 and making all of our repositories public. This is a significant step for the company as we look to build a platform around Node-RED. The main question we get asked is: 'What is FlowFuse?" - which is a very reasonable question to ask. FlowFuse is a platform for managing Node-RED instances at scale. It lets you have multiple users on the platform, organised into teams to provide proper access control to individual Node-RED instances, or Projects as we call them. With the 0.1 release, we have the basic building blocks of the platform in place. - Add multiple users to the platform - Create teams for those users - Create Node-RED projects quickly and easily through the platform UI For a more complete walk-through of the platform in this early release, you can watch this video. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: YYZDx8n17Ys --- :: ### Getting started with FlowFuse The documentation provides a guide for [installing FlowFuse on a local server](https://github.com/FlowFuse/flowfuse/tree/main/docs){rel=""nofollow""}. We also have drivers for deploying to Docker Compose and Kubernetes based environments to enable a larger scale of deployment. We'll have more documentation on those options in the near future. ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. ### What's next? With so much in the plan and lots of exciting features to come, we will have a regular cycle of releases every four weeks. So you can expect the next release, 0.2, on Thursday 17th February. As we're only at 0.1 today, there is a lot still to do. We will be following the principles of [Semantic Versioning](https://semver.org/){rel=""nofollow""} with our releases, but until we reach 1.0, there may be some disruptive changes along the way. Sign up to the mailing list below if you want to hear more about the work we're doing. # Welcome Steve We're continuing to grow the FlowFuse team with our latest hire; Steve McLaughlin who is joining our development team. Steve is a well known face in the Node-RED community. He is a regular contributor to the community forum, always happy to help users with their questions. He has published a number of very popular nodes, include [buffer-parser](https://flows.nodered.org/node/node-red-contrib-buffer-parser){rel=""nofollow""}, [cron-plus](https://flows.nodered.org/node/node-red-contrib-cron-plus){rel=""nofollow""} and [image-tools](https://flows.nodered.org/node/node-red-contrib-image-tools){rel=""nofollow""}. He has also made some significant contributions to the core of Node-RED, such as delivering MQTTv5 support and introducing the Monaco code editor into the heart of the editor. Steve joins us from a background in Industrial IoT in the automative manufacturing space - experience and knowledge that will be invaluable as we look to growing the FlowFuse platform. Steve will be focussed on the Node-RED side of our activities - helping to continue the ongoing development and growth of the open source project at the heart of what FlowFuse is about. Welcome aboard Steve! # Welcome ZJ This year the FlowFuse team will grow further, and I'm excited to announce our newest addition to the team; Zeger-Jan van de Weg ([@ZJvandeWeg](https://twitter.com/ZJvandeWeg){rel=""nofollow""}). Zeger-Jan, also known as ZJ, is joining FlowFuse as CEO. ZJ previously worked at GitLab where he helped build a vibrant open source community and we're thrilled to have him to continue this with the Node-RED community. Under his supervision the involvement of GitLab in the Git project grew significantly, allowing Git and GitLab to be successful together. This aligns well with our vision for FlowFuse: > We will only be successful if the whole Node-RED community is successful. As our first non-engineering hire, ZJ will be focussed on growing the business side of FlowFuse, working on marketing and ongoing business development. Welcome aboard ZJ! # Announcing FlowFuse Cloud As an open core company, anyone is free to [download and install](https://github.com/FlowFuse/flowfuse/tree/9219e81399eaf52fb0ee5573707a52f5520fbfdd/docs/install){rel=""nofollow""} our platform. In many cases this is a great solution, it allows for custom setups in your own environment. We know this isn't for everyone though, some people just want to start building with Node-RED without having to manage their servers. We are excited to announce FlowFuse Cloud, a hosted Node-RED as a service offering and today we are opening the waitlist. Our waitlist captures your email, and we'll reach out to you on that address once your account is created. ### Starting operations After having released v0.2 recently, we're now working on v0.3 that will include a user flow for [billing](https://github.com/FlowFuse/flowfuse/issues/224){rel=""nofollow""}. When that work has been done and deployed people on the waitlist will slowly be invited to the platform. Currently that's scheduled for April 1st, no joke, although it could happen either sooner or later. More details on the exact pricing will be availble nearer the time. Once you are invited you will be able to; - Create multiple Node-RED projects hosted on flowforge.cloud, - Invite team members to collaborate on those projects, - And many more features will automatically become availble with each new release. # FlowFuse 0.2 released Four weeks have passed since our initial release of FlowFuse, and we're happy to release v0.2 today as we continue moving forward and evolve the platform. There aren't lots of headline features in this release to tell you about as a lot of the work has been on the internals, as well as responding to some of the early feedback from the community. Features like improving the test framework, and building a database migration framework may not sound too exciting to the end user, but they are critical pieces when build a platform that needs to be stable and easy to upgrade. We've also been doing work to get our own instance of the platform running in the Cloud - and figuring out how to automate as much of that as possible. Aside from being a key way to test the platform, it helps validate the work we're doing for when others come to run it in that way. It also lays the ground work for our own cloud service we'll be sharing more about in the coming days. The full change-log for the core of the platform is available [on GitHub](https://github.com/FlowFuse/flowfuse/blob/v0.2.0/CHANGELOG.md){rel=""nofollow""}. But with a further 15 repositories containing different components, each with its own change-log, we're still thinking about how best to share a single view of the updates. ### Getting started with FlowFuse The documentation provides a guide for [installing FlowFuse on a local server](https://github.com/FlowFuse/flowfuse/tree/main/docs){rel=""nofollow""}. If you haven't played with FlowFuse 0.1 yet, here's a more complete walk-through of the platform: ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: YYZDx8n17Ys --- :: ### Upgrading FlowFuse If you installed FlowFuse 0.1 and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. ### What's next? Our regular release cycle puts the next release on Thursday 17th March. We've got some key features planned in this release around [Project Templates](https://github.com/FlowFuse/flowfuse/issues/141){rel=""nofollow""} and [Stacks](https://github.com/FlowFuse/flowfuse/issues/285){rel=""nofollow""} - which will underpin how you can customise Node-RED within FlowFuse. We'll also have some exciting news to share about our own hosted service you'll be able to sign-up for. Sign up to the mailing list below if you want to hear more about the work we're doing. # Using Node-RED to keep Solar PV afloat [Krisnan Ravichandran](https://www.linkedin.com/in/krishnanravichandran/){rel=""nofollow""} works for [spb sonne](https://www.sbp.solar){rel=""nofollow""}, a engineering consultancy for renewable energy. Within the company he is part of the engineering effort on the [Gömbhal](https://www.sbp.de/en/news/goembhal-sbp-sonnes-pioneering-floating-pv-system/){rel=""nofollow""} project, creating a floatation device for solar panels. In this post, he shares his experiences of using Node-RED. Currently there’s a prototype deployed in Hungary, while the company is located in Stuttgart: “We’re remotely monitoring the installation. There are over 40 sensors that all connect to an ADAM-6717, Compact Intelligent Gateway. When the data is acquired we leverage Node-RED flows that maintain the structure, monitor performance, and provide reporting back to our offices. Node-RED is embedded in the ADAM 6717, it was very new to me. I was already experienced in programming, mainly Python, and within a month I felt very comfortable and productive in Node-RED. Understanding programming is useful, though not a necessity. Building and improving flows I did on my own through trial-and-error, as well as a lot of times through help on the [Node-RED Forum](https://discourse.nodered.org/){rel=""nofollow""}. The community is helpful and welcoming to new users. Now I maintain multiple flows with very different purposes. Some track temperature, irradiation wind-speed, direction, tilt and wave height; to ensure the floating PV installation remains floating. Other sensors are connected to actuators through flows that control pressure. We do have some challenges; the ADAM 6717 contained an older version of Node-RED. This raised questions around security and maintenance, as our Node-RED version isn’t updated to a newer version in an easy manner. It also hampers training a bit because documentation might reference an API or node for a flow that’s just not the same on older versions. However, I’d choose Node-RED again, it’s well known as well as easy to learn. Furthermore I found it very versatile.” --- Thanks to Krisnan for sharing his story. If you have a Node-RED story for us to share, please get in touch via . # Welcome Joe Today we welcome Joe Pavitt ([@joepavitt3d](https://twitter.com/joepavitt3d){rel=""nofollow""}) as our new Head of UX & Design. This is a key role that will help deliver the awesome user experience of the FlowFuse platform. Joe has a passion for user experience, data visualisation and creativity in technology. He joins us having been at IBM for 9 years where he specialised in building bespoke, first of a kind experiences in IBM's Emerging Technology and Research teams. We worked together at IBM and I saw first-hand the range and quality of what he can do. I was super pleased when he agreed to join us and I look forward to seeing the real impact he'll have on what we're building. Welcome aboard Joe! # Community News February 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [Announcing FlowFuse Cloud](https://flowfuse.com/blog/2022/02/announcing-flowforge-cloud/):br We are excited to announce FlowFuse Cloud, a hosted Node-RED as a service offering and today we are opening the waitlist. [FlowFuse 0.2](https://flowfuse.com/blog/2022/02/flowforge-02-released/):br We continue to iterate with our 4 weekly releases of the FlowFuse platform. [Node-RED 2.2.2](https://discourse.nodered.org/t/node-red-2-2-2-released/58606){rel=""nofollow""}:br The 2.2 release of Node-RED (last month) has received 2 maintenance releases to fix bugs with duplicate wires in the editor and MQTT. [New Team Members](https://flowfuse.com/blog/2022/02/welcome-joe/):br FlowFuse is now up to 6 people, Joe Pavitt has joined the team [We still are Hiring](https://boards.greenhouse.io/flowfuse/jobs/4312861004){rel=""nofollow""}:br We're looking for the next member of our team, If you're a Node.JS developer and want to work with us take a look at the link. # FlowFuse 0.3 released The FlowFuse 0.3 release brings us closer to the launch of FlowFuse Cloud. Find out more about what's in this new release. This release of the FlowFuse platform brings some significant new features that will underpin more of what is to come. ### Project Stacks & Templates When we think about what a makes a Project inside FlowFuse, the simple answer is Node-RED. The more complete answer is: a version of Node-RED, a version of Node.js, some memory, some CPU and a bunch of Node-RED settings to customise the instance. In a platform like FlowFuse, it's important to have the tools to manage all of these things. This is where Project Stacks and Template come in. A Project Stack defines the underlying characteristics of the Node-RED process - or the container it is running in. For example, with our Local deployment model, it defines the version of Node-RED to use and how much memory the process should try to use. In our container based deployment models, the stack identifies the container to use for the project, along with memory and CPU limits. In a future release, this will be the way we will support upgrading the version of Node-RED a project is using - and doing so in a well managed way. An Administrator will be able to create a new Stack containing the new version of Node-RED. Project owners will then be able to update their projects to use the new Stack - at a time that is convenient to them. A Project Template is more about how the Node-RED instance is configured - exposing the options a user would traditional modify in their Node-RED settings file. With this release, we're not exposing a lot of settings as the focus has been more on the underlying Template concept. But it will be the basis for gradually exposing more options for customisation in the future. - [Epic #285 - Project Stacks](https://github.com/FlowFuse/flowfuse/issues/285){rel=""nofollow""} - [Epic #141 - Project Templates](https://github.com/FlowFuse/flowfuse/issues/141){rel=""nofollow""} ### Billing Integration With our open core philosophy, the heart of the FlowFuse platform is open source and available under the Apache 2 license for anyone to use. But the plan was always to have certain features that were licensed separately. This release brings the first of those features - Stripe Billing Integration. This feature brings the ability to require a Team to have a Stripe Billing agreement in place and to be able to charge on a per-project basis within that Team. Being able to charge is an important feature for any commercial platform, and with our own FlowFuse Cloud launching soon, we needed to get this feature in place today. We've structured the code in the repository and updated the LICENSE file to make it very clear what parts of the code base are *not* covered by the Apache 2 license. - [Epic #224 - Billing](https://github.com/FlowFuse/flowfuse/issues/224){rel=""nofollow""} ### Getting started with FlowFuse The documentation provides a guide for [installing FlowFuse on a local server](https://github.com/FlowFuse/flowfuse/tree/main/docs){rel=""nofollow""}. If you haven't played with FlowFuse yet, here's a more complete walk-through of the platform: ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: YYZDx8n17Ys --- :: ### Upgrading FlowFuse If you installed FlowFuse 0.1 or 0.2 and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. ### What's next? Our regular release cycle puts the next release on Thursday 14th April. We're still in planning stage for the release, but we'll also be beginning to invite people from the waiting list to sign-up to FlowFuse Cloud. For more information, check out the [annoucement blog post](https://flowfuse.com/blog/2022/02/announcing-flowforge-cloud/). You can also sign up to our general mailing list below if you want to hear more about the work we're doing. # Community News March 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [New Website](https://flowfuse.com):br Some of you may have noticed the new design on our website, this is the first step in our more refined corporate identity, the same look will be coming to the FlowFuse application [soon.](https://github.com/FlowFuse/flowfuse/issues/430){rel=""nofollow""}. [FlowFuse 0.3](https://flowfuse.com/blog/2022/03/flowforge-03-released/):br The latest release introduced 2 new concepts, Templates and Stacks, with these we are starting to show the value of the FlowFuse platform allowing you to easily manage the Node-RED settings and versions used by your projects. [Intro to JSON for Node-RED](https://www.opto22.com/support/resources-tools/videos/video-introduction-to-json-for-node-red/){rel=""nofollow""}:br Our friends over at Opto 22 published a handy guide to JSON and how it works in Node-RED, great introduction for new builders and a good refresher for seasoned pros. # FlowFuse 0.4 released This release of the FlowFuse adds a seemingly small, but significant new feature. With [Node-RED 3.0 fast approaching](https://nodered.org/about/releases/){rel=""nofollow""} we've been making sure we are ready to support this. ### Upgrading Node-RED The goal of FlowFuse is to be the best way to run Node-RED at any scale, whether that's many users or many instances. Node-RED is a constantly developing as a platform and therefore part of running Node-RED is also upgrading the version you are running. With the 0.4 release today we've made that super simple in FlowFuse. Last month we introduced the concept of [Project Stacks](https://flowfuse.com/docs/user/concepts/#stack). One of the key elements of a Stack was the version of Node-RED in use. Initially this may have seemed fairly basic, when you create a new project you usually want to use the latest version of Node-RED. However what happens when a new version is released and you have an existing project? Now you can change the stack that a project is running on, which in turn will change the version of Node-RED. This is a simple process from the project settings, it only requires a short period of downtime while the project restarts on the new stack, typically around 10-15 seconds. Our driver to get this feature into the 0.4 release is the approaching release of Node-RED 3.0, now we know that we can be ready to offer our users Node-RED 3.0 as soon as it is released. We will also be making available the Beta's of Node-RED 3.0 within FlowFuse Cloud, this becomes a great way to test out the new features without having to touch your own environments. - [Story #288 - Change Stacks](https://github.com/FlowFuse/flowfuse/issues/288){rel=""nofollow""} - [Docs](https://flowfuse.com/docs/user/changestack/) ### Environment Variables Another key new feature we are introducing is the ability to set and manage environment variables within your projects. Environment Variables are a key tool when building applications as they allow you to to separate the configuration of your system from the logic in the code. Even in Low-Code platforms this is an important design pattern. Environment variables are fully integrated into [Templates](https://flowfuse.com/docs/user/concepts/#template) that we introduced last month so they can be set both at the platform level or on an individual project. Our plans for the next release will make these even more useful as we introduce the ability to [duplicate a project](https://github.com/FlowFuse/flowfuse/issues/271){rel=""nofollow""} and then modify those variables for the new project. - [Story #225 - Project Environment Variables](https://github.com/FlowFuse/flowfuse/issues/225){rel=""nofollow""} - [Docs](https://flowfuse.com/docs/user/envvar/) ### There's more There are many more improvements in this release, such as the ability to [Set the timezone](https://github.com/FlowFuse/flowfuse/issues/239){rel=""nofollow""} your project is running in, we've also been iterating on our billing experience as we've welcomed the first paying customers to FlowFuse Cloud. Finally we're very happy that we've had our first external contribution to the code base, as an Open Core company we believe strongly that Open Source lives at the heart of everything we do. We would like to say a big thank-you to [Fakorede Damilola Idris](https://fakocodes.netlify.app/){rel=""nofollow""} for his work on fixing a [bug](https://github.com/FlowFuse/flowfuse/issues/424){rel=""nofollow""} in the UI. ### Getting started with FlowFuse The documentation provides a guide for [installing FlowFuse on a local server](https://flowfuse.com/docs/install/). If you haven't played with FlowFuse yet, here's a more complete walk-through of the platform: ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: YYZDx8n17Ys --- :: ### Upgrading FlowFuse If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. ### What's next? Our regular release cycle puts the next release on Thursday 12th May. We will be building on features in the last few releases around managing your projects and using templates, we're also setting the foundations of our work to [manage Node-RED on your own devices running at the Edge](https://github.com/FlowFuse/flowfuse/issues/446){rel=""nofollow""}. For more information, check out the [announcement blog post](https://flowfuse.com/blog/2022/02/announcing-flowforge-cloud/). You can also sign up to our general mailing list below if you want to hear more about the work we're doing. # FlowFuse is accepting customers now A year ago our CTO, Nick O'Leary, [introduced FlowFuse](https://flowfuse.com/blog/2021/04/first-deploy/). Since then major milestones have been achieved. As we grow as a company, important steps are taken. Today we make another very important step; we're accepting our first customers. A few weeks ago our product was nearly in a state where it could support customers, when that was the case, we [opened up our waitlist](https://flowfuse.com/blog/2022/02/announcing-flowforge-cloud/). The waitlist has been growing daily and now we're ready to start inviting those users onto the platform. The first lucky few users are being added today, and each working day for the foreseeable future we'll continue onboarding users. New users will receive an email from our team with their login details and can start creating new workflows with Node-RED minutes after. While the source code of FlowFuse is [available](https://github.com/FlowFuse/flowfuse){rel=""nofollow""}, there's a chance you're unfamiliar with what has been built around Node-RED. With FlowFuse, our intent is to build a platform to aid with colaboration of flows in Node-RED. The first steps to our vision include a managed Node-RED instance to connect virtually any online service. Multiple users will have access to the same flows and can collaborate. Further, once Node-RED 3.0 has been released, the platform will provide a pain-free way to upgrade and keep your projects up to date. There's many more exciting features right around the corner on our [roadmap](https://flowfuse.com/changelog/) # Community News April 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [FlowFuse 0.4](https://flowfuse.com/blog/2022/04/flowforge-04-released/):br The next release has enabled us to offer multiple versions of Node-RED along with adding more features to help you configure your Node-RED projects. [Node-RED 3.0.0-beta-1](https://discourse.nodered.org/t/node-red-3-0-0-beta-1-released/62124){rel=""nofollow""}:br The first beta for Node-RED 3.0.0 has been published, there's some exciting improvements on UI in the editor to make designing your flows even easier. [Node-RED Beta on FlowFuse](https://flowfuse.com/blog/2022/05/node-red-3-beta-stack/):br As promised in out 0.4 announcement, we've made the beta available to our users on FlowFuse as a separate stack, this is just one way that we're able to demonstrate the flexibility you get from running Node-RED on FlowFuse. [Cisco & Node-RED](https://developer.cisco.com/meraki/build/exploring-meraki-and-spark-apis-with-node-red/){rel=""nofollow""}:br The team at Cisco DevNet published some great guides on using Node-RED to manage both your Meraki Access points and to integrate with WebEx Teams, they've also produced a handy [starter guide](https://blogs.cisco.com/developer/helloworldlowcodenodered01){rel=""nofollow""} for those that are new to Node-RED [Node-RED Con 2022](https://nrcon.nodered.org){rel=""nofollow""}:br Our friends in the Node-RED Japan User Group have run a number of successful Node-RED conferences over the last few years. This year, we're joining forces with them to bring the event to a wider audience. The [Call for Papers](https://www.papercall.io/nrcon2022){rel=""nofollow""} is open now and we'd love to see your submissions. # FlowFuse 0.5 released The cycle continues with our next regularly scheduled release, bringing a fresh new look to the platform. Since joining the team, Joe has been hard at work bringing a more consistent design language to what we're doing. This release brings a lot of his hardwork to the platform itself. There's more to be done on the individual pages of the platform, but this gives us a solid framework to build on. ![](https://flowfuse.com/blog/2022/05/images/ff-05-dashboard.png) - [Epic #430 - Rebrand Forage App](https://github.com/FlowFuse/flowfuse/issues/430){rel=""nofollow""} ### Copying Projects One of the usage scenarios we want to support is having an easy way to have separate test and production environments. The previous release added the ability to configure environment variables on individual projects. This release unlocks the next piece of the puzzle - making it easy to copy flows between projects. The Project settings page has two new options: - Copy Project lets you create a complete copy of the project. - Export to existing project lets you copy over selected aspects of the project over to another project. In both cases, you get to pick what parts of the project should be copied. - [Epic #268 - Export Project](https://github.com/FlowFuse/flowfuse/issues/268){rel=""nofollow""} - [Story #271 - Duplicate Project](https://github.com/FlowFuse/flowfuse/issues/271){rel=""nofollow""} - [Story #272 - Export to Existing Project](https://github.com/FlowFuse/flowfuse/issues/272){rel=""nofollow""} ### Improve Billing Information We've been getting some great feedback from the users of FlowFuse Cloud. One of the areas we identified as needing some more clarity was around the point users are asked to setup their billing information. - [Story #563 - Improve Information on billing](https://github.com/FlowFuse/flowfuse/issues/563){rel=""nofollow""} ### Edge Devices Whilst we always want to deliver new functionality to end users in each release, sometimes bits of work don't fit naturally into a single four week iteration. That's the case here with some of the preliminary work we've done to introduce the idea of Edge Devices to the platform. The goal here is to provide a way to easily deploy and manage Node-RED projects on remote devices. This release introduces a bunch of work to the core app and front-end to begin introducing the concept of a Device. It includes the basic workflows for registering a device on the platform and being able to assign it to a team. The whole feature is hidden behind a feature flag so users on FlowFuse Cloud won't see any of this quite yet. The next release will introduce the Edge Agent piece of this - the bit that runs on devices. - [Epic #446 - Devices](https://github.com/FlowFuse/flowfuse/issues/446){rel=""nofollow""} ### Upgrading FlowFuse If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # Node-RED 3.0 Beta Stack The first beta of Node-RED 3.0 is here and FlowFuse is ready for you to try it out. When we released [FlowFuse 0.4](https://flowfuse.com/blog/2022/04/flowforge-04-released/) last month we talked about allowing users to select the stack their project runs on. Until now we've only offered one stack which has been the latest Node-RED release (2.2.2). Yesterday the first beta of Node-RED 3.0 was [released](https://discourse.nodered.org/t/node-red-3-0-0-beta-1-released/62124){rel=""nofollow""}, so as of today we have added a choice of stacks to FlowFuse Cloud. You can stick with the *Default* and use Node-RED 2.2.2 or if you want to try out the beta you can select *Node-RED-3.0.0-beta-1*. :video{ariaLabel="Selecting the beta Stack" autoPlay="true" height="730" loop="true" muted="true" playsInline="true" preload="none" width="954"} FlowFuse is the best way to run multiple Node-RED instances at different versions. Beta releases are exciting to try out, but you don't want to risk your production applications with an early upgrade. FlowFuse makes it easy to create a new project to try things out. We'll continue to update the stack choice with each beta when they are released. # FlowFuse open for everybody FlowFuse wants to enable everyone to build workflows in Node-RED. Since announcing [FlowFuse Cloud](https://flowforge.com/blog/2022/02/announcing-flowforge-cloud/){rel=""nofollow""} two months ago we've had a waiting list for users to sign up to. That allowed us to control the pace we were bringing new users onto the platform, learning what is needed to scale up our platform and continue to improve our first user experience. Today we have removed the waiting list. Anyone can sign up to FlowFuse and start a new Node-RED project in under a minute! ::div{.max-w-md.m-auto} [Sign up](https://app.flowfuse.com/account/create){.ff-btn.ff-btn--primary} :: ## What we offer Besides the sub minute time to start a new Node-RED project, there's many more features our offering includes. Two we'd like to highlight. To start our blog highlight reel: Collaboration. FlowFuse allows you to work with a team on your flows. There's the ability to create multiple users, each with their own credentials that can alter the flows on Node-RED, and it's execution environment like for example the [environments variables](https://flowforge.com/docs/user/envvar/){rel=""nofollow""}. Furthermore, [stacks](https://flowforge.com/docs/user/changestack){rel=""nofollow""}. These allow a user to select the execution environment for their Node-RED project. For example; the Node-RED version being used. Combined with the ability for one to copy a project to a new stack, this allows FlowFuse users to copy their project to the Node-RED 3.0-beta stack to validate their solutions will continue to work on the new release without disrupting their main project. ## Our roadmap Currently we're working towards our 0.6 release. The main feature of this release will be support for Devices. This will allow you to send a snapshot of a project to a Node-RED instance running outside of the FlowFuse platform and update the flows remotely. Remote devices will run our [agent](https://github.com/FlowFuse/device-agent){rel=""nofollow""} to communicate with the FlowFuse Cloud project. While the first iteration will be considered an Alpha release, by shipping early and often, it lets us get welcome feedback from our users and the wider community - helping to shape the future direction. It also allows users to start validating the feature for their own proof of concept projects. Over the next few months we're continuing to drive development of the platform across a number of areas - including further improvements to the Device feature. But also looking at new Enterprise-ready features, such as Single-Sign On integration and more tools to make collaboration even easy. We intend to grow our offering so that FlowFuse remains the best way to run Node-RED. Stay informed by registering for our newsletter! # Community News May 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [FlowFuse 0.5 AND 0.6](https://flowfuse.com/blog/2022/06/flowforge-06-released/):br We've had two releases since our last newsletter, there are a lot of related features between them. We introduced a new design for the forge application which aligns with the branding on our website. We've added [Devices](https://flowfuse.com/docs/user/concepts/#device), allowing you to run and manage your Node-RED projects on your own hardware, this is ideal for applications that need to connect to either sensor data or specialist hardware deployed outside the cloud. We added a new concept as part of this work, [Snapshots](https://flowfuse.com/docs/user/concepts/#snapshot) allow you to take a point in time copy of your project, today that can then be deployed to one or more devices but we have plans to expand this concept for things like [rolling back](https://github.com/FlowFuse/flowfuse/issues/587){rel=""nofollow""} a project to a previous point in time. [0.5](https://flowfuse.com/blog/2022/05/flowforge-05-released/) Introduced the capabilities of copying a project or certain parts of it, allowing for scenarios like having multiple environments for Development and Production. [Node-RED 3.0.0-beta-3](https://discourse.nodered.org/t/node-red-3-0-0-beta-3-released/64027){rel=""nofollow""}:br The Node-RED 3.0 beta releases continue as the project is getting very close to the full release in the coming weeks, We'll be making it availble on FlowFuse cloud very soon. [Creating custom node from subflow in Node-RED](https://kazuhitoyokoi.medium.com/creating-custom-node-from-subflow-in-node-red-ce52cc42bbba){rel=""nofollow""}:br One of the contributors to Node-RED, Kazuhito Yokoi wrote a nice tutorial on how to turn a subflow into a custom node. [YouTube Channel](https://www.youtube.com/channel/UCbBzP8NZbv3WDtlt4UouA-g){rel=""nofollow""}:br Joe has been busy creating short videos to present FlowFuse and our key concepts, these will start to appear on our YouTube channel, so please like and subscribe! [Visa Direct in Node-RED](https://www.42flows.tech/blog/why-have-we-decided-to-implement-visa-direct-api-for-node-red/){rel=""nofollow""}:br The folks over at 42flows use Node-RED in a financial and banking context, they've published an article about integrating with the Visa payments APIs [Node-RED Con CFP](https://www.papercall.io/nrcon2022){rel=""nofollow""}:br A reminder about Node-RED Con and the call for papers, submissions close at the end of July but don't wait until the last minute to submit your proposal. # FlowFuse 0.6 released Node-RED is well known for its role in IoT solutions - which often means running flows on devices. This was something we always wanted to support in FlowFuse and with this release we're taking the next steps in that direction. ### Devices This release includes the first alpha release of the [FlowFuse Device Agent](https://github.com/FlowFuse/device-agent){rel=""nofollow""}. This is a small piece of node.js software that can be installed on a device, such as a Raspberry Pi. It connects back to the FlowFuse platform to get the Node-RED flows it should be running. This builds on the work we added in 0.5 that lets you register the device, generate credentials for it and pick which Project in your team it should be assigned to. It makes it super simple to start developing your flows in FlowFuse and push them out to a group of devices with a couple clicks. There's plenty of work still to come on the Devices feature. Under the covers it uses an HTTP polling approach to check for updates. That was a pragmatic choice to get something working - but it isn't our long term strategy. We'll be working towards a more IoT-native MQTT/WebSocket appoach in the coming releases. - [Devices documentation](https://flowfuse.com/docs/device-agent/introduction/) - [Epic #446 - Devices](https://github.com/FlowFuse/flowfuse/issues/446){rel=""nofollow""} ### Snapshots This release adds the ability to create Snapshots of your projects. These are point-in-time backups of your project's flows, credentials and settings. With this release we support *creating* snapshots and pushing them to devices. We don't have the ability to revert a project back to a previous snapshot, but that will come soon. - [Snapshot documentation](https://flowfuse.com/docs/user/snapshots/) - [Story #587 - Snapshot/Rollback](https://github.com/FlowFuse/flowfuse/issues/587){rel=""nofollow""} ### Other updates Beyond these headline features, there are a number of smaller, but just as useful items in this release. We've continued with the rebranding work started in 0.5 with some more improvements to the overall UX of the platform. Little touches like placeholder loading graphics give the UI a more responsive feel. When you log out of the platform we now also automatically log you out of any Node-RED editor sessions you have open. ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 0.6 - ready for you to start creating snapshots and adding devices right now. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # Community News June 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [FlowFuse 0.7 ](https://flowfuse.com/blog/2022/07/flowforge-07-released):br We've shipped the next version of FlowFuse, features in this release include the ability to Rollback a project to a previous snapshot, setting environment variables for a specific device and we've begun to unify the experience between the Forge app and the Node-RED editor with our own Node-RED theme. [Node-RED 3.0 Released ](https://nodered.org/blog/2022/07/14/version-3-0-released){rel=""nofollow""}:br Node-RED 3.0 Has been officially released, there are a lot of improvements in the user experience of the editor, with new menus and junctions. There's also the ability to stop your flows while you can continue to edit and deploy. Take a look at the blog post for all the details. This is now the default stack on FlowFuse Cloud. [Node-RED for Microcontrollers](https://discourse.nodered.org/t/node-red-flows-on-esp8266-and-esp32/64345){rel=""nofollow""}:br[Peter Hoddie](https://twitter.com/phoddie){rel=""nofollow""} of [Moddable](https://moddable.com){rel=""nofollow""} Has released some early [work](https://github.com/phoddie/node-red-mcu){rel=""nofollow""} on getting the Node-RED runtime to execute a flow on a microcontroller like the ESP32. It's still very early days so don't expect to be building flows directly on the MCU, and the number of nodes that are supported is limited, but this is an interesting development for Node-RED in the IoT space. [Node-RED Con CFP](https://www.papercall.io/nrcon2022){rel=""nofollow""}:br A reminder about Node-RED Con and the call for papers, submissions close at the end of July so get your proposals in now. # FlowFuse 0.7 released Rollback projects to a previous snapshot, improvements in using Devices, and more. Keep reading for the details of whats in this release our you can watch our 1 min roundup video of the new release above. We're pleased to announce version 0.7 is now available. the next release of the FlowFuse application. ## Features [Rollback](https://github.com/FlowFuse/flowfuse/issues/587){rel=""nofollow""} FlowFuse is about running Node-RED at any scale, part of that scale is having multiple users collaborate on the same project. When you are collaborating with people it's important to be able to go back in time to a known working state. As part of that we are introducing rollbacks, this means that you can now take a snapshot of your project at a point in time and then make changes safe in the knowledge that you can rollback to that previous snapshot if you need to. [Device Environment Variables](https://github.com/FlowFuse/flowfuse/issues/680){rel=""nofollow""} In the last release we introduced the concept of devices. We're already learning from how these are used and one feature we've added in 0.7 is Device Environment Variables. You have been able to set Environment Variables at the project level but when deploying a snapshot to multiple devices you may want to override these values for each device, for example to set a site ID. With device specific variable users are enabled to differentiate based on the context in their flows. [FlowFuse Theme](https://github.com/FlowFuse/flowforge-nr-theme/){rel=""nofollow""} Now that we have a stronger visual identity in the Forge application we have continued that work through to the Node-RED editor. If you create or upgrade a project with a Node-RED 3.0 stack you will see a different theme in the editor. It's still very much Node-RED but just has some subtle hints to tie it back to the FlowFuse application. We will continue to iterate on this to further integrate the experience between FlowFuse and Node-RED in both directions. !["Screenshot showing the FlowFuse theme when the Node-RED 3.0 stack is selected"](https://flowfuse.com/blog/2022/07/images/ff-07-theme.png "Screenshot showing the FlowFuse theme when the Node-RED 3.0 stack is selected") [ProjectTypes](https://github.com/FlowFuse/flowfuse/issues/380){rel=""nofollow""} The introduction of ProjectTypes is a way to group Stacks together that share common characteristics - such as memory/cpu limits, or the availability of particular features. In platforms with billing enabled, such as our own FlowFuse Cloud, the ProjectTypes can have different price points set on them. Within FlowFuse Cloud, you'll see we've introduced the Small ProjectType - which applies to all existing projects on the platform. [Stack Versions](https://github.com/FlowFuse/flowfuse/issues/694){rel=""nofollow""} This allows an admin to link different stacks together in their lineage. This allows administrators to nudge users to new Node-RED versions or upgrade pre-installed dependencies when running in a container environment. Any users with projects on an old version will be prompted that there is an update available, making it even easier to stay up to date with Node-RED versions when you build your flows on FlowFuse. ## Improvements We've made a number of improvements to the overall experience of running FlowFuse. - The Team Switch menu has been moved to a more prominent position in the interface, this also makes it easier to see how to create a new team. [#616](https://github.com/FlowFuse/flowfuse/issues/616){rel=""nofollow""} - Notifications have had an overhaul, you will now see waiting invites on all pages. [#515](https://github.com/FlowFuse/flowfuse/issues/515){rel=""nofollow""} - If you are running your own copy of FlowFuse you can now see the version details in the admin pages [#655](https://github.com/FlowFuse/flowfuse/issues/655){rel=""nofollow""} - Device polling is no longer an INFO level message filling the log on your devices [#10](https://github.com/FlowFuse/device-agent/issues/10){rel=""nofollow""} ## Bug Fixes We've fixed the following bugs in this release. - [Devices now listen on all Interfaces allowing you to run local http servers](https://github.com/FlowFuse/device-agent/issues/7){rel=""nofollow""}:br - [Solved an issue where a device gets an error unknown device](https://github.com/FlowFuse/device-agent/issues/7){rel=""nofollow""}:br - [The Audit Log in the Forge app displays the correct IP when a user logs in to Node-RED](https://github.com/FlowFuse/flowfuse/issues/507){rel=""nofollow""}:br - [Resolved an issue with devices downloading snaphots from legacy stacks](https://github.com/FlowFuse/flowfuse/issues/507){rel=""nofollow""}:br - [Fixed an error where objects in the Node-RED log would hang the log page](https://github.com/FlowFuse/flowfuse/issues/735){rel=""nofollow""}:br - [Next Billing Date is now shown correctly](https://github.com/FlowFuse/flowfuse/issues/745){rel=""nofollow""}:br - [Fixed a bug where the loading page would flash during polling](https://github.com/FlowFuse/flowfuse/issues/689){rel=""nofollow""}:br ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 0.7 - ready for you to try out rollbacks and the new theme. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Customers of FlowFuse Cloud can raise a ticket by emailing We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # Introducing Medium Projects on FlowFuse Cloud We've added a second size of project to FlowFuse Cloud. A bigger project type with more resources available to it. Our [0.7 release](https://flowforge.com/blog/2022/07/flowforge-07-released/){rel=""nofollow""} introduced the concept of Project Types. This allows platforms to provide different sizes of projects, varying the memory/cpu or features available within a given type. Today we've put this feature to work on FlowFuse Cloud by introducing the new **Medium Project** type. !["Screenshot showing the new stack selection feature"](https://flowfuse.com/blog/2022/07/images/project-type.png "Screenshot showing the new stack selection feature") Medium projects have 3 times the resources of the existing Small type, allowing for more complex flows and larger message objects. This will be useful to business users looking to process complex sets of data. Our Medium project is priced at $50 a month and we'll be adding new features to this project type in the coming months to further enhance the value of this new tier. We don't currently support directly upgrading a project between types, but that is in the [plan for the future](https://github.com/FlowFuse/flowfuse/issues/595){rel=""nofollow""}. In the meantime, you can use the 'Export Project' feature on a project's settings tab to copy it over into a new Medium type project. # Community News July 2022 Welcome to the FlowFuse newsletter, a regular roundup of what\`s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [FlowFuse 0.8 ](https://flowfuse.com/blog/2022/08/flowforge-08-released/):br Version 0.8 was released, notable features include the new Project Link nodes for sharing data between projects, and the ability to stop and start flows within Node-RED. We've updated the format of our release posts as well to detail all the user facing changes, from new Features through to small improvements and bugs. [Raspberry Pi Pico with Node-RED](https://www.tomshardware.com/how-to/raspberry-pi-pico-w-node-red){rel=""nofollow""}:br[Les Pounder](https://twitter.com/biglesp){rel=""nofollow""} Has published a great tutorial on communicating with the new [Raspberry Pi Pico W](https://www.raspberrypi.com/products/raspberry-pi-pico/){rel=""nofollow""} and Node-RED. He shows you how to capture data from an environmental sensor then display this on a dashboard. [Medium Projects](https://flowfuse.com/blog/2022/07/new-projecttype/):br We've added a second size of project to FlowFuse Cloud. A bigger project type with more resources available to it. [Using Node-RED to control IoT Devices on Golioth](https://blog.golioth.io/how-to-use-node-red-to-control-iot-devices-on-golioth/){rel=""nofollow""}:br Our friends at [Golioth](https://golioth.io/){rel=""nofollow""} have published an article on how to use Node-RED to control and process data from IoT Devices connected to their platform. [Node-RED 3.0.2 and 2.2.3](https://discourse.nodered.org/t/node-red-2-2-3-and-3-0-2-released/66018){rel=""nofollow""}:br Node-RED 3.0.2 has been released fixing some bugs in the 3.0.0 release, The Node-RED 2.x stream has also had a maintenance release with many of the fixes in 3.0 back-ported. As usual these are already available as stacks on FlowFuse Cloud. # FlowFuse 0.8 released Easily pass messages between your projects on the cloud or devices, UX improvements, and more. Keep reading for the details of whats in this release our you can watch our 1 minute roundup video of the new release above. We're pleased to announce version 0.8 is now available. The next release of the FlowFuse application containing new features, a number of improvements, and bug fixes. ## Features [Project Link Nodes](https://github.com/FlowFuse/flowfuse/issues/662){rel=""nofollow""} We've introduced our first custom FlowFuse nodes to the palette of new projects. The Project Link nodes allow you to easily pass data between different projects within the same team. These projects can be running in the cloud or on devices, with the communication powered by our own internal MQTT broker. Try these out today on FlowFuse Cloud by creating a new project or updating your existing project's stack. There's more information in the [README](https://github.com/FlowFuse/nr-project-nodes/blob/main/README.md){rel=""nofollow""} for the nodes. For local installs of FlowFuse, the nodes are only available with an Enterprise Edition license. :video{ariaLabel="Video showing the message being sent from one project to another using project link nodes" autoPlay="true" height="678" loop="true" muted="true" playsInline="true" preload="none" width="1864"} [Start & Stop Flows](https://github.com/FlowFuse/flowfuse/issues/839){rel=""nofollow""} Node-RED 3.0 [introduced a new feature](https://nodered.org/blog/2022/07/14/version-3-0-released#editing-stopped-flows){rel=""nofollow""} that allows you to stop your flows from processing requests while still being able to work in the editor and deploy changes. We've now enabled this feature within FlowFuse for projects running a Node-RED 3.x stack. [Default Team](https://github.com/FlowFuse/flowfuse/issues/298){rel=""nofollow""} If you are a member of multiple teams you can now set your preferred default saving you from having to change teams each time you log in. ## Improvements We've made a number of improvements to the overall experience of running FlowFuse. - Devices now communicate to the Forge application over MQTT instead of polling [#754](https://github.com/FlowFuse/flowfuse/issues/754){rel=""nofollow""}. You'll need to update your Device Agent to the latest version to take advantage of this. - The table views have had a major overhaul allowing you to sort and search items [#28](https://github.com/FlowFuse/forge-ui-components/issues/28){rel=""nofollow""} - If the application receives an error you now see a notification in the UI. [#771](https://github.com/FlowFuse/flowfuse/issues/771){rel=""nofollow""} - The Verification email page has been cleaned up [#718](https://github.com/FlowFuse/flowfuse/issues/718){rel=""nofollow""} - The initial Thank-you page has been cleaned up [#648](https://github.com/FlowFuse/flowfuse/issues/648){rel=""nofollow""} ## Bug Fixes We've fixed the following bugs in this release. - [Logo Distorted in Safari](https://github.com/FlowFuse/flowfuse/issues/793){rel=""nofollow""}:br - [LocalFS Install doesn't check for Build Tools](https://github.com/FlowFuse/flowfuse/issues/729){rel=""nofollow""}:br - [Users with Expired passwords can create teams](https://github.com/FlowFuse/flowfuse/pull/842){rel=""nofollow""}:br - [Click-jacking Vulnerability](https://github.com/FlowFuse/flowfuse/pull/790){rel=""nofollow""} - [Users with can create teams without verifying email](https://github.com/FlowFuse/flowfuse/pull/824){rel=""nofollow""}:br - [Occasional Timeout when deploying flows](https://github.com/FlowFuse/flowforge-nr-storage/issues/17){rel=""nofollow""}:br - [Notification of member deletion contains internal ID](https://github.com/FlowFuse/flowfuse/issues/833){rel=""nofollow""}:br - [Pressing Enter in the Team Delete modal triggers cancel](https://github.com/FlowFuse/flowfuse/issues/334){rel=""nofollow""}:br - [Node-RED Isn't ready when Forge app says it is running (Docker)](https://github.com/FlowFuse/flowfuse/issues/751){rel=""nofollow""}:br ## Contributors We'd like the thank the following for their contributions to this release: - [HaroldPetersInskipp](https://github.com/HaroldPetersInskipp){rel=""nofollow""} helped [updating our documentation](https://github.com/FlowFuse/flowfuse/pull/812){rel=""nofollow""} - [Steveorevo](https://github.com/Steveorevo){rel=""nofollow""} also [updated our documentation](https://github.com/FlowFuse/flowfuse/pull/818){rel=""nofollow""} As an open-source project, we welcome the community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 0.8 and the stacks updated. Upgrade your project stacks to the latest version and start using the Project Link nodes now. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Customers of FlowFuse Cloud can raise a ticket by emailing We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # Community News August 2022 Welcome to the FlowFuse newsletter, a regular roundup of what’s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [**FlowFuse 0.9**](https://flowfuse.com/blog/2022/09/flowforge-09-released/):br Version 0.9 was released on 1st September. Our latest release includes some great new features, quality of life improvements and bug fixes. Notable additions included the ability to [suspend your projects](https://github.com/FlowFuse/flowfuse/pull/893){rel=""nofollow""}, [login with your email](https://github.com/FlowFuse/flowfuse/pull/856){rel=""nofollow""} and [define custom paths for your dashboards](https://github.com/FlowFuse/flowfuse/issues/774){rel=""nofollow""}. If you’d like to learn more about what else was included in 0.9 you can do so on our [blog post](https://flowfuse.com/blog/2022/09/flowforge-09-released/), on our [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v0.9.0){rel=""nofollow""} and on our [Youtube channel](https://www.youtube.com/watch?v=d23Pmyc0k7I){rel=""nofollow""}. We’d also love for more of you to get involved in the development of FlowFuse, [contributions to the code](https://github.com/FlowFuse/flowfuse/blob/main/CONTRIBUTING.md){rel=""nofollow""} and [bug reports](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""} are really appreciated. [**Node-RED Con 2022**](https://nrcon.nodered.org/){rel=""nofollow""}:br We are happy to again be involved in Node-RED con. The event is being held online on 7th October, with content for both English and Japanese speakers. You can find out more on the [Node-RED Con website](https://nrcon.nodered.org/){rel=""nofollow""}. [**FlowFuse Team News**](https://flowfuse.com/about/):br We’d like to welcome Rob Marcer to the FlowFuse team. Rob has joined as our Developer Educator, he's going to work to help you get the best value from FlowFuse by developing our documentation and community support. We are also recruiting for [NodeJS Developers](https://boards.greenhouse.io/flowfuse/jobs/4463977004){rel=""nofollow""}, if you’re interested in joining our team please [apply here](https://boards.greenhouse.io/flowfuse/jobs/4463977004#app){rel=""nofollow""}. [**Official Node-RED Docker Image Passes Milestone**](https://twitter.com/Docker/status/1559919666721693699?t=QBzGGzY2kJ12Z5aoi1QPTA){rel=""nofollow""}:br Docker has [announced](https://twitter.com/Docker/status/1559919666721693699?t=QBzGGzY2kJ12Z5aoi1QPTA){rel=""nofollow""} that the Node-RED Docker image has now been downloaded over 100 million times. They have also created a [guide to using Node-RED](https://www.docker.com/blog/build-retail-store-items-detection-system-no-code-ai/?utm_campaign=2022-08-17-brnd-nocode&utm_medium=social&utm_source=twitter){rel=""nofollow""} to Build and Deploy a Retail Store Items Detection System Using No-Code AI Vision at the Edge. We think it’s worth a read. [**Simulating IOT Projects in Your Browser**](https://wokwi.com/){rel=""nofollow""}:br While not directly related to FlowFuse we’ve enjoyed wasting a little too much time looking at the simulated IOT projects on [Wokwi](https://wokwi.com/){rel=""nofollow""}. The [Simon Game with Score](https://wokwi.com/projects/328451800839488084){rel=""nofollow""} project is a little too addictive. [**Try FlowFuse for Free**](https://app.flowfuse.com/account/create){rel=""nofollow""}:br As a thank you for reading our newsletters we’d like to offer you a free, small project for one month on FlowFuse when you create a new team. To get this discount please use the code RELEASE09 when on the payment page after creating a new team. # FlowFuse 0.10 released Secure your HTTP endpoints, create read-only users in your teams and use our static IP address for outbound traffic Keep reading for the details of what's in this release or you can watch our 1 minute roundup video of the new release above. We're pleased to announce version 0.10 is now available. The next release of the FlowFuse application containing new features, a number of improvements, and bug fixes. Keep reading for a promotion code to get your first month free on FlowFuse Cloud. ## Features [Secure HTTP Endpoints](https://github.com/FlowFuse/flowfuse/issues/578){rel=""nofollow""} We've added the ability for you to secure your HTTP endpoints. You can now control who can access Dashboards or API endpoints you create in FlowFuse. [Read-only Users](https://github.com/FlowFuse/flowfuse/issues/657){rel=""nofollow""} We've added a new user role for Read-only access. This will allow users to login to your FlowFuse project and view the Node-RED flows without them being able to edit anything. [Static Outbound IP Addresses](https://flowfuse.com/docs/cloud/introduction/#ip-addresses) We've updated FlowFuse Cloud so that all outbound traffic from your projects now comes from a single IP address. When trying to access a remote resource such as a database it is often a requirement for the IP address the traffic comes from to be fixed. ## Improvements We've made a number of improvements to the overall experience of running FlowFuse. - Allow both key and component in a ff-data-table column definition [#43](https://github.com/FlowFuse/forge-ui-components/issues/43){rel=""nofollow""} - Default Stack and Templates [#989](https://github.com/FlowFuse/flowfuse/issues/989){rel=""nofollow""} - Provide platform containers and base stack container for administrators [#917](https://github.com/FlowFuse/flowfuse/issues/917){rel=""nofollow""} ## Bug Fixes We've fixed the following bugs in this release. - [Provide platform containers and base stack container for administrators](https://github.com/FlowFuse/flowfuse/issues/917){rel=""nofollow""} - [User names can be same (but different case)](https://github.com/FlowFuse/flowfuse/issues/983){rel=""nofollow""} - [User list not refreshing after changing user details](https://github.com/FlowFuse/flowfuse/issues/463){rel=""nofollow""} - [Navigating directly to a device page gets the wrong team selected](https://github.com/FlowFuse/flowfuse/issues/986){rel=""nofollow""} - [Node-RED Isn't ready when FlowFuse app says it is running following a project restart](https://github.com/FlowFuse/flowfuse/issues/941){rel=""nofollow""} - [Invitations left for deleted teams](https://github.com/FlowFuse/flowfuse/issues/923){rel=""nofollow""} - [Following email verification link twice throws error](https://github.com/FlowFuse/flowfuse/issues/1024){rel=""nofollow""} - [Agent does not log stderr from the Node-RED process](https://github.com/FlowFuse/device-agent/issues/21){rel=""nofollow""} - [On Kubernetes project names can not start with a number](https://github.com/FlowFuse/flowfuse/issues/948){rel=""nofollow""} - [When creating projects stack options do not wrap](https://github.com/FlowFuse/flowfuse/issues/930){rel=""nofollow""} - [Save button in admin user-edit dialog doesn't close dialog](https://github.com/FlowFuse/flowfuse/issues/979){rel=""nofollow""} - [Setting UI doesn't allow me to update settings](https://github.com/FlowFuse/flowfuse/issues/911){rel=""nofollow""} ## Contributors We'd like the thank the following for their contributions to this release: [Pezmc](https://github.com/Pezmc){rel=""nofollow""} for their work on [Add device count and project counts by type to admin](https://github.com/FlowFuse/flowfuse/pull/949){rel=""nofollow""} [ArshErgon](https://github.com/ArshErgon){rel=""nofollow""} for their work on [Update vue component name for NoVerifiedEmail.vue](https://github.com/FlowFuse/flowfuse/pull/977){rel=""nofollow""} As an open-source project, we welcome the community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Try it out [Sign up for FlowFuse Cloud](https://app.flowfuse.com/account/create?code=RELEASE010){rel=""nofollow""} with this link or at the checkout enter the code **RELEASE010** to get your first project free for a month. ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 0.10 and the stacks updated. Upgrade your project stacks to the latest version to make sure you get all the latest changes. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Customers of FlowFuse Cloud can raise a ticket by emailing We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # FlowFuse 0.9 released Suspend your projects when you don't need them, login with either your username or email, and introducing Team Types Keep reading for the details of what's in this release our you can watch our 1 minute roundup video of the new release above. We're pleased to announce version 0.9 is now available. The next release of the FlowFuse application containing new features, a number of improvements, and bug fixes. Keep reading for a promotion code to get your first month free on FlowFuse Cloud. ## Features [Suspend Projects](https://github.com/FlowFuse/flowfuse/issues/893){rel=""nofollow""} Sometimes you want to put a project to one side for a while, maybe your development has stalled or you're waiting on something external to be ready. Perhaps you don't need it to be running all the time. With the 0.9 release we've added the ability to suspend a project. Once suspended, your flows are safely stored in the platform database, but Node-RED isn't running and the project doesn't consume any resources. In FlowFuse Cloud we do not charge you for suspended projects - you only pay when the project is running. Your project will be there ready to start back up when you need it with just one click. Remember that any context data or anything written to the filesystem will not persist through a restart or a suspend of a project. Alongside this change, we've removed the option to 'stop' a project. That option would only stop Node-RED, but the underlying container would still be running, consuming resources. With Node-RED 3.0 adding the ability to stop the flows, but still be able to edit them, that provides a much better user experience. You can still restart the Node-RED process from the Forge app as before for example when you have updated a package in your flows. [Team Types](https://github.com/FlowFuse/flowfuse/issues/733){rel=""nofollow""} We've introduced another concept into the platform with this release. Team Types will allow us to offer more advanced features to teams on FlowFuse Cloud. You won't see much difference in this release but it allows us to build on in future releases. [PostHog Analytics](https://github.com/FlowFuse/flowfuse/issues/695){rel=""nofollow""} We've changed the analytics tooling integrated into the platform. With this release, we've deprecated the use of Plausible Analytics as it didn't quite provide the sort of insight we wanted. We now integrate with [PostHog](https://posthog.com/){rel=""nofollow""}. They share our ethos and approach to open source and self hosting - something you can take advantage of if you're running your own FlowFuse platform. For FlowFuse Cloud, the data is sent to our PostHog account so we can better understand how the platform is being used. If you're running your own instance, the information is only captured if you configure it with your own PostHog instance details - it does not send any data back to us. [Login with email](https://github.com/FlowFuse/flowfuse/issues/856){rel=""nofollow""} A common problem that we've seen from users is trying to login with their email address instead of their username. As of 0.9 you can now enter either at the login screen. [Custom Dashboard Path](https://github.com/FlowFuse/flowfuse/issues/774){rel=""nofollow""} If you are using the Node-RED Dashboard set of nodes, you can now change the path where the dashboard will be served from. The default is still `/ui` but you can now move that onto `/` or anything else. This is helpful when migrating existing projects over to FlowFuse. ## Improvements We've made a number of improvements to the overall experience of running FlowFuse. - Improvements to the FlowFuse Theme [#883](https://github.com/FlowFuse/flowfuse/pull/883){rel=""nofollow""}. - Upper-case characters in Project Names [#546](https://github.com/FlowFuse/flowfuse/issues/546){rel=""nofollow""} - Password reset requests are logged[#773](https://github.com/FlowFuse/flowfuse/issues/773){rel=""nofollow""} - Admin can manually verify users email [#902](https://github.com/FlowFuse/flowfuse/issues/692){rel=""nofollow""} ## Bug Fixes We've fixed the following bugs in this release. - [Cannot edit template settings](https://github.com/FlowFuse/flowfuse/issues/875){rel=""nofollow""}:br - [Project Link Nodes Appear in CE Install](https://github.com/FlowFuse/nr-project-nodes/issues/10){rel=""nofollow""} - [Project Link Nodes MQTT Connection](https://github.com/FlowFuse/nr-project-nodes/issues/14){rel=""nofollow""} - [Theme shows white characters on white background](https://github.com/FlowFuse/flowforge-nr-theme/issues/19){rel=""nofollow""} - [Changing Project on device doesn't remove old modules](https://github.com/FlowFuse/device-agent/issues/27){rel=""nofollow""} - [Device Agent and Node-RED use different time in logs](https://github.com/FlowFuse/device-agent/issues/30){rel=""nofollow""} ## Contributors We'd like the thank the following for their contributions to this release: [Bonantech](https://github.com/bonanitech){rel=""nofollow""} for his work [cleaning up the theme CSS](https://github.com/FlowFuse/flowforge-nr-theme/commit/30e21a3777dc3438ef206157ee9110728011f59c){rel=""nofollow""} As an open-source project, we welcome the community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowforge.com/docs/contribute/){rel=""nofollow""}. ### Try it out [Sign up for FlowFuse Cloud](https://app.flowfuse.com/account/create){rel=""nofollow""} and at the checkout enter the code **RELEASE09** to get your first project free for a month. ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 0.9 and the stacks updated. Upgrade your project stacks to the latest version and start using the Project Link nodes now. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please do raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Customers of FlowFuse Cloud can raise a ticket by emailing We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # Static Outbound IP Addresses On Friday last week we updated FlowFuse Cloud to use a static IP address for outbound traffic. This will allow you to predict which IP address your traffic will come from for example when traversing a firewall or accessing a remote database. You will need to manually suspend then start each of your projects (a restart will not move your projects to the fixed IP address). Once that action is completed all outbound connections will come from one of our static IP address. Any inbound traffic should still use the hostname assigned to each of your projects, you cannot use our IP address to route http traffic to your projects. You can view our IP address in the [Docs](https://flowfuse.com/docs/cloud/introduction/#ip-addresses) section of our website. If you’d like to stay up to date with our latest releases you can do so on [our blog](https://flowfuse.com/blog). # Community News September 2022 Welcome to the FlowFuse newsletter, a regular roundup of what’s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [**FlowFuse 0.10**](https://flowfuse.com/blog/2022/09/flowforge-010-released/) Version 0.10 was released on 30th September. Our latest release includes some great new features, quality of life improvements and bug fixes. Notable additions included the ability to [Secure your HTTP endpoints](https://github.com/FlowFuse/flowfuse/pull/893){rel=""nofollow""}, [Add read-only users to your projects](https://github.com/FlowFuse/flowfuse/issues/657){rel=""nofollow""} and [use our static IP address for outbound connections](https://flowfuse.com/docs/cloud/introduction/#ip-addresses) If you’d like to learn more about what else was included in 0.10 you can do so on our [blog post](https://flowfuse.com/blog/2022/09/flowforge-010-released/), on our [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v0.10.0){rel=""nofollow""} and on our [Youtube channel](https://youtube.com/watch?v=mjR1iiEFiBg){rel=""nofollow""}. We’d also love for more of you to get involved in the development of FlowFuse, [contributions to the code](https://github.com/FlowFuse/flowfuse/blob/main/CONTRIBUTING.md){rel=""nofollow""} and [bug reports](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""} are really appreciated. [**Node-RED Con 2022**](https://nrcon.nodered.org/){rel=""nofollow""}:br We are happy to again be involved in Node-RED con. The event is being held online on 7th October, with content for both English and Japanese speakers including our colleagues Nick O'Leary and Sam Machin. You can find out more on the [Node-RED Con website](https://nrcon.nodered.org/){rel=""nofollow""}. [**FlowFuse Team News**](https://flowforge.com/team/){rel=""nofollow""}:br We are currently recruiting [NodeJS Developers](https://boards.greenhouse.io/flowfuse/jobs/4463977004){rel=""nofollow""}, if you’re interested in joining our team please [apply here](https://boards.greenhouse.io/flowfuse/jobs/4463977004#app){rel=""nofollow""}. We are also looking for a [PeopleOps Manager](https://boards.greenhouse.io/flowfuse/jobs/4687876004){rel=""nofollow""} to help us grow our team. You can [apply here](https://boards.greenhouse.io/flowfuse/jobs/4687876004#app){rel=""nofollow""} for that position. [**Forest Fire Alerts Using ML, IOT and Node-RED**](https://hackster.io/user102774/fight-fire-wild-fire-prediction-using-tinyml-df7572){rel=""nofollow""}:br This fascinating project came up a few days ago and we wanted to share it with you all. The concept is to use a mesh network of IoT devices to monitor various indicators of potential and current wildfires and report data back to the relevant authorities. The system will use ML to predict wildfire risk levels and hopefully send warnings before a fire actually starts. The two developers [Muhammed](https://linkedin.com/in/zainmfd/){rel=""nofollow""} and [Salman](https://linkedin.com/in/salmanfarisvp/){rel=""nofollow""} are planning to use Node-RED to manage the reporting of fires to the authorities. [**Try FlowFuse for Free**](https://app.flowfuse.com/account/create){rel=""nofollow""}:br As a thank you for reading our newsletters we’d like to offer you a free, small project for one month on FlowFuse when you create a new team. To get this discount please follow [this link](https://app.flowfuse.com/account/create?code=RELEASE010){rel=""nofollow""} or use the code RELEASE010 when on the payment page after creating a new team. # Scheduled maintenance: Database encryption October 2022 As part of an on-going security [review](https://flowfuse.com/platform/security/#data-at-rest) of the FlowFuse Cloud offering we discovered that the backend database was not using encrypted storage. In keeping with industry best practices we plan to migrate the database to a new instance using encrypted at rest storage. ## Impact Customers' Node-RED instances will remain running, though any features that depend on FlowFuse will not operate as expected during the migration. This includes user sessions, project and team management, as well as the project nodes for inter-project communication. Self-hosted installations are unaffected by this change. ## When The migration will be on 26 October 2022 at 22:00 UTC and is expected to take under 2 hours. The platform will be available as soon as the migration is complete. We will post updates during the migration period to our Twitter account [@FlowFuseInc](https://twitter.com/flowforgeinc){rel=""nofollow""}. # Install FlowFuse Docker on Google Cloud As part of our preparations for FlowFuse 1.0 we have been testing various real world scenarios to see where we can add to our documentation and where we might be able to improve our releases to make the install process easier for users. As a benefit of that testing we have been able to hone these installation processes and we wanted to share one of those with you today. In this first of three articles, we are going to run through the process for installing FlowFuse on Google Cloud Platform (GCP) within a virtual machine (VM) using Docker. We have set ourselves the goal of delivering a production environment. We want this installation benefit from: - Email alerts (emails to users when they are added to teams etc) - HTTPS access to the install - FlowFuse [Device](https://flowfuse.com/docs/user/concepts/#device) deployment via the included MQTT server that comes in our Docker build We will follow up with a second article covering the process of getting HTTPS running then we will close out the series by covering how to use key features of FlowFuse including [Devices](https://flowfuse.com/docs/user/concepts/#device). # Prerequisites - A domain name - We've registered flowforge-demo.com to demonstrate these steps - A DNS provider - Our Domain registrar provides a basic DNS service for free - A GCP account - Google will often give you free service credits on sign up so setting up FlowFuse on GCP should not cost you anything for at least a few weeks - An email provider which will allow SMTP connections to send email - To manage users on your FlowFuse platform you will need to be able to send emails to them. We have used a Google Workspace account for this purpose # GCP VM Creation Create a GCP account, once logged in navigate to Compute Engine then VM Instances. Select Create Instance you should now be [here](https://console.cloud.google.com/compute/instancesAdd?project){rel=""nofollow""}. Give your instance a name, select a Region and Zone. I have found that the default machine configuration works fine but depending on your project you may wish to change the resources. !["Screenshot showing the interface for creating GCP VM"](https://flowfuse.com/blog/2022/10/images/1.png "Screenshot showing the interface for creating GCP VM") You now need to allow access to your FlowFuse installation from the internet. In the Firewall section tick Allow HTTP traffic and Allow HTTPS traffic. !["Screenshot showing the firewall section in the interface for creating a GCP VM"](https://flowfuse.com/blog/2022/10/images/2.png "Screenshot showing the firewall section in the interface for creating a GCP VM") Next up, assign a static IP address to the VM. Click Advanced options, then Networking. Now scroll down until you see Network interfaces and click on default to expand that section. In External IPv4 address select Create IP Address, give it a name than press Reserve. !["Screenshot showing the network section in the interface for creating a GCP VM"](https://flowfuse.com/blog/2022/10/images/3.png "Screenshot showing the network section in the interface for creating a GCP VM") Once you have reserved your IP it will be shown in the External IPv4 address field, write it down as we will need it later to create the DNS records. Our IP address was 34.125.156.130. !["Screenshot showing your reserved IP in the External IPv4 address field"](https://flowfuse.com/blog/2022/10/images/4.png "Screenshot showing your reserved IP in the External IPv4 address field") You are now ready to create and boot your VM, scroll to the bottom of the page and press Create. It can take a minute or two for the VM to be ready to use. # DNS Set Up So that you can run FlowFuse on your newly created GCP VM you will need to set up 2 DNS records. These records are slightly different to what is suggested in the FlowFuse install docs. We were keen to be able to run other services on this domain so we set up the following records. ![Screenshot showing interface for setting DNS](https://flowfuse.com/blog/2022/10/images/5.png "Screenshot showing interface for setting DNS") DNS changes need to propagate, and depending on your DNS provider, ISP, and other factors, this can take anywhere between a few seconds to 4 hours. Our’s were in place very quickly. To validate the DNS records you can use `dig` on either a Mac or Linux. ![Screenshot showing output of the dig command](https://flowfuse.com/blog/2022/10/images/6.png "Screenshot showing output of the dig command") The DNS records are set to the IP record we noted down earlier, so we're good to continue. # FlowFuse Docker Installation The next step is to install Docker on our GCP VM. If you return to GCP you should see that your VM is now up and running, you can now click on SSH to connect to your VM. This will open up a browser based SSH session to your VM. ![Screenshot showing access to SSH in GCP](https://flowfuse.com/blog/2022/10/images/7.png "Screenshot showing access to SSH in GCP") Once you have a Secure Shell (SSH) session open, the first step is to install Docker using the following commands. `sudo apt-get update` ```text sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release ``` `sudo mkdir -p /etc/apt/keyrings` `curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg` ```text echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null ``` `sudo apt-get update` `sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin` You can read a lot more detail about what each these commands actually do [here](https://docs.docker.com/engine/install/debian/){rel=""nofollow""}. # Download FlowFuse’s latest Docker build The next step is to get the codebase for FlowFuse onto your VM, to do so you will need to run the following commands. Please note that we are working with our 0.10.0 build, you will need to update the version number in the commands below if you are working with a newer build. Use curl to download the files we need. `sudo curl -L https://github.com/FlowFuse/docker-compose/archive/refs/tags/v0.10.1.tar.gz -o v0.10.1.tar.gz` Make the directory where we will store FlowFuse. `sudo mkdir /opt/flowforge` Uncompress FlowFuse and save it to the directory. `sudo tar zxf v0.10.1.tar.gz --directory /opt/flowforge` You should now have all the code you need for FlowFuse in the directory `/opt/flowforge/docker-compose-0.10.1`, it should look something like this. !["Screenshot showing directory listing for FlowFuse"](https://flowfuse.com/blog/2022/10/images/8.png "Screenshot showing directory listing for FlowFuse") # Configure FlowFuse We can now configure FlowFuse on your VM. We are going to need to edit two files. Firstly we need to switch into the directory where we just placed FlowFuse. `cd /opt/flowforge/docker-compose-0.10.1` Then we need to edit the flowforge.yml file, we're using Nano to do that. `sudo nano /opt/flowforge/docker-compose-0.10.1/etc/flowforge.yml` At the top of the file you need to update the domain and base\_url to match your domain !["Screenshot showing domain configuration in flowforge.yml"](https://flowfuse.com/blog/2022/10/images/9.png "Screenshot showing domain configuration in flowforge.yml") Next we will need to edit the Email Configuration section to match your SMTP provider. Set enabled to true then add in the details provider by your email provider. For example in this case I am using our Google Workspace account. !["Screenshot showing email configuration in flowforge.yml"](https://flowfuse.com/blog/2022/10/images/10.png "Screenshot showing email configuration in flowforge.yml") Finally, you need to update the `public_url` for your mqtt broker to match your DNS record. !["Screenshot showing MQTT configuration in flowforge.yml"](https://flowfuse.com/blog/2022/10/images/11.png "Screenshot showing MQTT configuration in flowforge.yml") You can now save and close that file, in Nano you can do that by pressing ‘control x’ then ‘y’ then the Return key. Now we need to edit the `docker-compose.yml` file. We will use Nano again to do that. `sudo nano /opt/flowforge/docker-compose-0.10.1/docker-compose.yml` We need to edit the file to add in to the domain as follows. !["Screenshot showing virtual hosts configuration in docker-compose.yml"](https://flowfuse.com/blog/2022/10/images/12.png "Screenshot showing virtual hosts configuration in docker-compose.yml") Save and exit from that file, in Nano you can do that by pressing ‘control x’ then ‘y’ then the Return key. # Start FlowFuse We are now ready to start up FlowFuse for the first time, to do so we will use the following command. `sudo docker compose -p flowforge up -d` The build process will take a few minutes, once it’s completed let’s make sure all the docker containers are running. `sudo docker ps` ![Docker PS output](https://flowfuse.com/blog/2022/10/images/13.png) You should see 4 running Docker containers. If everything went well you should now be able to access your FlowFuse server via the DNS record you created. ![FF Login page](https://flowfuse.com/blog/2022/10/images/14.png) Nice, you now have a working instance of FlowFuse running on GCP but remember that all traffic is currently running on HTTP so we still have some work to do. In the next article we will cover how to add HTTPS support to this FlowFuse installation. # FlowFuse 1.0 released Predefined environment variables for your Instances and Devices, manage your Project's modules and import your existing flows (and credentials) into your FlowFuse [Projects](https://flowfuse.com/docs/user/concepts/#instance). We're pleased to announce version 1.0 FlowFuse is now available. Keep reading for a promotion code to get your first month free on FlowFuse. Version 1.0 represents our vision of the base set of features needed for you to get great value from using FlowFuse in a production environment. That's not to say we are done, we will continue to add features, improve our interfaces and fix bugs with the same enthusiasm as we've worked towards 1.0. We'd like to hear your feedback on what we will be including in [1.1 and beyond](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""}. ## Features [Standard Environment Variables set for both Projects and Devices](https://github.com/FlowFuse/flowfuse/issues/841){rel=""nofollow""} Projects now get a set of predefined environment variables that can be used by their flows. These give your flows access to the projects unique id and name. When the flows are deployed to [devices](https://flowfuse.com/docs/user/concepts/#device), they also get the device's id and name. That makes is easier to deploy flows across multiple devices and have each able to identify itself. [Add additional node modules to your projects](https://github.com/FlowFuse/flowfuse/issues/405){rel=""nofollow""} This feature allows you to pre-define additional Node-RED nodes and node modules you may want to be installed in your FlowFuse project, making it easier to manage. [Import existing projects into FlowFuse](https://github.com/FlowFuse/flowfuse/issues/835){rel=""nofollow""} You can now import your existing flow and credentials files straight into your FlowFuse project - making it really easy to move your existing projects into the platform. ## Improvements We've made a number of improvements to the overall experience of running FlowFuse. - Editable Stack labels [#915](https://github.com/FlowFuse/flowfuse/issues/915){rel=""nofollow""} - Check for suitable version of Node on Devices [#37](https://github.com/FlowFuse/device-agent/issues/37){rel=""nofollow""} - Realtime Project status details in Project overview [#990](https://github.com/FlowFuse/flowfuse/issues/990){rel=""nofollow""} - Improve Template creation & Edit Project Settings UX [#1041](https://github.com/FlowFuse/flowfuse/issues/1041){rel=""nofollow""} ## Bug Fixes We've fixed the following bugs in this release. - [Pressing return in search box reloads page](https://github.com/FlowFuse/flowfuse/issues/1143){rel=""nofollow""} - [Vue Router Warn](https://github.com/FlowFuse/flowfuse/issues/1126){rel=""nofollow""} - [Kebab menu in Settings breaks](https://github.com/FlowFuse/forge-ui-components/issues/58){rel=""nofollow""} - [flowforge-nr-launcher missing try/catch on http request](https://github.com/FlowFuse/flowfuse/issues/1096){rel=""nofollow""} - [Invite with + in email address is incorrectly sanitised](https://github.com/FlowFuse/flowfuse/issues/1145){rel=""nofollow""} - [Table does not sort correctly when empty fields are present](https://github.com/FlowFuse/forge-ui-components/issues/59){rel=""nofollow""} - [4xx Errors not shown in App](https://github.com/FlowFuse/flowfuse/issues/929){rel=""nofollow""} - [Inconsistent errors returned from the API](https://github.com/FlowFuse/flowfuse/issues/1076){rel=""nofollow""} - [Module install not working on windows](https://github.com/FlowFuse/flowforge-nr-launcher/issues/77){rel=""nofollow""} - [Avatar lettering is mis-allinged when only rendering 1 character](https://github.com/FlowFuse/flowfuse/issues/1038){rel=""nofollow""} - [it.only is not prohibited](https://github.com/FlowFuse/flowfuse/issues/968){rel=""nofollow""} - [No feedback when an API error occurs editing user](https://github.com/FlowFuse/flowfuse/issues/966){rel=""nofollow""} - [Start action is available on a running project](https://github.com/FlowFuse/flowfuse/issues/1040){rel=""nofollow""} ## Contributors We'd like the thank the following for their contributions to this release: [Jozefik](https://github.com/Jozefik){rel=""nofollow""} for their work on [Adding limits to admin panel](https://github.com/FlowFuse/flowfuse/pull/1082){rel=""nofollow""}. As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Try it out [Sign up for FlowFuse-Managed Premium](https://app.flowfuse.com/account/create?code=RELEASE1){rel=""nofollow""} with this link or at the checkout enter the code **RELEASE1** to get your first project free for a month. As an open source project you can also use [FlowFuse-Community](https://flowfuse.com/docs/install/) for free, forever. ### Upgrading FlowFuse Our managed \[FlowFuse]\({{ site.appURL }}) is already running 1.0. Upgrade your project Stacks to the latest version to make sure you get all the latest changes. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading FlowFuse on a local server](https://flowfuse.com/docs/upgrade/#upgrading-flowfuse). ### Getting help If you hit any problems with the platform, or have questions to ask, please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Customers of FlowFuse Cloud can raise a ticket by emailing We also have a `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. # FlowFuse raises $7.25M Seed Round to bring Node-RED to the Enterprise Since the [launch of FlowFuse](https://flowfuse.com/blog/2021/04/first-deploy/) in April 2021 our goal has been to build a low-code, enterprise-ready development platform based on Node-RED. We wanted to make it easier for enterprises to adopt, integrate, and scale Node-RED into their existing environments. We published our [first version](https://flowfuse.com/blog/2022/01/flowforge-01-released/) at the start of this year and have continued the journey to the [launch of FlowFuse v1.0](https://flowfuse.com/blog/2022/10/flowforge-1-released/) just last week. Each of these releases has moved us towards our goal of creating a platform for integrating the IT and OT for all organizations, built around an open core. ### FlowFuse is Node-RED for the enterprise Node-RED is an incredible tool that has allowed many organizations to quickly and easily integrate both their IT and OT. The sheer range and variety of solutions created with Node-RED today go far beyond what we imagined. But we’ve also seen the challenges faced by companies wanting to take their Node-RED solutions into production and beyond. This is where FlowFuse comes in. It addresses those challenges by adding security, collaboration and deployment capabilities. ### Our $7.25M Seed Round Today, we’re super excited to announce a $7.25M seed round led by Cota Capital, joined by Westwave Capital, Uncorrelated Ventures, and Open Core Ventures. This brings a huge amount of extensive knowledge and experience in IoT, open source, enterprise-ready software solutions. This investment will enable us to continue growing the team and platform to realize our vision of FlowFuse being the best way to achieve enterprise-ready Node-RED. It will enable us to invest back into the core Node-RED project to further its development. One of our core company principles is around Open Source Stewardship - the success of FlowFuse relies on a strong and successful Node-RED community. We’ll be bringing more collaboration features to the FlowFuse platform to enable true collaborative, team-based working. We’ll expand our [Devices offering](https://flowfuse.com/docs/user/concepts/#device) to make it far easier to run Node-RED wherever it suits your use-case, from data center to remote edge locations. Above all, we’ll be building a company that is open, sustainable, and fun to work at. We already have some job openings available - check out our \[jobs board]\({{ site.jobBoard }}) if you’re interested in joining our team. If you’re interested in learning more about what we’re doing, or have any questions, please do [get in touch](https://flowfuse.com/contact-us/)! # Community News October 2022 Welcome to the FlowFuse newsletter for October 2022, a monthly roundup of what’s happening with both FlowFuse and the wider Node-RED community. If you've got something that you'd like us to share please email . [**FlowFuse 1.0 Released**](https://flowfuse.com/blog/2022/10/flowforge-1-released/):br Version 1.0 was released on 27th October. Our latest release represents our vision of the base set of features needed for you to get great value from using FlowFuse in a production environment. That's not to say we are done, we will continue to add features, improve our interfaces and fix bugs with the same enthusiasm as we've worked towards 1.0. We'd like to hear your feedback on what we will be including in [1.1 and beyond](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""}. If you’d like to learn more about what else was included in 1.0 you can do so on our [blog post](https://flowfuse.com/blog/2022/10/flowforge-1-released), on our [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v1.0.0){rel=""nofollow""} and on our [Youtube channel](https://www.youtube.com/watch?v=5TLT7CQR7iI){rel=""nofollow""}. We’d also love for more of you to get involved in the development of FlowFuse, [contributions to the code](https://github.com/FlowFuse/flowfuse/blob/main/CONTRIBUTING.md){rel=""nofollow""} and [bug reports](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""} are really appreciated. [**Node-RED Nears 3.1 Release**](https://github.com/node-red/node-red/milestone/19){rel=""nofollow""} The next release of Node-RED has some great new features including support for [locking flows in the editor](https://github.com/node-red/node-red/pull/3938){rel=""nofollow""} and [improving the user experience around hiding flows](https://github.com/node-red/node-red/pull/3930){rel=""nofollow""}. We'd expect the first 3.1 beta to be available in November with a full release following shortly afterwards. \[\*\*FlowFuse raises [[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[7.25]{.mord}[M]{.mord.mathnormal style="margin-right:0.109em;"}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[b]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[N]{.mord.mathnormal style="margin-right:0.109em;"}[o]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[D]{.mord.mathnormal style="margin-right:0.0278em;"}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[t]{.mord.mathnormal}[h]{.mord.mathnormal}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[i]{.mord.mathnormal}[se]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[∗]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[∗]{.mord}[]]{.mclose}[(]{.mopen}[/]{.mord}[b]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[o]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[/2022/10/]{.mord}[see]{.mord.mathnormal}[d]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[b]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[n]{.mord.mathnormal}[o]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6984em;vertical-align:-0.0833em;"}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[i]{.mord.mathnormal}[se]{.mord.mathnormal}[/]{.mord}[)]{.mclose}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[i]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[t]{.mord.mathnormal}[hi]{.mord.mathnormal}[s]{.mord.mathnormal}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[ee]{.mord.mathnormal}[k]{.mord.mathnormal style="margin-right:0.0315em;"}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[e]{.mord.mathnormal}[ann]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[ce]{.mord.mathnormal}[d]{.mord.mathnormal}[a]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex} 7.25M seed round led by [Cota Capital](https://www.cotacapital.com/knowledgecapital/flowforge-closes-the-gap-between-it-and-ot){rel=""nofollow""}, joined by Westwave Capital, Uncorrelated Ventures, and Open Core Ventures. This brings a huge amount of extensive knowledge and experience in IoT, open source, enterprise-ready software solutions. You can read more about what this investment means for FlowFuse in this [TechCrunch article](https://techcrunch.com/2022/11/03/flowforge-nabs-7-2m-to-help-companies-integrate-iot-using-node-red){rel=""nofollow""}. [**Node-RED Dashboard - Beginners Guide**](https://stevesnoderedguide.com/node-red-dashboard){rel=""nofollow""}:br It's great to see members of the Node-RED community taking their personal time to help us all build better projects. This write up takes you through the basics of creating your first Dashboard through to more advanced techniques to help your interfaces look professional and provide great user experiences. [**FlowFuse Team News**](https://flowfuse.com/about/):br We are currently recruiting [NodeJS Developers](https://boards.greenhouse.io/flowfuse/jobs/4463977004){rel=""nofollow""}, a [Product Manager](https://boards.greenhouse.io/flowfuse/jobs/4717778004){rel=""nofollow""}, a [Recruiter / PeopleOps Manager](https://boards.greenhouse.io/flowfuse/jobs/4687876004){rel=""nofollow""} and a [Senior Community Manager](https://boards.greenhouse.io/flowfuse/jobs/4700809004){rel=""nofollow""}. You can view any of the roles we currently have open and apply on our [Jobs page](https://boards.greenhouse.io/flowfuse){rel=""nofollow""}. We'd also like to welcome [Pez Cuckow](https://github.com/Pezmc){rel=""nofollow""} who joined FlowFuse as a Senior Software Engineer in October. [**Try FlowFuse for Free**](https://app.flowfuse.com/account/create?code=RELEASE1){rel=""nofollow""}:br As a thank you for reading our newsletters we’d like to offer you a free, small project for one month on our managed FlowFuse platform when you create a new team. To get this discount please follow [this link](https://app.flowfuse.com/account/create?code=RELEASE010){rel=""nofollow""} or use the code RELEASE1 when on the payment page after creating a new team. As an open source project you can also use [FlowFuse](https://flowfuse.com/docs/install/) for free, forever. # FlowFuse 1.1 released with persistent file storage Persist files on your FlowFuse Projects, publish locally developed flows to dozens of Devices in a few clicks, and use our new interface for managing Project Deployments. We're pleased to announce version 1.1 is now available! The latest release of the FlowFuse application contains new features, many improvements, and bug fixes. Keep reading for the details of what's in this release or you can watch our 1 minute roundup video of the new release above. ## Features [Persistent File Storage](https://github.com/FlowFuse/flowfuse/issues/998){rel=""nofollow""} We've had a great deal of feedback from our customers that being able to persist files in a project is a vital feature in Node-RED. In FlowFuse 1.1 flows can now create and persist files within your Projects. We know those files are used in many creative ways and we're looking forward to seeing how users improve their Projects using this new feature. [Import Snapshots from Outside FlowFuse](https://flowfuse.com/docs/migration/node-red-tools/) Developers may wish to work on Node-RED in a local environment but want an easy path to share that with their team. You can now link your Node-RED instances running outside of FlowFuse and push Snapshots directly into your FlowFuse Projects to leverage FlowFuse fully. With this new feature we've made it effortless to push a local build of a project to FlowFuse for deployment to your staging and production FlowFuse instances. ## Improvements [Project Deployments UX](https://github.com/FlowFuse/flowfuse/issues/1046){rel=""nofollow""} We've reworked the interface for managing your FlowFuse Deployments of Node-RED. We are seeing FlowFuse users deploying their Projects to edge devices at scale. This is another step towards making it easier for users to manage a large quantity of devices in their Projects. When users change their username, email address, or password they'll now be notified through email of changes to ensure they were made by the user. In this release a lot of effort went into the install process, specifically the local install method. First and foremost; a default Stack and Template will be installed automatically. That will ensure users get up and running with Node-RED more quickly. Administrators of each platform can still change Stacks and Templates when needed. Secondly, the installer now auto generates the configuration file for Mosquitto, the MQTT broker FlowFuse uses. This again should save administrators time when installing FlowFuse. ## Bug Fixes The v1.0.1 release included a bug fix where [snapshot rollbacks](https://github.com/FlowFuse/flowfuse/issues/1186){rel=""nofollow""} didn't work, which has also been included in v1.1 onwards. We've fixed the following bugs in this release. - When installing the stack during a FlowFuse installation the process would quit on Windows [#62](https://github.com/FlowFuse/installer/issues/62){rel=""nofollow""} - After accepting an invite to join a team, users are no longer seeing a blank page [#1208](https://github.com/FlowFuse/flowfuse/issues/1208){rel=""nofollow""} - Pagination on device deployments wasn't showing all devices [#1207](https://github.com/FlowFuse/flowfuse/issues/1207){rel=""nofollow""} - Markdown rendering when selecting the project type wasn't quite working, fixed now! [#1171](https://github.com/FlowFuse/flowfuse/issues/1171){rel=""nofollow""} - Continuous spinner in UI body when entering a new (short) password [#1280](https://github.com/FlowFuse/flowfuse/issues/1280){rel=""nofollow""} - Friendly stack name not shown in the Change Project Stack option list [#1169](https://github.com/FlowFuse/flowfuse/issues/1169){rel=""nofollow""} - The stacks view in the admin area didn't render properly [#1260](https://github.com/FlowFuse/flowfuse/issues/1260){rel=""nofollow""} - Like the deployments page, pagination for stacks was broken. [#1164](https://github.com/FlowFuse/flowfuse/issues/1164){rel=""nofollow""} - Several UX and UI bugs got polished away! ## Contributors We'd like the thank the following for their contributions to this release: [mikermcneil](https://github.com/mikermcneil){rel=""nofollow""} for their work on [#1301](https://github.com/FlowFuse/flowfuse/pull/1301){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Try it out As said before, a lot of effort went into the local installer. We're confident you can have your own FlowFuse running locally in about 30 minutes. [Get started right away!](https://flowfuse.com/docs/contribute/local/) ([Docker](https://flowfuse.com/docs/install/docker/) and [Kubernetes](https://flowfuse.com/docs/install/kubernetes/) are available too!) If you'd rather use our hosted offering: [Sign up for FlowFuse Cloud](https://app.flowfuse.com/account/create?code=RELEASE11){rel=""nofollow""} with the coupon **RELEASE11** to get your first project free for a month. ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.1. To use persisted files you'll need to upgrade your projects stack. You'll be prompted to do so on the project page. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/) ### Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. You can also raise a support ticket by emailing # Re-spin of Docker-Compose install package After [yesterdays 1.1.0 FlowFuse release](https://flowfuse.com/blog/2022/11/flowforge-1-1-released/) we noticed a few minor issues with the docker-compose install instructions. We had relied on the fact we are now publishing container images to Docker Hub to install the new `flowforge/file-server` container. At this time we are only building images for amd64 and arm64. Further, it was only tagged them with the current release number. To remedy this we tagged v1.1.1 of the docker-compose project in which we have included the required `Dockerfile` and resources to build the `flowforge/file-server` locally, updated the `build-containers.sh` script to build this container. We will also build the published containers for armv7 and include a latest tag going forward. # Challenges scaling Node-RED with DIY tooling In this post, I'm going to share some of the challenges customers face when scaling Node-RED with Do-It-Yourself tooling. Specifically, we'll talk about common threads in their journey building their own tooling around Node-RED, its flows, and deploying them. Node-RED is a visual programming environment for wiring together hardware devices, APIs and online services in a single application. It's great because it's flexible enough to be used by both beginners and experts alike; however, going from one instance of Node-RED to 100 isn't for the faint-hearted. ### Zero to hero in a few days If you’re new to Node-RED and are just getting started, the first step is simple: go to [nodered.org](https://nodered.org){rel=""nofollow""}, download, install and run. There’s no need to configure anything or set up credentials or security or alerts. You can get started with Node-RED straight away by simply running it on your machine. The guides and scripts provided on Node-RED or are more than enough to get started. You'll dive right in and start developing your flows. ### Onto the second instance The next instance is a simple copy of the first. You'll need to make sure that you are installing the same version on both instances, and that any configuration files (such as the `settings.js`) are also in sync. The second instance improves the first equally. Docs are read again, improvements are made, and copied over. Life's good! Although there's a slight itch to start automating the setup, it's ignored. Just too many open questions on how to achieve it: write scripts in bash? Or can we use Node-RED to manage Node-RED? Automation can wait, there's new flows to implement! ### Wake-up call; how many of them do we have? There's nothing like a good ol' wake-up call to get you back on track. One of our team members asked a questions about a buggy flow you've got no recollection of. During the investigation there's issues left and right; it's running a much older version, missing standard packages, the settings are out of date, and the timezone not set to UTC? Adoption of Node-RED is going quite well, it's useful and effective. Gets the job done without fuss. But now there's toil in maintaining them; ensure the right tooling is build, properly documented, it needs dashboards and overviews, lots of work to be done. Not quite business related, but Node-RED is important for the company, the bosses sure would approve spending 2 months building these tools! But then something happened, there was a higher priority project to be picked up. Some tooling could be written for a couple of hours a month, but not two dedicated months to get it in tip top shape. Better than nothing! Built a dashboard in Node-RED to keep track of all devices, there's some scripts and a few flows that aid in monitoring and maintenance. Scaling further is possible, but confidence in the tooling isn't sky high. ### Data extraction at scale; now there's over 100 At [FlowFuse](https://flowfuse.com) we've got regular conversations with customers managing 100s of devices. Scaling to that many devices and runtimes requires hours of development each week alongside monitoring, maintenance, and auditing. As a side effect of investing more time into Node-RED and its ecosystem the organization has developed a few standards. Standard custom nodes that are pre-installed (👋 `moment.js`), a style guide published for developing flows, maybe even flow linting: {rel=""nofollow""} The security model is fairly decent. One just hopes the [CISO](https://nl.wikipedia.org/wiki/Chief_Information_Security_Officer){rel=""nofollow""} doesn't inspects them, but it's a fair bet they won't; we're far away from the headquarters, right? Many other edge cases itch in the back of our heads, but we can't focus on those right now. ### Conclusion Node-RED is a great tool, it's got many built-in features that make it easy to get started with no coding experience necessary. Running it at scale, in a production environment can require a lot of sys-ops and dev-ops time and we think [FlowFuse](https://flowfuse.com) is a great solution to keep that admin and tech debt in check. If you're running into these challenges we believe we can help, you can adopt our [free and open source edition](https://flowfuse.com/docs/install/). Additionally, to get a head start or enhance your current setup, explore our [Beginner's Guide to Professional Node-RED](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/). This comprehensive ebook offers a clear overview of Node-RED’s capabilities and practical tips for making the most of its features. # Community News November 2022 Welcome to the FlowFuse newsletter for November 2022, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. If you've got something that you think we should share on our newsletters please [get it touch](mailto\:contact@flowfuse.com). [**Node-RED Nears 3.1 Release**](https://github.com/node-red/node-red/milestone/19){rel=""nofollow""} As we mentioned last month, the release of Node-RED 3.1 is expected very soon. 3.1 includes lots of great new features such as support for [locking flows in the editor](https://github.com/node-red/node-red/pull/3938){rel=""nofollow""} and [improving the user experience around hiding flows](https://github.com/node-red/node-red/pull/3930){rel=""nofollow""}. As an open source project the development of Node-RED is entirely dependent on individuals and companies giving their time to work towards each new release. If you'd like to know how you can get involved you can read more on the [Node-RED web site](https://nodered.org/about/contribute/){rel=""nofollow""}. [**FlowFuse 1.1 Released**](https://flowfuse.com/blog/2022/11/flowforge-1-1-released/):br Version 1.1 of FlowFuse was released on 24th November. Our latest release included some great new features such as [Persistent file storage](https://github.com/FlowFuse/flowfuse/issues/998){rel=""nofollow""}, the ability to [import Node-RED snapshots](https://flowfuse.com/docs/migration/node-red-tools/) from outside of FlowFuse and a much improved interface to [deploy projects to your devices](https://github.com/FlowFuse/flowfuse/issues/1046){rel=""nofollow""}. We're now working towards our final release of 2022 which is due just before Christmas. You can see what we are planning to deliver in that release and beyond on [FlowFuse's project board](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""}. If you’d like to learn more about what else was included in 1.1 you can do so on our [blog post](https://flowfuse.com/blog/2022/11/flowforge-1-1-released), [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v1.1.0){rel=""nofollow""}, and [Youtube channel](https://www.youtube.com/watch?v=134iljE_urI){rel=""nofollow""}. [**Node-Redscape - 100% Free, Open-Source Escape Room Control Software**](https://github.com/playfultechnology/node-redscape){rel=""nofollow""}:br As we'll come to later in this newsletter, FlowFuse visited an Escape Room in Winchester as part of our team meet-up. Co-incidentally, a great Node-RED project came up a few days after our visit which we thought was worth sharing. In their own words [Node-Redscape](https://github.com/playfultechnology/node-redscape){rel=""nofollow""} 'provides a set of templates, flows, and examples that turn Node-RED into a complete Escape Room automation system'. Very topical for us and also seems like a great project. You can learn more about the project on [Youtube](https://www.youtube.com/watch?v=f9yYDxqK_2E){rel=""nofollow""} as well as on [Github](https://github.com/playfultechnology/node-redscape){rel=""nofollow""}. **FlowFuse team meetup** FlowFuse is a fully remote team, we currently have a strong skew towards western Europe but we are in the process of adding team members in each of the continents. On that point, any great Product Managers who live in Antarctica are encouraged to [apply for a job with us](https://boards.greenhouse.io/flowfuse/jobs/4717778004){rel=""nofollow""}! Remote work is great but it's also valuable to get everyone together in the real world from time to time. FlowFuse had such a meet up last month in Winchester, UK. We came from near and far and took the opportunity to have productive round-table discussions about some features we are working towards in 1.2 and beyond. We also dropped into [Clue Capers](https://cluecapers.co.uk/){rel=""nofollow""}, a great Escape Room in the center of Winchester who provided the photo below to mark the occasion. !["The FlowFuse team pictured during our visit to Clue Capers"](https://flowfuse.com/blog/2022/12/images/clue-capers.jpg "The FlowFuse team pictured during our visit to Clue Capers") [**FlowFuse Team News**](https://flowfuse.com/about/):br We are currently recruiting a [Product Manager](https://boards.greenhouse.io/flowfuse/jobs/4717778004){rel=""nofollow""}, and a [Senior Community Manager](https://boards.greenhouse.io/flowfuse/jobs/4700809004){rel=""nofollow""}. You can view any of the roles we currently have open and apply on our [Jobs page](https://boards.greenhouse.io/flowfuse){rel=""nofollow""}. [**Try FlowFuse for Free**](https://app.flowfuse.com/account/create?code=RELEASE11){rel=""nofollow""}:br As a thank you for reading our newsletters we’d like to offer you a free, small project for one month on our managed FlowFuse platform when you create a new team. To get this discount please follow [this link](https://app.flowfuse.com/account/create?code=RELEASE11){rel=""nofollow""} or use the code RELEASE11 when on the payment page after creating a new team. As an open source project you can also use [FlowFuse](https://flowfuse.com/docs/install/) for free, forever. # Create HTTP triggers with authentication Having an HTTP endpoint trigger your flows is very useful. From any browser or command line you now have the ability to trigger your flows. Doing so safely with authentication is slightly harder, but not a lot. FlowFuse makes it rather easy to accomplish. ### Creating the HTTP flow When you start a project on FlowFuse, remember the project name. For this how-to we’ll use `example`. Open the editor and drag in the HTTP In node as well as the HTTP response node. Connect them, and add a debug node, which is connected to the “HTTP in” node. First off; let’s set the HTTP in node properties: ![Shows the UI to edit the node's properties](https://flowfuse.com/blog/2022/12/images/edit-http-node.png "Shows the UI to edit the node's properties") You can import this flow into your own project if you’d like: ```text [{"id":"4faa84d37a52bb28","type":"group","z":"3c6e2dc732ada815","name":"Allow HTTP Post request to trigger a flow","style":{"label":!0},"nodes":["1fa26e0ed3ddec1a","45a180052e1a2f43","09347881f4fa4057"],"x":34,"y":79,"w":472,"h":122},{"id":"1fa26e0ed3ddec1a","type":"http in","z":"3c6e2dc732ada815","g":"4faa84d37a52bb28","name":"HTTP Trigger","url":"/http-trigger","method":"post","upload":!1,"swaggerDoc":"","x":130,"y":120,"wires":[["45a180052e1a2f43","09347881f4fa4057"]]},{"id":"45a180052e1a2f43","type":"http response","z":"3c6e2dc732ada815","g":"4faa84d37a52bb28","name":"Empty HTTP response","statusCode":"200","headers":{},"x":360,"y":120,"wires":[]},{"id":"09347881f4fa4057","type":"debug","z":"3c6e2dc732ada815","g":"4faa84d37a52bb28","name":"Print HTTP Request","active":!0,"tosidebar":!0,"console":!1,"tostatus":!1,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":360,"y":160,"wires":[]}] ``` Now I’ve opened a terminal and executed: ```text curl -X POST https://example.flowforge.com/http-trigger ``` When there’s no output, that means it’s all good! There should be an empty message in the debug console in the Node-RED editor though ### Securing the HTTP trigger with a username and password The problem with our trigger is that anyone with internet access could trigger it. That’s not a great idea. So let’s secure this endpoint with HTTP Basic Authentication. There are various ways to include a secure endpoint in Node-RED, we’ve built authentication directly into FlowFuse to make it easier for all users. On the FlowFuse project, go to settings and then to ‘Editor’. Under the section HTTP Auth you can set a username and password. You should generate both by a random string generator, and store the credentials somewhere safe. Restart the project to have the runtime pick up the changes, and the endpoint is secured! Let’s validate the endpoint that worked a minute ago doesn’t anymore: ```text curl -X POST https://example.flowforge.cloud/http-trigger => Unauthorized ``` Let’s get it working again: (replace `` and `` with the details from the sticky note) ```text curl -X POST https://:@example.flowforge.cloud/http-trigger ``` That’s it! You now have a flow that’s protected by a username and password combination! # FlowFuse 1.1.2 released We've published a maintenance fix with an important fix for the Palette Manager in Node-RED. A [bug](https://github.com/FlowFuse/flowfuse/issues/1367){rel=""nofollow""} was reported this week where a user was unable to install additional nodes into their Node-RED project using the editor's palette manager. We tracked it down to an issue that was introduced in the 1.1 release, where the project template lets you list nodes that should be blocked from being installed. It was interpreting an empty list to mean *disallow everything*! Not quite the intended behaviour. Whilst tracking this down, we also spotted a bug ([#1379](https://github.com/FlowFuse/flowfuse/issues/1379){rel=""nofollow""}) around editing this same setting that made it tricky to work around without this release being published. These issues have now been fixed and FlowFuse 1.1.2 published. ## Bug Fixes In additional to the above issues, this release includes some further fixes around the docker and helm components: - Fix fileStore hostname by @flecoufle in [#59](https://github.com/FlowFuse/docker-compose/pull/59){rel=""nofollow""} - Fix healthcheck [#62](https://github.com/FlowFuse/docker-compose/pull/62){rel=""nofollow""} [#63](https://github.com/FlowFuse/docker-compose/pull/63){rel=""nofollow""} [#74](https://github.com/FlowFuse/helm/pull/74){rel=""nofollow""} ## Contributors We'd like to thank the following people for their contributions to this release: [flecoufle](https://github.com/flecoufle){rel=""nofollow""} for their work on [#59](https://github.com/FlowFuse/docker-compose/pull/59){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Upgrading FlowFuse This release has already been rolled out to \[FlowFuse Cloud]\({{ site.appURL }}). If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/) ### Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform, please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. You can also raise a support ticket by emailing # FlowFuse 1.2 is now available with single sign on and persistent context storage Control access to FlowFuse using single sign-on and retain context values when restarting projects. We're pleased to announce version 1.2 is now available! The latest release of the FlowFuse application contains new features, improvements, better documentation, and bug fixes. We've put a great deal of work in this release to make it easier to run your own self-managed instance of FlowFuse. That includes significant improvements for FlowFuse in [Kubernetes](https://flowfuse.com/docs/install/kubernetes/) and [Docker](https://flowfuse.com/docs/install/docker/). Keep reading for the details of what's in this release or you can watch our 1 minute roundup video of the new release above. ## Features [Single Sign-On](https://github.com/FlowFuse/flowfuse/issues/226){rel=""nofollow""} Single sign-on (SSO) is a method of authentication that allows a user to access multiple applications or systems with a single set of login credentials, improving security, productivity, and user experience, and reducing IT overhead. We've implemented SSO using the Security Assertion Markup Language [(SAML)](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language){rel=""nofollow""} framework. This allows users of FlowFuse Cloud, Premium and the open source edition to easily access their FlowFuse projects. [Persistent Context](https://github.com/FlowFuse/flowfuse/issues/212){rel=""nofollow""} Node-RED provides a way to store information that can be shared between different nodes and flow executions without using the messages that pass through a flow. This is called ‘context’. You can now select if context should be stored in memory or in persistent storage. Persistent storage allows the stored values to be recalled after a restart of your project. You can see a demonstration of this feature on our [Youtube channel](https://youtu.be/ma2vYrXmssc){rel=""nofollow""}. ## Improvements - In FlowFuse 1.1 we added logging of user actions. In 1.2 we’ve [improved the audit log interface](https://github.com/FlowFuse/flowfuse/issues/517){rel=""nofollow""} to help you read the recorded user actions. !["An image of the new audit log interface of FlowFuse"](https://flowfuse.com/blog/2022/12/images/audit-log.png "An image of the new audit log interface of FlowFuse") - Configuring DNS for FlowFuse has historically been challenging as for most FlowFuse installs you'll need two entries. One for the FlowFuse application, and one for the Node-RED projects. There's been updates to the documentation to make it much easier to set this up, and much faster. Please checkout the new [documentation](https://flowfuse.com/docs/install/dns-setup/). - We've updated our documentation to always link to the latest build (older builds are still available). - Previously customers were asked to build their own containers for the main FlowFuse applications, as well as the Node-RED ones. For the Node-RED containers this allows customers to pre-install packages in the container they intent to use. For FlowFuse Cloud these containers are build by FlowFuse. These containers are now published to the [Docker Hub](https://hub.docker.com/u/flowforge){rel=""nofollow""}. This makes it much easier to get up and running with your first containers. - We are now pushing our Docker builds to Docker Hub, this saves users from having to build the Docker images when installing or updating. These containers are used by default by `docker-compose`. - Setting up MQTT for inter-project communication and communication with devices has been simplified. Please read the improved the documentation around configuration of [MQTT](https://github.com/FlowFuse/flowfuse/issues/1397){rel=""nofollow""}. ## Bug Fixes We've fixed the following bugs in this release. - Unable to edit 'Prevent Install of External nodes' template option [#1376](https://github.com/FlowFuse/flowfuse/issues/1376){rel=""nofollow""} - Self-managed FlowFuse needs an external email server to deliver email to users. FlowFuse should be able deal with the email server being offline and gracefully recover once it is back online. [#1159](https://github.com/FlowFuse/flowfuse/issues/1159){rel=""nofollow""} - Duplicate Activity Log for Project whose state is in flight [#1461](https://github.com/FlowFuse/flowfuse/issues/1461){rel=""nofollow""} ## Contributors We'd like the thank the following for their contributions to this release: [flecoufle](https://github.com/flecoufle){rel=""nofollow""} for their work on [#59](https://github.com/FlowFuse/docker-compose/pull/59){rel=""nofollow""} [sumanpaikdev](https://github.com/sumanpaikdev){rel=""nofollow""} for their work on [#53](https://github.com/FlowFuse/docker-compose/pull/53){rel=""nofollow""} [sdirosa](https://github.com/sdirosa){rel=""nofollow""} for their work on [#1326](https://github.com/FlowFuse/flowfuse/pull/1326){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ### Try it out In 1.2 we've continued to improve the experience of running your own self managed FlowFuse installation. We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install our [local build](https://flowfuse.com/docs/contribute/local/), through [Docker](https://flowfuse.com/docs/install/docker/), or [Kubernetes](https://flowfuse.com/docs/install/kubernetes/). If you'd rather use our hosted offering: [Sign up for FlowFuse Cloud](https://app.flowfuse.com/account/create?code=FF12){rel=""nofollow""} with the coupon **FF12** to get your first project free for a month. ### Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.2. To use persistent context you'll need to upgrade your projects stack. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ### Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. You can raise a support ticket by emailing . We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. # Configure FlowFuse in Docker to secure all traffic Following on from our [previous article](https://flowfuse.com/blog/2022/10/ff-docker-gcp/) in which we covered how to run FlowFuse in Docker on Google’s Cloud Platform, today we are going to look at how to secure HTTP traffic to your FlowFuse server. ### Introduction When we wrote the first part of this series FlowFuse didn't have an easy path to secure HTTP traffic. Happily, two versions of FlowFuse later and at least partially inspired by these blogs, we have added the configuration you need in Docker to use HTTPS with minimal work. That addition makes our job of explaining this setup a lot easier, credit to our developers for seeing the value of having an easy implementation of HTTPS in FlowFuse as part of our [1.0 build](https://flowfuse.com/blog/2022/10/flowforge-1-released/). To achieve secure HTTPS traffic we are employing a great service called [Let's Encrypt](https://letsencrypt.org/){rel=""nofollow""}. In their own words, "Let’s Encrypt is a free, automated, and open certificate authority (CA), run for the public’s benefit". In practice Let's Encrypt will allow us to easily secure HTTPS traffic. We are also utilising a Docker image called [acme-companion](https://github.com/nginx-proxy/acme-companion){rel=""nofollow""} which makes the configuration of Let's Encrypt a breeze. To quote the project's own Github page "acme-companion is a lightweight companion container for nginx-proxy. It handles the automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol". It's a great project and credit to the team over there for making it a lot easier to secure the internet. Now we've covered our goals and the tools we are going to use let's configure our existing GCP VM to secure all web traffic. ### Prerequisites As mentioned above, you will need to be running FlowFuse version 1.0 or higher to follow this guide. If you are using an older version you can upgrade now using the [instructions here](https://flowfuse.com/docs/upgrade/). ### Update Docker Compose The first step is to edit our Docker compose file. We're using Nano again to edit files so we will run this command: ```text sudo nano /opt/flowforge/docker-compose-1.1.1/docker-compose.yml ``` In the docker-compose.yml file, un-comment the following lines: ```yaml - "./certs:/etc/nginx/certs" ``` ```yaml - "443:443" ``` ```yaml acme: image: nginxproxy/acme-companion volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./acme:/etc/acme.sh" volumes_from: - nginx:rw environment: - "DEFAULT_EMAIL=mail@example.com" depends_on: - "nginx" ``` We should also redirect all traffic to use HTTPS, to do that un-comment the following in the nginx service section: ```yaml Environment: - "HTTPS_METHOD=redirect" ``` We now need to add the configuration for LetsEncrypt, edit the following lines to include a valid email address and the correct domain for where you are hosting your FlowFuse server: ```yaml - "DEFAULT_EMAIL=mail@example.com" ``` ```yaml - "LETSENCRYPT_HOST=mqtt.example.com" ``` ```yaml - "LETSENCRYPT_HOST=forge.example.com" ``` Save and exit from that file, in Nano you can do that by pressing ‘control x’ then ‘y’ then the Return key. ### Update flowforge.yml Next, we need to edit the public\_url for the MQTT broker: ```text sudo nano /opt/flowforge/docker-compose-1.1.1/docker-compose.yml ``` Then replace ws\:// with wss\:// ```yaml public_url: wss://mqtt.flowforge-demo.com ``` Save and exit from that file, in Nano you can do that by pressing ‘control x’ then ‘y’ then the Return key. ### Restart your Docker containers OK, we should be ready to restart the Docker containers, run the command: ```text sudo docker compose -p flowforge up -d ``` If you reload your FlowFuse root directory in a web browser you should now see that your traffic is encrypted using LetsEncypt. ![A screenshot from Safari web browser showing that the traffic to FlowFuse is encrypted](https://flowfuse.com/blog/2022/12/images/https-working.png "A screenshot from Safari web browser showing that the traffic to FlowFuse is encrypted") Nice! That’s it, you can now access your FlowFuse installation securely. In the next and final part of this series of articles, we are going to look at how we can actually use FlowFuse including how to build flows and deploy and update them on Devices linked to a project. # FlowFuse Inc. becomes a member of the OpenJS Foundation We're pleased to share the news we've joined the OpenJS Foundation to bolster our support for the Node-RED community. One of our [founding principles](https://flowfuse.com/handbook/company/principles/#open-source-stewardship) is the importance of Open Source being at the core of what we do. Node-RED has been part of the [OpenJS Foundation](https://openjsf.org/){rel=""nofollow""} since it was formed in 2019. The Foundation provides the project a vendor-neutral home and enables its open governance model that allows anyone to get involved. It also provides some vitally important resources to the community, such as the hosting for the [community forum](https://discourse.nodered.org){rel=""nofollow""}, which regularly gets over 1.5 Million page views a month. With my Node-RED Project Lead hat on, I regularly see the impact the foundation has had on the growth of the Node-RED community. These are things that may not always be obvious to our end users, but we wouldn't be where we are today with them. The success of what we're building with FlowFuse relies on a strong and healthy Node-RED community. Becoming members of the Foundation allows us to provide more yet more support to Node-RED and the wider JavaScript communities represented by the foundation. For more information about this news, check out the [OpenJS Foundation blog post](https://openjsf.org/announcement/2022/12/13/welcoming-flowforge-to-the-openjs-foundation/){rel=""nofollow""}. # Format your Node-RED flows for better team collaboration When it comes to working on Node-RED flows as part of a team, there are a few best practices that can make things go more smoothly. From formatting your flows for readability to providing clear comments on nodes and groups, a little bit of effort upfront can save your team a lot of headaches down the road. In this post, we'll cover some of the main things to keep in mind when working on Node-RED flows as part of a team. ### Give your groups descriptive names Let’s start with [grouping your flows](https://nodered.org/docs/user-guide/editor/workspace/groups){rel=""nofollow""} and giving each group a clear explanation of what it does. Compare the first to the second example below and consider how much more quickly you can understand what the flow is doing. #### This is not helpful, 'Time' doesn't tell you enough to understand the flow's purpose. !["Screenshot showing the example of flow having the bad group name"](https://flowfuse.com/blog/2022/12/images/name-bad.png "Screenshot showing the example of flow having the bad group name") #### This is much better, we know what the flow is doing without inspecting the nodes. !["Screenshot showing the example of flow having the good group name"](https://flowfuse.com/blog/2022/12/images/name-good.png "Screenshot showing the example of flow having the good group name") ### Explain what your switches do Try to make it obvious what each switch does without having to open the node editor. Ask a question in the switch's name and make a positive answer the top connection out. #### This is not easy to understand, what does the switch do? !["Screenshot showing the example of flow having the switch with bad name"](https://flowfuse.com/blog/2022/12/images/switch-bad.png "Screenshot showing the example of flow having the switch with bad name") #### This is a lot better, we can see that the top debug should be triggered. !["Screenshot showing the example of flow having the switch with good name"](https://flowfuse.com/blog/2022/12/images/switch-good.png "Screenshot showing the example of flow having the switch with good name") ### Where possible your flows should work down the canvas It makes it so much easier to understand what happens and in which order if your flows start at the top of the canvas and work down to the bottom. #### This is almost unreadable, it's very hard to work out the order of the groups. !["Screenshot showing an example of flow that doesn't work down the canvas"](https://flowfuse.com/blog/2022/12/images/flowdown-bad.png "Screenshot showing an example of flow that doesn't work down the canvas") #### Where as this is so much easier to understand. !["Screenshot showing an example of flow that works down the canvas"](https://flowfuse.com/blog/2022/12/images/flowdown-good.png "Screenshot showing an example of flow that works down the canvas") ### Use link nodes rather than wires to join groups Groups should not be joined using wires, it just looks untidy and quickly reduces readability of your flows. #### The wire is blocking the title, it only gets worse as you add more wires. !["Screenshot showing an example of flow with wires blocking group titles"](https://flowfuse.com/blog/2022/12/images/link-bad.png "Screenshot showing an example of flow with wires blocking group titles") #### You can see the group titles easily now. !["Screenshot showing an example of flow with link nodes improving readability"](https://flowfuse.com/blog/2022/12/images/link-good.png "Screenshot showing an example of flow with link nodes improving readability") ### Keep your groups compact Keeping your groups compact will save time when reading the flow. This is especially helpful if when viewed on a smaller screen. #### Consider how hard a flow made of groups spaced out like this would be to read on a smaller laptop screen. !["Screenshot showing an example of flow with widely spaced groups"](https://flowfuse.com/blog/2022/12/images/compact-bad.png "Screenshot showing an example of flow with widely spaced groups") #### This now takes up less space and is arguably easier to read on any screen size. !["Screenshot showing an example of flow with compact groups"](https://flowfuse.com/blog/2022/12/images/compact-good.png "Screenshot showing an example of flow with compact groups") ### Don’t cross ~~beams~~ wires Crossed wires are not only hard to read, they can lead to misinterpretation of what a flow actually does. Where possible don’t cross your wires, where you can’t avoid it try to make sure it’s easy for the reader to understand where wires cross as rather than join. #### This is confusing, which change node does the top switch output link to? !["Screenshot showing an example of the flow with nodes having crossed beams/wires"](https://flowfuse.com/blog/2022/12/images/wires-bad.png "Screenshot showing an example of the flow with nodes having crossed beams/wires") #### This is better, much less chance of confusing the change nodes. !["Screenshot showing an example of the flow with nodes having correctly linked beams/wires"](https://flowfuse.com/blog/2022/12/images/wires-good.png "Screenshot showing an example of the flow with nodes having correctly linked beams/wires") ### Don’t use link nodes in groups where avoidable Excessive link nodes within groups can make a flow much harder to understand, where possible use wires to join nodes within a group. #### This is hard to read and you will end up checking the link nodes again and again. !["Screenshot showing the example of flow having the uneccessary link nodes"](https://flowfuse.com/blog/2022/12/images/groupwires-bad.png "Screenshot showing the example of flow having the uneccessary link nodes") #### Functionally identical to the example above, it should only take a few seconds to understand this flow now. !["Screenshot showing an example of the flow with the avoided link nodes"](https://flowfuse.com/blog/2022/12/images/groupwires-good.png "Screenshot showing an example of the flow with the avoided link nodes") ### Boost Collaboration with FlowFuse [FlowFuse](https://flowfuse.com) is a cloud-based platform that makes working together on Node-RED projects easier and more efficient. It’s trusted by industries like manufacturing and smart building management, as well as textiles, to improve their systems. For more information, refer to our [customer stories](https://flowfuse.com/customer-stories/). With FlowFuse, you can quickly [set up and manage teams](https://flowfuse.com/docs/user/team/), giving each member the right level of access. It keeps all your [Node-RED instances organized in one place](https://www.youtube.com/watch?v=KOnQnR7yfT0&list=PLpcyqc7kNgp3nRacWBJ9JUVUJqtTjXdvh&index=2){rel=""nofollow""}, so your team can collaborate seamlessly. Plus, it features [snapshots](https://www.youtube.com/watch?v=m2Onip4Lf4w){rel=""nofollow""}, which let you restore previous versions of your flows if something goes wrong. FlowFuse simplifies team collaboration, making it easier to manage and work on Node-RED projects. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and experience FlowFuse's powerful collaboration tools!** ### Conclusion Working on Node-RED flows as part of a team doesn't have to be a headache. By following some simple best practices you can make collaboration smooth sailing for everyone involved. So next time you're starting work on a new Node-RED flow, remember these tips and make life easier for yourself and your teammates. # Why you need FlowFuse when you already have Node-RED? Many organizations face challenges in IT/OT convergence. There are many protocols, disparate devices, and everything is a brownfield project. Furthermore, skilled engineers are hard to find. Many organizations find themselves maintaining systems and integrations, and have little time left to improve continuously. This is why Node-RED is adopted so widely, it makes a world of difference through two of its properties: Low-Code Development and the ability to integrate with a wide range of hardware and software systems. Node-RED makes it easy to program data collection, then use that data for decision making, and provide feedback to both your digital platform and the physical reality of your business. Furthermore, Node-RED is unrivalled in making data accessible. The data previously locked in a walled garden of your hardware providers is now accessible and obtainable. With that, more data is available and better decisions are made, faster. FlowFuse makes developer collaboration, flow deployment, and scaling of infrastructure easy when working with Node-RED. It offers an intuitive user interface for creating, deploying, monitoring, and managing multiple Node-RED projects. FlowFuse also provides one-click deployment to thousands of devices, making it easy to manage large-scale environments. These features make FlowFuse a valuable tool for organizations using Node-RED to build applications and automate processes. Enabling for further adoption of Node-RED to integrate more systems, for even better decisions. This not only reduces operational costs but it also allows an organization to be far more agile with their Node-RED projects than was possible without FlowFuse. We would be happy to talk to you more about how FlowFuse can help you get the most value from Node-RED. Please [contact us](https://flowfuse.com/contact-us/) to learn more. # Community News December 2022 Welcome to the FlowFuse newsletter for December 2022, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. 2022 was a really exciting year for the development of both Node-RED and FlowFuse. Node-RED released version 3, another major milestone for the project. FlowFuse hit version 1, leaving the beta development stage. We are excited to see what the community can achieve in 2023! If you've got something that you think we should share on our newsletters please [get it touch](mailto\:contact@flowfuse.com). [**Node-RED Nears 3.1 Release**](https://github.com/node-red/node-red/milestone/19){rel=""nofollow""} The release of Node-RED 3.1 is expected very soon. 3.1 includes lots of great new features such as support for [locking flows in the editor](https://github.com/node-red/node-red/pull/3938){rel=""nofollow""} and [improving the user experience around hiding flows](https://github.com/node-red/node-red/pull/3930){rel=""nofollow""}. As an open source project the development of Node-RED is entirely dependent on individuals and companies giving their time to work towards each new release. If you'd like to know how you can get involved you can read more on the [Node-RED web site](https://nodered.org/about/contribute/){rel=""nofollow""}. [**FlowFuse 1.2 Released**](https://flowfuse.com/blog/2022/12/flowforge-1-2-0-released/):br Version 1.2 of FlowFuse was released on 23rd December. Our last release of the year included some great new features such as [Single Sign-On](https://flowfuse.com/docs/cloud/introduction/#single-sign-on) to make it easier for your team to access your projects and [Persistent Context](https://flowfuse.com/docs/cloud/introduction/#node-red-context) which allows you to retain context values even when restarting projects. We're now working towards our first release of 2023 which is due on 19th January. You can see what we are planning to deliver in that release and beyond on [FlowFuse's project board](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""}. If you’d like to learn more about what else was included in 1.2 you can do so on our [blog post](https://flowfuse.com/blog/2022/12/flowforge-1-2-0-released/), [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v1.2.0){rel=""nofollow""}, and [Youtube channel](https://www.youtube.com/watch?v=u7TjqUAub1g){rel=""nofollow""}. [**5 IoT Sensor Technologies to Watch**](https://iot-analytics.com/5-iot-sensor-technologies/){rel=""nofollow""}:br According to this detailed article from iot-analytics.com, on average, four new sensors are connected with every new IoT device that comes online. With approximately 14 billion current IoT connections, this means more than 50 billion connected sensors have been deployed. IoT sensor technology plays a crucial role in the IoT tech stack because these sensors collect data from the physical world and convert it into digital signals. We think [their article](https://iot-analytics.com/5-iot-sensor-technologies/){rel=""nofollow""} is worth a read. [**Custom Node Spotlight - node-red-contrib-string**](https://flows.nodered.org/node/node-red-contrib-string){rel=""nofollow""} String manipulation is the bread and butter of so many programming tasks. Node-RED has a lot of tools to help you edit your strings including support for [JSONata](https://jsonata.org/){rel=""nofollow""} and using the trusty function node. For those of us who prefer to keep things 'no-code' [node-red-contrib-string](https://flows.nodered.org/node/node-red-contrib-string){rel=""nofollow""} allows you to stack string manipulations together quickly and easily. It has a huge library of ready to use functions. [**FlowFuse Team News**](https://flowfuse.com/about/):br We’d like to welcome two new members to the FlowFuse team. [Ian Skerrett](https://twitter.com/ianskerrett){rel=""nofollow""} has joined as our Head of Marketing and [Tracy Anthony](https://www.linkedin.com/in/tracyanthonyfernandez/){rel=""nofollow""} has joined as our HR Manager. FlowFuse continues to grow and we are becoming a truly global team. Would you like to work for FlowFuse? We are currently recruiting [NodeJS Developers](https://boards.greenhouse.io/flowfuse/jobs/4463977004){rel=""nofollow""} to join our team. You can view any of the roles we currently have open and apply on our [Jobs page](https://boards.greenhouse.io/flowfuse){rel=""nofollow""}. [**Try FlowFuse for Free**](https://app.flowfuse.com/account/create?code=FF12){rel=""nofollow""}:br As a thank you for reading our newsletters we’d like to offer you a free, small project for one month on our managed FlowFuse platform when you create a new team. To get this discount please follow [this link](https://app.flowfuse.com/account/create?code=FF12){rel=""nofollow""} or use the code FF12 when on the payment page after creating a new team. As an open source project you can also use [FlowFuse](https://flowfuse.com/docs/install/) for free, forever. # Using Environment Variables in Node-RED (2026) Programs, written with Node-RED or otherwise, need to sometimes retrieve information that wasn’t decided on during the creation of the program. Contextual data like configuration, which user is executing the code, differentiate based on what device is executing a flow, or sometimes secrets which shouldn’t be exposed in the code. This is usually done through environment variables. These are pairs of strings, a key with an attached value, which are accessed by their key. Say you want to access an API endpoint with a key, you’d save the key as `API_KEY` with the value set to `yoursupersecretkey`. FlowFuse allows setting environment variables. Let’s start using them to understand how they work. One of the options for the `inject` node is to inject a `env variable`, short for; you guessed it: Environment Variable. In this case we’re going to one that’s pre-defined by Node-RED: `NR_FLOW_NAME`. The name of each variable is in all caps by convention. When connecting this inject to a debug it prints “Flow 1” for me. :cta-image{alt="Walk through your FlowFuse setup with our team - book a demo" cta="demo" src="https://flowfuse.com/images/cta/book-a-demo.png"} ![Using an environment variable in Node-RED](https://flowfuse.com/blog/2023/01/images/node-red-use-env-var.png "Using an environment variable in Node-RED") Leveraging environment variables can also be done with other nodes, like for example `change`, `switch`. Note however; you can set the `inject` node to output the value for `FOO` even when it doesn’t exist, but it doesn’t allow you to check in the switch node for example if `FOO` exists. Node-RED allows you to set environment variables, but not to change them when executing flows. If you want to update data during execution, look into using [persistent context](https://flowfuse.com/docs/user/persistent-context/). Node-RED doesn’t support Environment Variables like other programming environments do. When the flow is deployed the environment variables are replaced with the known values at that time. This is the biggest gotcha for most developers. ### Predefined variables Our first example was using a predefined variable, exposed by Node-RED. As of 3.0 it exposes a few environment variables among which `NR_NODE_NAME`, `NR_GROUP_NAME`, and `NR_FLOW_NAME`. [FlowFuse](https://flowfuse.com) extends this list with for example a `FF_PROJECT_ID` allowing you to for example understand what group of instances sent a certain message, but also sets them for each [device agent](https://flowfuse.com/docs/device-agent/introduction/). This allows users to pinpoint which device sent a message, for example to update a dashboard accordingly. ### Managing environments variables In FlowFuse it’s easy to manage variables set for instances. Under settings in the environment tab it’s a form to set them. You’ll have to restart your instances to make them available in the cloud, and update the target snapshot for devices. When done, these are available. !["Setting a environment variable in FlowFuse"](https://flowfuse.com/blog/2023/01/images/flowforge-set-env-var.png "Setting a environment variable in FlowFuse") ## Boost Your Node-RED Security with FlowFuse FlowFuse provides a comprehensive platform for managing and securing your Node-RED solutions. It includes advanced security features such as role-based access control, Multi-factor Authentication (MFA), Single Sign-On (SSO), and encryption to protect your data and enhance operational efficiency. Learn how FlowFuse can boost your Node-RED security and streamline management through the [FlowFuse security statement](https://flowfuse.com/platform/security/#application). ### Explore More on Security - [Role-Based Access Control (RBAC) for Node-RED with FlowFuse](https://flowfuse.com/blog/2024/04/role-based-access-control-rbac-for-node-red-with-flowfuse/) - [Protecting Instances from Being Modified](https://flowfuse.com/docs/user/devops-pipelines/#protected-instances) - [How to Set Up SSO LDAP for Node-RED](https://flowfuse.com/blog/2024/07/how-to-setup-sso-ldap-for-the-node-red/) - [How to Set Up SSO SAML for Node-RED](https://flowfuse.com/blog/2024/07/how-to-setup-sso-saml-for-the-node-red/) - [Securing HTTP Traffic for Node-RED with FlowFuse](https://flowfuse.com/blog/2024/03/http-authentication-node-red-with-flowfuse/) - [FlowFuse is now SOC 2 Type 1 Compliant](https://flowfuse.com/blog/2024/01/soc2/) # FlowFuse 1.3 is now available, share your flows through our new team libraries and much more Share your flows via team libraries, control access to your Node-RED dashboards using FlowFuse credentials, and filter your audit logs by users and actions. We're pleased to announce version 1.3 is now available! Due to the recent holiday season, most of our team have been away from their desks but we still have some great new features to share. Keep reading for the details of what's in this release or you can watch our 1 minute roundup video of the new release above. To make it easy for everyone to experience FlowFuse, we are introducing a new [free 30-day trial](https://app.flowfuse.com/account/create){rel=""nofollow""}. With this trial, you can experience the power of using FlowFuse to quickly deliver Node-RED applications in a reliable, repeatable, collaborative, and secure manner. To get your trial simply [sign up for a new FlowFuse team](https://app.flowfuse.com/account/create){rel=""nofollow""}. ## Features [Share your flows via team libraries](https://github.com/FlowFuse/flowfuse/issues/237){rel=""nofollow""} :br FlowFuse has now added the ability for you to share your flows via the import and export features in Node-RED. Once you export a flow everyone else in your FlowFuse team will be able to import your work into their projects. You can see a demonstration of this new feature in [the video](https://youtu.be/B7XK3TUklUU){rel=""nofollow""} below. ::div :::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: B7XK3TUklUU --- ::: [Control access to your Node-RED dashboards using FlowFuse credentials](https://github.com/FlowFuse/flowfuse/issues/1325){rel=""nofollow""} :br In FlowFuse 0.10 we added the ability to secure endpoints created within your FlowFuse projects. This allows you to create dashboards or APIs and limit who can access them. In 1.3 we've added the ability for you to limit access to those same resources based on the visitor having a user account on your FlowFuse team. You can see a demonstration of this new feature in [the video](https://youtu.be/JRk-Cf7eNIo){rel=""nofollow""} below. :::div ::::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: JRk-Cf7eNIo --- :::: [Filter your audit logs for easier reading](https://github.com/FlowFuse/flowfuse/issues/1448){rel=""nofollow""} :br In FlowFuse 1.3 we’ve added the ability to filter your admin logs by user or action type. We think this is a great new feature which will help admins have confidence that they will be able to review the audit logs quickly when needed. You can see a demonstration of this new feature in [the video](https://youtu.be/p0Vuy5x42Go){rel=""nofollow""} below. ::::div :::::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: p0Vuy5x42Go --- ::::: ## Improvements [Allow installation of FlowFuse on devices which can't access npm](https://github.com/FlowFuse/device-agent/issues/45){rel=""nofollow""} :br We've had feedback from customers that in some cases they want to use FlowFuse devices on hardware which cannot access [Node Package Manager](https://www.npmjs.com/){rel=""nofollow""} (npm). In a standard configuration of Node-RED, access to npm is mandatory to run your flows. In FlowFuse 1.3.0 we've added the ability for you to import all the data usually installed from npm without your devices having access to the npm service. ## Bug Fixes We've fixed the following bugs in this release. - Project status UI sometimes getting stuck when restarting [#1232](https://github.com/FlowFuse/flowfuse/issues/1232){rel=""nofollow""} - SSO users asked to click link in email to verify [#1543](https://github.com/FlowFuse/flowfuse/issues/1543){rel=""nofollow""} - SSO users unable to edit settings [#1542](https://github.com/FlowFuse/flowfuse/issues/1542){rel=""nofollow""} - SSO users not redirected to editor when signing in [#1481](https://github.com/FlowFuse/flowfuse/issues/1481){rel=""nofollow""} ## Contributors We'd like to thank the following for their contributions to this release: [flecoufle](https://github.com/flecoufle){rel=""nofollow""} for their work on [#89](https://github.com/FlowFuse/helm/pull/89){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.3. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That's also a great place to send us any feedback or feature requests. You can also get help on [the Node-RED forums](https://discourse.nodered.org/){rel=""nofollow""} AS well as in the [forum within our Github project](https://github.com/FlowFuse/flowfuse/discussions){rel=""nofollow""} Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""} You can raise a support ticket by emailing We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. :::: ::: :: # FlowFuse 1.2.1 released We've published a maintenance release containing a fix for new users. This release fixes an [issue](https://github.com/FlowFuse/flowfuse/issues/1537){rel=""nofollow""} introduced in FlowFuse 1.2 where users were not being sent their welcome email when they first sign up to the platform. As the sign-up page asks them to click on the link in that email, it left them waiting for an email that wasn't going to arrive. The same fix also addresses an [issue](https://github.com/FlowFuse/flowfuse/issues/1514){rel=""nofollow""} where users could not sign up using an email with a `+` in it. This is a restriction we apply to users signing up with Single Sign-On enabled email domains - but the check was being applied to everyone. ### Upgrading FlowFuse This release has already been rolled out to \[FlowFuse Cloud]\({{ site.appURL }}). If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/) ### Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform, please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That also includes if you have any feedback or feature requests. Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""}. You can also raise a support ticket by emailing # Telling the FlowFuse Story During my first month at FlowFuse, I have been helping to refine the FlowFuse story. Trying to answer the questions: What is the value FlowFuse brings to our customers and what features does FlowFuse want to offer the Node-RED community? People love Node-RED because it makes it trivial to connect different types of services and data, create flows of the data, and store/forward data to the cloud or a database. The Node-RED [library of nodes and flows](https://flows.nodered.org/){rel=""nofollow""} enables connections to almost anything. The speed and ease of use of Node-RED allows non-traditional programmers to accelerate their innovation and exploration of what is possible. Node-RED is great for individuals to unlock what is possible for an organization. However, there are some challenges if you want to scale Node-RED into a corporate development platform. Challenges like how do you set up a development -> test -> production delivery pipeline, how to share instances with different developers, how do you create repeatable release processes, how can you deploy the same Node-RED instance out to multiple target environments (devices or servers). Today, we see many industrial engineers using Node-RED to collect and integrate data from different factory and industrial equipment. Many PLC vendors, like Opto22, are enabling this by making Node-RED as a development environment for the PLC platform. In the manufacturing and industrial automation industries, benefits like reliability and security are important for the continuous operation of these facilities. This is why we believe these types of organizations will be looking for tools that improve their software development lifecycle. In the larger software development community, DevOps tools are being used to address many of these same challenges. DevOps is all about increasing the reliability and speed of the software development lifecycle. Using tools that increase automation of the software development process and improve collaboration and communication between developers. These are also many of the same challenges that FlowFuse solves for Node-RED users. For these reasons, we see FlowFuse as being the DevOps platform for Node-RED. We believe FlowFuse enables a more reliable way to deliver Node-RED applications in a repeatable, collaborative and secure manner. What FlowFuse adds to Node-RED are some key features: - Tools for collaborative Node-RED development - The ability to manage remote deployments of Node-RED instances - A more streamline way to deliver Node-RED applications - FlowFuse Cloud provides a hosted Node-RED platform but FlowFuse can also be self-hosted - FlowFuse also offers professional technical support to organizations that require the assurance of receiving help with production deployments. We will be rolling out this new way of talking about FlowFuse over the next couple of days and weeks. We are always open to feedback about FlowFuse and would love to hear about your experience of using Node-RED. # Getting Started with Node-RED Node-RED is a visual programming tool for working with IoT devices and web services. It allows users to create flows using a drag-and-drop interface, making it easy to connect different nodes together to build powerful automations. In this blog post, we'll take a look at how to get started with Node-RED and create some basic flows. We'll also explore the palette manager, a powerful feature that allows users to install and manage additional nodes for Node-RED. ### Installing Node-RED First, you'll need to get an installation of Node-RED up and running. There are several ways to do this. We suggest using FlowFuse as it's very easy to get Node-RED running. You can also install Node-RED locally using npm (Node Package Manager), which comes with Node.js. #### FlowFuse To get Node-RED running on FlowFuse [sign up as a new user](https://app.flowfuse.com/account/create){rel=""nofollow""}. New users are enrolled in a trial and a Node-RED instance will be started for you within a minute. Once that instance has booted up you can access Node-RED by pressing "Open Editor". #### npm To install Node-RED locally using npm, open up your terminal and type the following command: ```bash npm install -g node-red ``` Once Node-RED is installed, you can start it by running the following command: ```bash node-red ``` This will start the Node-RED server and open up the [editor in your web browser](http://localhost:1880){rel=""nofollow""}. You can also specify a different port or a settings file if you want to. If you want to run Node-RED locally but manage it remotely through FlowFuse, check out [this guide](https://flowfuse.com/blog/2025/09/installing-node-red/). ### First Flow Now that you have Node-RED running, let's take a look at how to create a simple flow. In this example, we'll create a simple "Hello World" endpoint. To do this, we'll use the `http in`, `http response`, and the `change` nodes, which can be found in the common nodes menu on the left of Node-RED. First, drag an `http in` node into the editor. This node will listen for incoming HTTP requests. Next drag in the "change" and the `http response` node into the editor. Connect the `http in` node to the `change` node and connect the `change` node to the `http response` node. Your flow should look similar to this: !["Screenshot showing the HTTP-in, Change, and HTTP-response nodes that we will be using throughout this blog for demonstration."](https://flowfuse.com/blog/2023/01/images/three-nodes.png "Screenshot showing the HTTP-in, Change, and HTTP-response nodes that we will be using throughout this blog for demonstration.") To configure the `http in` node, double-click on it to open its properties. Here, you can set the URL that the node will listen to, as well as the method (GET, POST, etc.). In this example, we'll set the URL to `/hello` and the method to `GET`. Now we need to set what the endpoint will respond with, we will do that in the `change` node. Double-click the `change` node then add "Hello World" to the field which says "to the value". It should look like this: !["Configuring the change node to set the payload to Hello World"](https://flowfuse.com/blog/2023/01/images/set-reply.png "Configuring the change node to set the payload to Hello World") To configure the `http response` node, double-click on it to open its properties. Here, you should set the "Status Code" to be 200. This is not vital for the demo to work but it's good practice to return the correct codes when something connects to an API. Status code 200 means the API responded OK. This is how your `http response` node should look: !["Configuring the status node to set the response to 200"](https://flowfuse.com/blog/2023/01/images/response-code.png "Configuring the status node to set the response to 200") You can read more about HTTP response codes in [this article](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes){rel=""nofollow""}. ### Testing Your Flow Now that we have our flow set up, we can deploy it by clicking the "Deploy" button in the top right corner of the editor. Once the flow is deployed, you can test it by opening up a web browser. If you installed Node-RED using npm navigate to "{rel=""nofollow""}". If you are working on FlowFuse and running cloud hosted instance, use your instance URL with "/hello" added to the end, it should look something like "{rel=""nofollow""}". You should see "Hello World!" displayed in the browser. ### Debug Output One of the most powerful features in Node-RED is the ability to debug your flow. This can be done by adding a debug node to your flow and connecting it to the nodes you want to debug. When a message is sent through the connected node, the debug node will print the message in the debug sidebar on the right side of the editor. This can be very helpful when trying to understand how a flow is working or troubleshoot any issues. ### The Palette Manager In addition to the built-in nodes, Node-RED also has a palette manager feature which allows users to easily install and manage additional nodes from the community. To access the palette manager, go to the menu in the top right corner and select "Manage Palette". Here, you can search for and install new nodes, as well as update or remove existing ones. This is a great way to extend the functionality of Node-RED and add new capabilities to your flows. ### Import the flow If you want to view this flow you can import it using the code below. Copy the code then select Import from the top right menu in Node-RED. Paste the code into the field then press Import. ::render-flow ```json [ { "id": "a742e7a95697bb40", "type": "http in", "z": "9e9af3caa4dc14d3", "name": "", "url": "/hello", "method": "get", "upload": false, "swaggerDoc": "", "x": 180, "y": 200, "wires": [ [ "883e7d597f7c7c4b" ] ] }, { "id": "aca024dcb79bdb92", "type": "http response", "z": "9e9af3caa4dc14d3", "name": "", "statusCode": "200", "headers": {}, "x": 500, "y": 200, "wires": [] }, { "id": "883e7d597f7c7c4b", "type": "change", "z": "9e9af3caa4dc14d3", "name": "", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "Hello World", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 340, "y": 200, "wires": [ [ "aca024dcb79bdb92" ] ] } ] ``` :: ### What's Next? Well done, you've now got your first flow up and running. Enjoy using Node-RED and thanks for reading. Now if you want to start with your first beginner friendly project, building a weather dashboard is great, read this [article for getting started](https://flowfuse.com/blog/2025/12/getting-weather-data-in-node-red/). If you'd like to dive deeper into more Node-RED capabilities and how it can help in an enterprise setting, check out our [eBook The Ultimate Beginner Guide to a Professional Node-RED](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/). # Node-RED Tips - Wiring Shortcuts There is usually more than one way to complete a given task in software and Node-RED is no exception. In this blog post we are going to share three useful tips to save yourself time when working on your flows. ### 1. Use control+left-click to search your nodes Sometimes it's quicker to search for a node using its name rather than scrolling through the palette. Simply hold control then left-click to bring up a searchable list. :video{ariaLabel="Selecting a node without having to use the palette" autoPlay="true" height="318" loop="true" muted="true" playsInline="true" preload="none" width="774"} ### 2. Split sections of your code using the link nodes If you want to separate your flow into two distinct sections, link nodes are a great way to format your work. As we covered in our blog on [Node-RED best practices](https://flowfuse.com/blog/2022/12/node-red-flow-best-practice), the combination of link nodes and grouped flows is very powerful. To split your flow select the input and output nodes then right click, select 'Show Action List' and then type 'split'. Select 'Split wire with link nodes'. :video{ariaLabel="Spliting your nodes with link nodes" autoPlay="true" height="440" loop="true" muted="true" playsInline="true" preload="none" width="726"} ### 3. Link multiple inputs and outputs in one command Once a switch node has several outputs it can be slow to manually wire each to the new node. Using the action menu (right click), select 'Show Action List' then 'Wire Node to Multiple' this option will join everything up in one step. :video{ariaLabel="Linking multiple inputs and outputs in one command" autoPlay="true" height="362" loop="true" muted="true" playsInline="true" preload="none" width="538"} We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). # Node-RED Tips - Deploying, Debugging, and Delaying There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. ### 1. Deploy just what you've changed When deploying your changes, the default option is deploy everything which also restarts all your flows. You can also select to deploy just the nodes you edited or just the flows in which any changes were made. This allows you to update part of your flow without restarting other sections. This can be really handy when you have different flows spread across your workspace or tabs but you don't want to reload them all each time you deploy. :video{ariaLabel="Deploying only the changed nodes" autoPlay="true" height="322" loop="true" muted="true" playsInline="true" preload="none" width="898"} ### 2. Find which debug node generated an entry in the log Once your flow has a few debug nodes it can become challenging to see which particular node generated an entry in the log. To quickly track an entry back to its source, click the text 'node: debug' and you will be whisked back to the specific debug node, even it it's elsewhere on you workspace or even on a different tab. :video{ariaLabel="Finding the debug node which generated the log line" autoPlay="true" height="258" loop="true" muted="true" playsInline="true" preload="none" width="898"} ### 3. The delay node can be used as a rate limiter Sometimes it's useful to be able to limit messages to only allow one every so many minutes. You may for example send alerts when a temperature sensor goes above a particular threshold but you don't want your email or instant messaging inbox being swamped with repeated alerts for the same issue. You can use the delay node to limit how many messages can pass through in a given period of time. Open the delay node settings, select Rate Limit then select 1 message per 15 minutes, then select 'Drop intermediate messages'. This flow will now output a maximum of one message every quarter of an hour, all others will be deleted. :video{ariaLabel="Limiting how many alerts are sent" autoPlay="true" height="298" loop="true" muted="true" playsInline="true" preload="none" width="764"} We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). ## Enhance Efficiency with FlowFuse FlowFuse is a cloud-based platform designed to boost collaboration, security, and scalability for your Node-RED applications. It features [numerous tools and functionalities](https://flowfuse.com/platform/features/) that streamline the development-to-deployment process, including one-click deployment, the [FlowFuse Assistant](https://flowfuse.com/docs/user/expert/), and other capabilities that simplify managing your Node-RED environment. For more tips, tricks, and professional development techniques with Node-RED, check out our recommended eBook: [The Ultimate Beginner's Guide to Professional Node-RED](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/) # Community News February 2023 Welcome to the FlowFuse newsletter for February 2023, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. If you've got something that you think we should share on our newsletters please [get in touch](mailto\:contact@flowfuse.com). ## News ### Node-RED Ask Me Anything ![AMA Session with Nick O'Leary and Rob Marcer](https://flowfuse.com/images/webinars/ama-feb.jpg "AMA Session with Nick O'Leary and Rob Marcer") Do you have any questions about Node-RED or need some advice on a tricky issue using Node-RED? Here is your opportunity to get help from the experts. Nick O'Leary, co-founder of Node-RED & CTO of FlowFuse, and Rob Marcer, Node-RED community member & Developer Educator at FlowFuse, will be hosting an interactive Ask Me Anything session. This is a great opportunity to ask questions of the Node-RED experts. If you have any questions for Nick and Rob you can send them in before the session using [this form](https://docs.google.com/forms/d/e/1FAIpQLSdfPq4lAQjdvqhTpoYtKiMNgP8vcMhZsAf_AG0MHuVMRK83_Q/viewform){rel=""nofollow""}. ### [Free FlowFuse project for 30 days, no catches](https://app.flowfuse.com/account/create){rel=""nofollow""} To make it easy for everyone to experience FlowFuse, we are introducing a new [free 30-day trial](https://app.flowfuse.com/account/create){rel=""nofollow""}. With this trial, you can experience the power of using FlowFuse to quickly deliver Node-RED applications in a reliable, repeatable, collaborative, and secure manner. To get your trial simply [sign up for a new FlowFuse team](https://app.flowfuse.com/account/create){rel=""nofollow""}. ### [1.3 Released](https://flowfuse.com/blog/2023/01/flowforge-1-3-0-released) Version 1.3 of FlowFuse was released on 19th January. Our first release of 2023 included some great new features such as the ability to share flows via a [team library](https://www.youtube.com/watch?v=B7XK3TUklUU){rel=""nofollow""}, [control access to your Node-RED dashboards](https://www.youtube.com/watch?v=JRk-Cf7eNIo){rel=""nofollow""} using FlowFuse credentials, and [filtering on your audit logs](https://www.youtube.com/watch?v=p0Vuy5x42Go){rel=""nofollow""} for easier reading. We also added the ability to use FlowFuse on devices which cannot access npm, we think this will be really valuable to users of networks with limited access to the internet. We're now working towards release 1.4 which is due on 16th February. You can see what we are planning to deliver in that release and beyond on [FlowFuse's project board](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""}. If you’d like to learn more about what else was included in 1.3 you can do so on our [blog post](https://flowfuse.com/blog/2023/01/flowforge-1-3-0-released/), [GitHub release page](https://github.com/FlowFuse/flowfuse/releases/tag/v1.3.0){rel=""nofollow""}, and [Youtube channel](https://www.youtube.com/watch?v=ey3xv5j5x7k){rel=""nofollow""}. ### [Team News](https://flowfuse.com/about/) We are currently recruiting [NodeJS Developers](https://boards.greenhouse.io/flowfuse/jobs/4463977004){rel=""nofollow""} as well as a [Graphic Designer](https://boards.greenhouse.io/flowfuse/jobs/4785058004){rel=""nofollow""} to join our team. You can view any of the roles we currently have open and apply on our [Jobs page](https://boards.greenhouse.io/flowfuse){rel=""nofollow""}. ## Node-RED in the Community ### [Twitch Streamer beats Elden Ring boss using mind control (and a little Node-RED)](https://www.vice.com/en/article/bvmqmm/watch-an-elden-ring-streamer-beat-a-boss-using-her-thoughts){rel=""nofollow""} [![Twitch Streamer beats Elden Ring boss using mind control (and a little Node-RED)](https://flowfuse.com/blog/2023/02/images/twitch.webp)](https://www.vice.com/en/article/bvmqmm/watch-an-elden-ring-streamer-beat-a-boss-using-her-thoughts){rel=""nofollow""} Streamer [Perrikaryal](https://www.twitch.tv/videos/1717013810){rel=""nofollow""} used an electroencephalography (EEG) headset to read her brain activity which in turn sends commands via Node-RED to her gaming computer. She then proceeded to use that control method to beat one of the harder bosses in Elden Ring, a notoriously difficult game to start with. The end result is a great example of how Node-RED can link disparate tech together easily. You can [watch the full stream on Twitch](https://www.twitch.tv/videos/1722048787){rel=""nofollow""}. ### [Run Node-RED in a web browser, client side!](https://www.linkedin.com/posts/kazuhitoyokoi_nodered-webassembly-activity-7015696090112958464-F3MA/?utm_source=share&utm_medium=member_android){rel=""nofollow""} [Kazuhito Yokoi](https://www.linkedin.com/in/kazuhitoyokoi/){rel=""nofollow""} has implemented an early prototype of Node-RED runtime for WebAssembly. As a demonstration, he built a simple flow which shows the current location of the International Space Station (ISS). This proof-of-concept shows that an entire Node-RED flow can be executed in a browser. We think this could be a very useful option for running Node-RED in places where it wasn’t previously practical. You can view his work on this [GitHub project](https://github.com/kazuhitoyokoi/node-red-wasm){rel=""nofollow""}. ### [Custom Node Spotlight - Moment](https://flows.nodered.org/node/node-red-contrib-moment){rel=""nofollow""} [![Moment converting a timestamp to ISO standard date and time](https://flowfuse.com/blog/2023/02/images/moment.png)](https://flows.nodered.org/node/node-red-contrib-moment){rel=""nofollow""} Being able to easily switch dates and times from one format to another is a huge timesaver, node-red-contrib-moment makes those tasks a breeze. The package actually includes two custom nodes, the first ‘Moment’ produces a nicely formatted Date/Time string using the Moment.JS library. The second custom node ‘Humanizer’ converts time durations (time spans) into textual descriptions (e.g. 2 minutes). We recommend you keep it in mind for any flows working with time conversions. # FlowFuse v1.4 with device provisioning in bulk and staged development process Deploy Node-RED to many devices quickly, and allow a staged development process with the latest release of FlowFuse v1.4. Keep reading for the details of what's in this release or you can watch our 1 minute roundup video of the new release above. To make it easy for everyone to experience FlowFuse, we are introducing a new free 30-day trial. You can now experience the power of using FlowFuse to quickly deliver Node-RED applications in a reliable, repeatable, collaborative, and secure manner. To get your free trial simply [sign up for FlowFuse Cloud;](https://app.flowfuse.com/account/create){rel=""nofollow""} no credit card is required! ## New User Features **Automatic Device Provisioning** Most prominently, FlowFuse 1.4 features automatic device onboarding for fleets. Simply download a FlowFuse device provisioning credential to allow quick roll-out to a whole fleet, without the need to have device specific configuration. When the agent starts, the FlowFuse agent and the Node-RED snapshot will automatically be provisioned to the device and start operations. [Issue #1212](https://github.com/FlowFuse/flowfuse/issues/1212){rel=""nofollow""} ::div :::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: XTVw4O4-Crg --- ::: **Support for Staged Development** A new feature of 1.4 is the ability to setup staged deployments. This makes it possible to simply move a project between a Development > Test > Production for your Node-RED application delivery. [Issue #1580](https://github.com/FlowFuse/flowfuse/issues/1580){rel=""nofollow""} :::div ::::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: 6QOmotlrwWw --- :::: ## Improvements - It's now much easier to change the resources available to your Node-RED instance. With a few clicks a resource intensive workload can be processed faster by changing between small, medium, or large instance types. [#595](https://github.com/FlowFuse/flowfuse/issues/595){rel=""nofollow""} - Last release allows users to capture flows in a shared library to reuse in another flow, now it's possible to preview the stored flows. [#1657](https://github.com/FlowFuse/flowfuse/issues/1657){rel=""nofollow""} - Add a synchronous mode for the FlowFuse persisted context store, next to the asynchronous mode already available. [#17](https://github.com/FlowFuse/flowforge-nr-persistent-context/issues/17){rel=""nofollow""} - Add a Last Seen status for devices connecting to FlowFuse. [#1599](https://github.com/FlowFuse/flowfuse/issues/1599){rel=""nofollow""} - Added a check to ensure the team slug is unique. [#1609](https://github.com/FlowFuse/flowfuse/issues/1609){rel=""nofollow""} - Optionally set snapshot as target at creation, to quickly roll out changes to remote deployments [#1527](https://github.com/FlowFuse/flowfuse/issues/1527){rel=""nofollow""} - Agents are more rugged when starting up if they're unable to connect to FlowFuse, and will retry to connect. - With FlowFuse v1.4 some changes were made under the hood to speed up the recovery of Node-RED instances. On terminal failures of an instance it will now be automatically be redeployed with the correct flows. This uses Kubernetes features and is also available if you've installed through [kubernetes](https://flowfuse.com/docs/install/kubernetes/). To migrate the old style of deployments to this system a restart or stack upgrade is needed. ## Bug Fixes We've fixed the following bugs in this release. - Deleting your only team, doesn't exit from the team UI. [#1630](https://github.com/FlowFuse/flowfuse/issues/1630){rel=""nofollow""} - Async Team Slug Check. [#1609](https://github.com/FlowFuse/flowfuse/issues/1609){rel=""nofollow""} - Improve communication of Device Last Seen and Status [#1599](https://github.com/FlowFuse/flowfuse/issues/1599){rel=""nofollow""} ## Contributors We'd like the thank the following for their contributions to this release: - [UlisesGascon](https://github.com/UlisesGascon){rel=""nofollow""} for their work on [#74](https://github.com/FlowFuse/installer/pull/74){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.4. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That's also a great place to send us any feedback or feature requests. You can also get help on [the Node-RED forums](https://discourse.nodered.org/){rel=""nofollow""} As well as in the [forum within our Github project](https://github.com/FlowFuse/flowfuse/discussions){rel=""nofollow""} Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""} You can raise a support ticket by emailing We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. ::: :: # Toward Highly Available Node-RED Over the past few months we've held a lot of product discovery sessions and a topic which keeps coming up is "HA Node-RED". All software will have failures, with HA (high availability) the intent is to allow the workload to be processed regardless. There's quite a few considerations which are often not covered during product discovery calls, I'm going to discuss some of those points in this article. When my job title was software engineer I was fortunate to design and implement a HA system. It's an incredibly challenging and rewarding task for any software engineer. As a topic, it's studied when obtaining a Computer Science Bachelors degree, masters and even PhD. When tasked to make a HA system, it took me a good month to define what our goals were, and what we were willing to exchange for the properties sought. This might be extra hardware, engineering hours, as well as organizational challenges. For now, let's focus on the first two. Let's start with defining the goal; reduce the impact of a Node-RED instance being unresponsive for an arbitrary reason. In many use-cases the MTTR (Mean Time To Recovery) is what's measured. When for example a hardware failure takes down the instance and the time to detection is zero, it will likely still take a few hours to recover. Most of the recovery work is also manual, and knowledge on how to recover is usually [tribal knowledge](https://en.wikipedia.org/wiki/Tribal_knowledge){rel=""nofollow""}. If the right person is on-site, the right hardware is available, you kept great backups, and are able to deploy the new hardware right away without support from other functions you might just achieve an MTTR of 120 minutes! ### 5-10 minute Mean Time to Recovery What's needed to bring this back to say 10 minutes? First, adopting FlowFuse will help massively here. FlowFuse can be installed on-premise, or you can use our managed Cloud offering. The software is the same, provided the on-premise install uses our [Kubernetes install](https://flowfuse.com/docs/install/kubernetes/) method. The key of the installation is the fact that the hardware layer is generalized as a fleet. Detecting failures is included in the install, and very fast. Comparing that to most alerting systems currently, it's usually a difference between night and day. Furthermore, to decrease the recovery time significantly there's a requirement to make software responsible for the whole procedure. Human intervention is much too slow. To get the MTTR down to 5 minutes there's a requirement to either make hardware automatically available to the fleet, or to over-provision (more hardware is available than is needed at any given moment). When a hardware failure occurs FlowFuse is configured to ensure all Node-RED instances that are KIA are replaced. Bringing down the time to recovery to about 5 minutes. For many use-cases a MTTR of 5 minutes is *good enough*. ### Sub minute MTTR To go below the minute, or dare I say go below 10 seconds, we'll need to increase the number of running Node-RED instances. Let's start with a hot-spare. Meaning there's a running Node-RED instance with the flows exactly the same as another, ready to pick up the work when the first has some failure. Note this isn't like a relay race, there's no baton being passed from one Node-RED to the other. While some data and messages might be lost, it's possible to redirect all workload from the plagued Node-RED to the hot-spare in a matter of seconds. Hot-spares taking over are usually only observed by humans a good few minutes after they replace a failed instance. ### Sub second! Before this post turns into a theoretical exercise we'd really need to understand which trade-offs are acceptable to you. There's the [CAP Theorem](https://en.wikipedia.org/wiki/CAP_theorem){rel=""nofollow""} which states 3 guarantees are wanted: Consistency, Availability, and Partition Tolerance. You get to pick only two. In manufacturing the line must never be stopped due to a software failure where possible, so Availability is the most important. The question is, what comes next? Is it Consistency meaning all 3 instances have the same view of the global state? Or maybe Partition Tolerance where it's vital for each instance need to be able to predict the intended action even if it can't communicate to the others? Whichever 2 you choose will dictate engineering choices in the pursuit of a great HA solution. ### The roadmap With FlowFuse v1.4, released February 2023, a 5 minute mean time to recovery is achieved for all flows running locally, that is: in the cluster. Going beyond this milestone requires your input! I'd love to chat about your challenges, please [pick a timeslot to discuss your requirements](https://meetings-eu1.hubspot.com/zeger-jan){rel=""nofollow""}! # MING Stack for IoT The folks at Balena have created a bundle they call the [MING stack](https://hub.balena.io/organizations/marc6/apps/MING){rel=""nofollow""}, (Mosquitto/MQTT, InfluxDB, Node-RED and Grafana) which first appeared back in [2019](https://forums.balena.io/t/ming-an-iot-sensor-stack-mosquitto-influxdb-nodered-grafana/36540){rel=""nofollow""}. It is an interesting way to look at the IoT tech stack and a pattern I have seen used many times. In the early days of the web, the [LAMP stack](https://en.wikipedia.org/wiki/LAMP_\(software_bundle\)){rel=""nofollow""} was popularized as being the open source tech stack for hosting web sites. First used in 1998, LAMP stood for Linux, Apache, MySQL and Python/Perl/PHP. The LAMP stack did a lot to popularize open source software and create architecture patterns for building web applications. The LAMP stack did a lot to simplify a confusing technology landscape back in the early days of the web. Can the IoT industry benefit from a MING stack? There is a fair amount of complexity building IoT systems. Therefore, having defined architecture patterns might help reduce some of the confusion. In fact, MING does bring together the key open source components of an IoT system: - [Mosquitto](https://mosquitto.org/){rel=""nofollow""} is the popular open source MQTT broker. MQTT has become the default protocol for IoT communications. The MQTT pub/sub protocol solves a lot for the communication challenges for IoT applications. In fact, I would define the M as being MQTT since there are a lot of MQTT broker implementations available. - [InfluxDB](https://www.influxdata.com/){rel=""nofollow""} is the popular open source time series database. Many IoT use cases are based on analyzing trends from different IoT devices. The classic examples is preventive maintenance of factory equipment. Having a time series database in your IoT architecture to record trending information will solve a lot of your data problems. - [Node-RED](https://flowfuse.com/node-red/) is the popular low-code development environment that helps create flows of data. IoT systems are often pulling data from many different sources. The data needs to be filtered, analysed or transformed before being forward to another service. Node-RED has a large community of data nodes that makes it easy to collect data from a wide variety of sources. For instance in the industrial world, Node-RED nodes are available for OPC-UA, Modbus, S7, MQTT, various PLC platforms like Opto22, etc, etc. - Finally [Grafana](https://grafana.com/){rel=""nofollow""} is a popular open source visualization platform. Real time monitoring of IoT data is often the first applications deployed for IoT systems. Having graphing and dashboard technology available in your architecture makes perfect sense. Another important feature of MING is that it can be deployed on the edge or in the cloud. IoT systems are inheritenty distributed so having the same technology available at different tiers is useful for lower barriers to adoption. The LAMP stack was successful because it was: - Open source and freely available for anyone to adopt and use. - Highly flexible and customizable that allowed developers to adopt the stack to their use case. - Back by large developer communities creating plugins/extensions, documentation, tutorials, etc. - Easy to learn for developers with limited experience. The MING stack has all the same characteristics. All four technologies are open source, highly flexible, back by large developer communities and relatively easy to learn. There are a lot of similarities between MING and LAMP. Is MING relevant for IoT use cases? In my experience, people building IoT solutions are using 3-4 of the MING components. What is your experience? ## Simplify IoT Complexity with FlowFuse’s MIND Stack If you’re concerned about managing multiple components in your IoT stack, FlowFuse has you covered. By bundling three of the four core elements of the MING stack, MQTT, Node-RED, and Grafana. FlowFuse simplifies your setup and management. This streamlined approach means fewer moving parts and less complexity, allowing you to focus on what matters most: your data and your applications. With the FlowFuse [MIND stack](https://flowfuse.com/blog/2024/05/node-red-mind-stack-with-flowfuse/), you get a unified platform that integrates MQTT for efficient communication, Node-RED for seamless integration and data transformation, and [Dashboard 2.0](https://flowfuse.com/platform/dashboard/) for powerful visualizations, all from one place. This integration reduces configuration and maintenance overhead while ensuring consistent performance and security across your deployments. Explore the MIND stack now. **Want an easier, more secure, and scalable IoT stack? Start your [free trial](https://app.flowfuse.com/account/create){rel=""nofollow""} of FlowFuse today.** # Service Disruption Report for January 27th, 2023 On January 27th, 2023, we were alerted to an issue on FlowFuse Cloud where a user was not able to access a newly created Node-RED Project, receiving a 404 error instead. This post examines the issue that was hit, the timeline of events and what we've done to resolve it. ## Summary We hit a limit in the AWS Load Balancer that capped how many projects could be exposed to the internet within our FlowFuse Cloud deployment. The result of this was that users could create a new Node-RED project, but they would not be able to access the editor. We freed up capacity on the platform to allow user projects to be created without hitting the limit, whilst also asking AWS to increase the limit in question which they duly did. However, we later discovered a second limit that was also being applied. That limit was not one AWS permits us to change. We successfully completely deployment of a change to our platform architecture today that removes these limits from our environment. In total, this lead to approximately 2 hours of disruptions on January 27th 2023 and again on February 8th 2023 during which newly created Node-RED projects were not accessible. Our logs show that two users were impacted during these times. ## Technical Details When running FlowFuse within a Kubernetes environment, each project creates a new Ingress Object configuration to tell the platform how to route HTTP traffic to that project. Our FlowFuse Cloud deployment runs within Amazon Elastic Kubernetes Service (EKS) and uses the Application Load Balancer (ALB) service as its ingress controller. When the FlowFuse platform creates the new Ingress Object configuration, EKS passes that to ALB to generate the necessary configuration, which, given the configuration we were using, created both a Target Group and Rule object. With a default of limit of 100 Target Groups and Rules, that meant we had a technical limit of 100 Node-RED projects within the FlowFuse Cloud environment. Increasing the Rule limit did not solve the problem as the Target Group limit still applied. Our initial mitigation was to delete any Node-RED projects we had created for our own internal testing. We also identified that we could safely delete any rules for suspended projects. A suspended project is one that is not actively running in the platform. The code that resumes a suspended project would recreate any ingress objects needed - so deleting the rule whilst suspended would not have any impact on the project. This gave us a small amount of headroom on the platform which crucially meant we had time to develop a longer term solution. ## Resolution There were two possible routes we could take to resolve this issue: - Investigate how to reuse existing Target Groups rather than create one for each project :br Our intial research was inconclusive on how to achieve this. The AWS docs weren't clear enough to give us a definitive answer we felt comfortable to invest our time in. - Move away from ALB in favour of nginx to provide our ingress load balancing. :br This was always our long term strategy as it was a prerequisite to FlowFuse features we have in the roadmap such as providing custom domains to projects. However it was potentially a large piece of work with a complicated migration for the existing environment. Given it fitted with our longer-term strategic goals, we decided to move ahead with replacing ALB with nginx. After some initial development work and experimentation, we felt comfortable that the migration was not as complicated as initially feared. We would have to manually copy the existing ALB rules over - something that could be scripted. Once we had nginx deployed we could push a small code change to FlowFuse to use it rather than ALB, and also switch over the DNS entry to point at nginx. Following a successful run through in our testing/staging environment, we decided to move ahead updating the production environment. This change was applied today, whilst we closely monitored the system to ensure no further disruption occurred. We have validated that new projects can be created without issue and everything is working as it should. ## Next Steps With FlowFuse Cloud updated to use the new load balancer, we'll be closely monitoring it over the next few days to ensure it operates normally. We will also be taking on some follow-up activities to minimise the risk of this type of issue happening again: 1. Review all AWS limits within our architecture. Identify any that pose a potential issue in the future. Ensure they are documented and a plan put in place to mitigate the impact based on our expected platform growth. 2. Add additional external monitoring for project liveness. 3. Review all logging around k8s apis ## Timeline *All times are GMT.* **2023-01-27 22:15** : (Friday evening) Customer reports via our support channel a newly created project was not accessible and returning a 404 error. **2023-01-27 22:22** : We start examining the platform logs **2023-01-27 22:47** : We identify we've hit the default Rules limit on the AWS Application Load Balancer **2023-01-27 23:15** : To free-up capacity on the platform we delete any ununsed internal projects we were using for general testing. We also identify we can safely delete any rules associated with suspended projects. **2023-01-27 23:41** : We complete deleting rules to give us enough head-room to see us through the weekend. **2023-01-30 10:00** : We submit a request to AWS to increase the rule limit to 200 which is accepted and actioned later that day. ... **2023-02-08 14:05** : Another customer reports seeing a newly created project returning a 404 error. We examine the ALB configuration and whilst it reports the new limit has been changed to 200, it appears to still be limiting at 100. We start identifing more suspended projects we can delete the rules for to free up capacity. **2023-02-08 14:20** : Sufficient capacity is freed to enable the customer's projects to be accessible. **2023-02-08 15:00** : We identify we've hit the ALB Target Group limit. This is a hard limit that AWS does not allow you to change. We begin researching options. **2023-02-09** : Commited to plan to replace ALB with nginx. Successful migration of our staging environment. **2023-02-10** : Change applied to production, FlowFuse 1.3.3 deployed and DNS updated to use the new load balancer. # Some questions we didn't get to in our first webinar There were some great questions from our [first webinar](https://www.youtube.com/watch?v=47EvfmJji-k){rel=""nofollow""} that we didn't get time to answer, we wanted to share those questions and our answers here. ### Irvin asks, 'is it possible to save the debug information or data flow to a storage like Splunk or SQL'? Hi Irvin, thanks for the question. I suspect you might be better using a custom node which is designed for logging data rather than capturing the debug node content to a database. I've come across [Flogger](https://flows.nodered.org/node/node-red-contrib-flogger){rel=""nofollow""} which seems to do a good job of logging, including support for multiple log files, and built in support for log rotation. :video{ariaLabel="Capturing debug to a log file using Flogger" autoPlay="true" height="580" loop="true" muted="true" playsInline="true" preload="none" width="1470"} If you really wanted to log to a database rather than a log file you could create your own logging subflow. Once that's in place you can drop it into your flow as needed to capture your debug data for later consumption. ### Anonymous asks 'can we make an mobile application with Node-RED or can the content only be accessed through a web browser'? Hello Anon', it would be great if a Node-RED flow could be built into a mobile app. Sadly, there isn't a simple way to do so at the time of writing. Assuming you want an easy way to package up and distribute the functionality you might be best creating a link to where the application is hosted. Both [iOS](https://www.macrumors.com/how-to/add-a-web-link-to-home-screen-iphone-ipad/){rel=""nofollow""} and [Android](https://www.androidauthority.com/add-website-android-iphone-home-screen-3181682/){rel=""nofollow""} support making a home icon. Once added using them is basically the same user experience as a locally installed application. ### John asks 'From the random node example in the webinar, the node connected to four different nodes. Is there an order to which is invoked first? Can that be controlled'? Hi John, interesting question, thanks for sending it in. For the sake of any readers who were not an the webinar here is what you are describing. !["Image showing the Flow where the random number generator sends a message to 4 nodes at the same time"](https://flowfuse.com/blog/2023/02/images/chart-flow.png "Image showing the Flow where the random number generator sends a message to 4 nodes at the same time") All downstream nodes linked to the same prior node will be triggered at practically the same time. You could use a delay node if you want to ensure a particular node is triggered first. It might also make sense to wire your flow in series rather than parallel. This would allow your functions to all execute in a specific order. That being said, in this case all but one of the nodes do not have outputs so using delays might be the only practical option. ### Abdelhamid asks, 'How can I delete a subflow'? Thanks Abdelhamid, that's actually really easy to do. Double click the subflow you want to delete, then select 'delete subflow' from the top of your workspace. !["Image showing how to delete a subflow"](https://flowfuse.com/blog/2023/02/images/delete-subflow.png "Image showing how to delete a subflow") Thanks again to everyone who attended and participated in our first webinar. We have lots of other useful live content coming up soon, you can view and register for future events on our website's [webinars page](https://flowfuse.com/webinars/). # Node-RED Tips - Exec, Filter, and Debug There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. ### 1. The Exec node allows you to interact with BASH from Node-RED Exec allows you to run Shell commands and receive the value back into your flow. This opens up almost any command which can be run on the host devices CLI to your Node-RED flows. :video{ariaLabel="Example flow using the Exec node" autoPlay="true" height="220" loop="true" muted="true" playsInline="true" preload="none" width="810"} ### 2. The Filter node helps you discard duplicate messages It can be useful to only allow messages to proceed through a flow where their value is unique. Filter makes that task simple, no need to store the past values and check each new message against a list. ![Configuring the Filter node to only allow unique payloads through](https://flowfuse.com/blog/2023/03/images/filter-config.png "Configuring the Filter node to only allow unique payloads through") Once your filter is configured as shown above, try sending different payloads through to see the outcome. :video{ariaLabel="Demonstration showing the Filter node" autoPlay="true" height="194" loop="true" muted="true" playsInline="true" preload="none" width="802"} ### 3. Counting the amount of messages sent to a Debug node The Debug node has a lot of great features that we don't see used that often. One example is the ability to show a count of how many messages have been sent to that Debug node since the last deploy. ![Setting up the debug to count messages](https://flowfuse.com/blog/2023/03/images/setup-counting-debug.png "Setting up the debug to count messages") Once you've setup the node as shown above, you will see a counter under the debug. :video{ariaLabel="Each message sent to the debug node is counted" autoPlay="true" height="380" loop="true" muted="true" playsInline="true" preload="none" width="916"} We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). ## Effortless Communication Between Node-RED Instances with FlowFuse Project Nodes Managing communication between multiple Node-RED instances can be a complex task, but FlowFuse [Project Nodes](https://flowfuse.com/docs/user/projectnodes/) simplify this process dramatically. With these nodes, you can easily send messages between different Node-RED instances without worrying about complex configurations or network setup. All you need to do is select the target instance by name, and FlowFuse takes care of the rest. This makes it faster and more efficient to handle multi-instance environments, ensuring seamless communication between flows across different devices or locations. Whether you're managing multiple environments or working on large-scale projects, FlowFuse Project Nodes save you time and reduce the risk of errors. FlowFuse continues to innovate, making collaboration and scalability in Node-RED projects even easier. To learn more about these features, check out the [FlowFuse website](https://flowfuse.com). # Node-RED Tips - Smooth, Catch, and Math There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. ### 1. Use the Smooth node to get the minimum and maximum values of your payloads When taking data in from sensors sometimes a spurious value can be sent into your flow. This can result in oddities in a graph or even misfiring of actions such as turning on a heating system. The Smooth custom node allows you to store the min and max of a payload for the last few messages received. ![Using the Smooth node to return highest value from the last 100 payloads](https://flowfuse.com/blog/2023/03/images/smooth.png "Using the Smooth node to return highest value from the last 100 payloads") :cta-image{alt="Walk through your FlowFuse setup with our team - book a demo" cta="demo" src="https://flowfuse.com/images/cta/book-a-demo.png"} You can in turn use this to ignore values that deviate too far from the sample. To help demonstrate the Smooth node, I've created a flow you can import into Node-RED. ```json [{"id":"9484c25a0120bd48","type":"group","z":"dd95c0bca1101c86","name":"Automatically outputs random value (temperature in Celcius) between 0 & 25 every second","style":{"label":!0},"nodes":["e4f972f9daad6246","c7fc075a1915e87b","966a772c46dc2888"],"x":34,"y":59,"w":574,"h":82},{"id":"e4f972f9daad6246","type":"link out","z":"dd95c0bca1101c86","g":"9484c25a0120bd48","name":"link out 1","mode":"link","links":["33594c64783cdc45","e6bf7b494b48861e"],"x":355,"y":100,"wires":[]},{"id":"c7fc075a1915e87b","type":"inject","z":"dd95c0bca1101c86","g":"9484c25a0120bd48","name":"","props":[],"repeat":"1","crontab":"","once":!1,"onceDelay":0.1,"topic":"","x":130,"y":100,"wires":[["966a772c46dc2888"]]},{"id":"966a772c46dc2888","type":"random","z":"dd95c0bca1101c86","g":"9484c25a0120bd48","name":"","low":"0","high":"25","inte":"true","property":"payload","x":260,"y":100,"wires":[["e4f972f9daad6246"]]},{"id":"37380f26e8bfc98a","type":"group","z":"dd95c0bca1101c86","name":"Calculate average, high and low, save to flow","style":{"label":!0},"nodes":["cc3978c7c4ea56ed","e1819526a5f365c8","33594c64783cdc45","fea261a15b3b7683","e28a69232f1cac53","aecb1727be523240","e30039ee13e480a8"],"x":34,"y":259,"w":552,"h":142},{"id":"cc3978c7c4ea56ed","type":"change","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"","rules":[{"t":"set","p":"high","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":!1,"x":310,"y":300,"wires":[["aecb1727be523240"]]},{"id":"e1819526a5f365c8","type":"change","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"","rules":[{"t":"set","p":"low","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":!1,"x":310,"y":360,"wires":[["e30039ee13e480a8"]]},{"id":"33594c64783cdc45","type":"link in","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"link in 1","links":["c07b2e101cecbd3b","e4f972f9daad6246"],"x":75,"y":320,"wires":[["e28a69232f1cac53","fea261a15b3b7683"]]},{"id":"fea261a15b3b7683","type":"smooth","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"Min","property":"payload","action":"min","count":"100","round":"2","mult":"single","reduce":!1,"x":170,"y":360,"wires":[["e1819526a5f365c8"]]},{"id":"e28a69232f1cac53","type":"smooth","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"Max","property":"payload","action":"max","count":"100","round":"2","mult":"single","reduce":!1,"x":170,"y":300,"wires":[["cc3978c7c4ea56ed"]]},{"id":"aecb1727be523240","type":"debug","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"debug 23","active":!0,"tosidebar":!1,"console":!1,"tostatus":!0,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":480,"y":300,"wires":[]},{"id":"e30039ee13e480a8","type":"debug","z":"dd95c0bca1101c86","g":"37380f26e8bfc98a","name":"debug 25","active":!0,"tosidebar":!1,"console":!1,"tostatus":!0,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":480,"y":360,"wires":[]},{"id":"ec11a9ee9148b0b5","type":"group","z":"dd95c0bca1101c86","name":"Evaluate if an incoming value is between flow.high and flow.low, if it is not, send the message down a different wire and show an alert in debug","style":{"label":!0},"nodes":["9393c22b2e3c1ec8","1cd18307cb919159","7c1f474e646765a7","a209cd10f33ec401","f4b15283e55babf4","e6bf7b494b48861e"],"x":34,"y":419,"w":1032,"h":162},{"id":"9393c22b2e3c1ec8","type":"switch","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"Was the value between flow.high and flow.low?","property":"payload","propertyType":"msg","rules":[{"t":"btwn","v":"high","vt":"flow","v2":"low","v2t":"flow"},{"t":"else"}],"checkall":"true","repair":!1,"outputs":2,"x":300,"y":480,"wires":[["1cd18307cb919159"],["7c1f474e646765a7","a209cd10f33ec401"]]},{"id":"1cd18307cb919159","type":"debug","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"debug 15","active":!0,"tosidebar":!0,"console":!1,"tostatus":!1,"complete":"false","statusVal":"","statusType":"auto","x":560,"y":460,"wires":[]},{"id":"7c1f474e646765a7","type":"debug","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"debug 16","active":!1,"tosidebar":!0,"console":!1,"tostatus":!1,"complete":"false","statusVal":"","statusType":"auto","x":560,"y":540,"wires":[]},{"id":"a209cd10f33ec401","type":"change","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"Alert to debug when value is outside of the range","rules":[{"t":"set","p":"payload","pt":"msg","to":"The value was outside of the range","tot":"str"}],"action":"","property":"","from":"","to":"","reg":!1,"x":690,"y":500,"wires":[["f4b15283e55babf4"]]},{"id":"f4b15283e55babf4","type":"debug","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"debug 17","active":!0,"tosidebar":!0,"console":!1,"tostatus":!1,"complete":"false","statusVal":"","statusType":"auto","x":960,"y":500,"wires":[]},{"id":"e6bf7b494b48861e","type":"link in","z":"dd95c0bca1101c86","g":"ec11a9ee9148b0b5","name":"link in 2","links":["e4f972f9daad6246","c07b2e101cecbd3b"],"x":75,"y":480,"wires":[["9393c22b2e3c1ec8"]]},{"id":"caf4214602d5f2c9","type":"group","z":"dd95c0bca1101c86","name":"Manually send a spurious value","style":{"label":!0},"nodes":["14097fb7ba3a9ecc","c07b2e101cecbd3b"],"x":34,"y":159,"w":232,"h":82},{"id":"14097fb7ba3a9ecc","type":"inject","z":"dd95c0bca1101c86","g":"caf4214602d5f2c9","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":!1,"onceDelay":0.1,"topic":"","payload":"75","payloadType":"num","x":130,"y":200,"wires":[["c07b2e101cecbd3b"]]},{"id":"c07b2e101cecbd3b","type":"link out","z":"dd95c0bca1101c86","g":"caf4214602d5f2c9","name":"link out 2","mode":"link","links":["33594c64783cdc45","e6bf7b494b48861e"],"x":225,"y":200,"wires":[]}] ``` ### 2. Perform simple maths functions using JSONata in Change nodes You can perform basic maths functions using the Change node and JSONata. Let's say you wanted to take a payload and multiply it by a value. You could use a custom node such as [node-red-contrib-calc](https://flows.nodered.org/node/node-red-contrib-calc){rel=""nofollow""} but you can also easily complete the same task within a change node. ![Using JSONata in a Change node to multiply a payload by 2.5](https://flowfuse.com/blog/2023/03/images/jsonata.png "Using JSONata in a Change node to multiply a payload by 2.5") This will take the input payload, multiply it by 2.5 then output it as the new payload. You can try this out using the code below. ::render-flow ```json [{"id":"6bbe9c1e81c4ee39","type":"inject","z":"cfe9fec308e144db","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2","payloadType":"num","x":350,"y":400,"wires":[["07aa636f3db17775"]]},{"id":"07aa636f3db17775","type":"change","z":"cfe9fec308e144db","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"msg.payload * 2.5","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":440,"wires":[["8bde558e6e2f8551"]]},{"id":"8bde558e6e2f8551","type":"debug","z":"cfe9fec308e144db","name":"debug 26","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":680,"y":440,"wires":[]},{"id":"1c04633997beb150","type":"inject","z":"cfe9fec308e144db","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3","payloadType":"num","x":350,"y":440,"wires":[["07aa636f3db17775"]]},{"id":"bc52c3d2f38115b1","type":"inject","z":"cfe9fec308e144db","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"4","payloadType":"num","x":350,"y":480,"wires":[["07aa636f3db17775"]]}] ``` :: ### 3. Use the Catch node to trigger flows on errors Sometimes you might be working with nodes which don't output anything when they error or maybe output text directly to debug. This makes it difficult for you to run flows when something fails. For example, when using the Read File node, where the expected file is not found, it would be useful to be able to run a specific flow which sends an alert. You can do this using the Catch node. Drop the node onto your workspace then select if you want errors from some or all nodes. For this example I am going to select just the Read File node. If I then rerun the flow I get an error message out of the Catch node every time there is an error with reading the file. :video{ariaLabel="Catching an error from the Read File node and outputting a message to debug" autoPlay="true" height="234" loop="true" muted="true" playsInline="true" preload="none" width="836"} Note that there are no wires connecting the flow to the error output. This means you can have a single Catch node monitoring a whole project and logging errors as well as sending alerts as needed. You can import the flows from this example using the code below. ::render-flow ```json [{"id":"d6399c6fddb572ef","type":"debug","z":"0c6a2ba248b5933f","name":"debug 28","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1100,"y":300,"wires":[]},{"id":"de70fda720070c57","type":"inject","z":"0c6a2ba248b5933f","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":790,"y":260,"wires":[["a18f9c8638c78e57"]]},{"id":"a18f9c8638c78e57","type":"file in","z":"0c6a2ba248b5933f","name":"","filename":"example.txt","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":930,"y":260,"wires":[[]]},{"id":"2dbb0cc4bc10d0bc","type":"catch","z":"0c6a2ba248b5933f","name":"","scope":["a18f9c8638c78e57"],"uncaught":false,"x":790,"y":300,"wires":[["b22988df6357a52a"]]},{"id":"b22988df6357a52a","type":"change","z":"0c6a2ba248b5933f","name":"Debug message","rules":[{"t":"set","p":"payload","pt":"msg","to":"There was an error reading the file","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":940,"y":300,"wires":[["d6399c6fddb572ef"]]}] ``` :: We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). ### Simplifying Multi-Instance Communication with FlowFuse Project Nodes Coordinating communication between multiple Node-RED instances can be challenging, but FlowFuse's [Project Nodes](https://flowfuse.com/docs/user/projectnodes/) make it effortless. With these nodes, you can seamlessly send messages between instances without dealing with complicated network configurations. Simply choose the target instance by name, and FlowFuse handles the connection automatically. This streamlines the management of multi-instance environments, ensuring smooth communication between flows across different devices or locations. Whether you're handling multiple projects or managing large-scale systems, FlowFuse Project Nodes help you save time and minimize errors. FlowFuse continues to push the boundaries of collaboration and scalability in Node-RED projects. For more details on these features, visit the [FlowFuse website](https://flowfuse.com). # Node-RED Tips - Importing, Exporting, and Grouping Flows There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. ### 1. Copy and share your flows using Export and Import Node-RED provides both import and export features that allow you to save your flows and settings as compressed JSON files. You can use these features to easily move your flows between multiple Node-RED instances or to back up your work. To import a flow, follow these steps: 1. Click on the three horizontal lines in the upper right corner of the Node-RED editor and click on "Import". 2. Select the JSON file that contains the flow you want to import. 3. Click on "Import" and the flow will be imported into the current instance of Node-RED. To export a flow, follow these steps: 1. Click on the three horizontal lines in the upper right corner of the Node-RED editor and click on "Export". 2. Select the type of export you want to perform - this can be either the "Clipboard" or "File". 3. If you chose "File" you will be prompted to save a compressed JSON file to your computer; if you chose "Clipboard" the flow JSON will be copied to your clipboard. 4. Use the exported file or clipboard content to import into a different instance of Node-RED. :video{ariaLabel="Importing and exporting your flows" autoPlay="true" height="474" loop="true" muted="true" playsInline="true" preload="none" width="978"} Keep in mind that some nodes or flows may require additional setup or node installation on the Node-RED instance you import your flow to. ### 2. Import helpful example flows provided with custom nodes In Node-RED, custom nodes can include examples that provide users with a starting point for using the node. These examples can help users understand how a node works and how it can be integrated into their flows. To use example flows in custom nodes, follow these steps: 1. Open the Node-RED editor and drag the custom node you want to use into your flow. 2. Double-click on the node to open its configuration panel. 3. Look for an "Examples" menu or button within the node configuration panel. The name and location of the Examples button can vary depending on the node. 4. Click on the "Examples" button to bring up a list of example flows included with the node. 5. Select the example flow you want to use and click on "Import" to add the example flow to your Node-RED workspace. Once the example flow has been added to your workspace, you can modify it to fit your specific needs. :video{ariaLabel="Using the example flow included in the moment node" autoPlay="true" height="474" loop="true" muted="true" playsInline="true" preload="none" width="978"} It's important to note that while custom node examples can be a useful starting point, they may not always work seamlessly with your other flows or nodes. Be sure to thoroughly test any custom node examples before incorporating them into a production environment. ### 3. Group nodes together to make your flows easier to read The group feature in Node-RED allows users to visually group nodes together within the workspace. This feature offers several benefits: 1. Improved organization: The group feature allows you to group related nodes visually, which can make your flow easier to understand and navigate. This can be particularly helpful for larger, more complex flows. 2. Simplified editing: When you group nodes together, you can edit or move them as a single unit, rather than individually. This can save time and reduce the chance of errors. 3. Easier sharing: When you share your flow with others, the group feature allows you to package related nodes together, making it easier for others to understand and use your flow. 4. Reduced clutter: Grouping nodes can help reduce the visual clutter in your workspace, making it easier to focus on key aspects of your flow. :video{ariaLabel="Grouping your nodes to make them easier to read" autoPlay="true" height="474" loop="true" muted="true" playsInline="true" preload="none" width="978"} Overall, the group feature in Node-RED is a valuable tool that can help users better organise, edit, and share their flows. We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). You can read our previous Node-RED tips here. [Node-RED Tips - Smooth, Catch, and Math](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-4/):br[Node-RED Tips - Exec, Filter, and Debug](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-3/):br[Node-RED Tips - Deploying, Debugging, and Delaying](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-2/):br[Node-RED Tips - Wiring Shortcuts](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-1/) ## Version Control and Collaboration for Your Node-RED Flows [FlowFuse](https://flowfuse.com) simplifies managing and collaborating on your Node-RED flows with seamless version control. You can easily track changes, take snapshots, and revert to previous versions, ensuring your work is always safe and recoverable. With FlowFuse [Team Library](https://flowfuse.com/docs/user/shared-library/#shared-team-library), sharing flows across different Node-RED instances is effortless. This Library feature allows you to organize and share flows among team members without the need for manual copying, making collaboration more efficient and effective. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} for a free trial today and discover how FlowFuse can enhance your Node**\* # Community News March 2023 Welcome to the FlowFuse newsletter for March 2023, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. ## Upcoming events ### Node-RED Ask Me Anything Back by popular demand, FlowFuse is hosting a monthly Node-RED Ask Me Anything session on March 9th. This is a great opportunity to ask Nick O’Leary, co-creator of Node-RED & FlowFuse CTO, and Rob Marcer, Node-RED FlowFuse Developer Educator your questions about Node-RED. ### DevOps for Node-RED: An Introduction to FlowFuse Webinar Join Nick O'Leary, FlowFuse CTO, as he presents an Introduction to FlowFuse and demonstrates FlowFuse’s platform for providing DevOps for Node-RED. This webinar will be on March 30th. [Register today](https://flowfuse.com/webinars/2023/introduction-to-flowforge/). ## From our Blog [Toward Highly Available Node-RED](https://flowfuse.com/blog/2023/02/highly-available-node-red/) - High availability is an often requested feature for Node-RED. This post from FlowFuse CEO discusses our approach to HA for Node-RED. [MING Stack for IoT](https://flowfuse.com/blog/2023/02/ming-blog/) - MING technology stack include M (Mosquitto/MQTT), InfluxDB, Node-RED and Grafana. Ian Skerrett, FlowFuse Head of Marketing discusses how this tech stack is used for IoT. [Introduction to Node-RED](https://www.youtube.com/watch?v=47EvfmJji-k){rel=""nofollow""} - An in-depth webinar recording on key Node-RED concepts and demonstration on how to get started with Node-RED. Node-RED Quick Tips - Rob Marcer, FlowFuse Developer Educator has a weekly series of Node-RED hints and tips - [Tips #1](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-1/) - [Tips #2](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-2/) ## From the Community ### Node-RED Community Survey The Node-RED open source project is running an Node-RED Community Survey. Give your [feedback on how you are using Node-RED](https://nodered.org/blog/2023/02/23/community-survey){rel=""nofollow""}. ### Good alternatives to Pis for your next project Raspberry Pis continue to be difficult to purchase. Eben Upton of the Raspberry Pi Foundation has said that [supply should improve this year](https://www.raspberrypi.com/news/supply-chain-update-its-good-news/){rel=""nofollow""} but in the mean-time there are some good alternatives you could consider. The Youtube channel [ExplainingComputers](https://www.youtube.com/@ExplainingComputers){rel=""nofollow""} has shared a [great video covering some of the most popular SBCs](https://www.youtube.com/watch?v=k8clrUclPIs){rel=""nofollow""} you could use for your next project, it’s worth a watch. ### Custom Node Spotlight - node-red-contrib-os [OS](https://flows.nodered.org/node/node-red-contrib-os){rel=""nofollow""} is a great custom node which allows you to monitor the performance of the device you are running Node-RED on. It can check RAM usage, disk space, CPU load, and a lot more. It’s really easy to use, we recommend you [take a look](https://flows.nodered.org/node/node-red-contrib-os){rel=""nofollow""}. ## Join Our Team FlowFuse is expanding our team. We have two openings right now: - **[Developer Advocate - Manufacturing & Industrial Automation](https://boards.greenhouse.io/flowfuse/jobs/4798023004){rel=""nofollow""}** - **[DevOps Engineer](https://boards.greenhouse.io/flowfuse/jobs/4796271004){rel=""nofollow""}** # Comparing Node-RED Dashboards Solutions Dashboards are a great feature of Node-RED, allowing you to easily expose data visualisations and interactive elements of your flows to users via a web browser. I often see discussions in the community about which dashboard option is best for any given scenario, I wanted to compare the most popular options as they stand in early 2023. ::div{.blog-update-notes} **UPDATE:** Since this article was published, it's worth noting a couple of important updates: - FlexDash is no longer maintained and supported. - [Node-RED Dashboard 2.0](https://flowfuse.com/platform/dashboard/) has been released, which is a new, modern dashboard stack for Node-RED, and offers all of the benefits of the original "Node-RED Dashboard", plus more. :: ## Which dashboards am I going to consider? Based on their downloads per week and active development, I believe there are 3 main dashboards worth considering. In no particular order, they are [Dashboard](https://flows.nodered.org/node/node-red-dashboard){rel=""nofollow""}, [uibuilder](https://flows.nodered.org/node/node-red-contrib-uibuilder){rel=""nofollow""}, and [FlexDash](https://flows.nodered.org/node/@flexdash/node-red-fd-corewidgets){rel=""nofollow""}. It's not to say that there are not other options, I am focusing on the dashboards I believe are popular in the Node-RED community. I'd like to take this opportunity to thank the project leads for each of the three dashboards for responding to me and providing their take on the current state and future development of each. Where possible I have quoted their words, either from their messages to me or from the projects' documentation. Thanks to [Dave](https://github.com/dceejay){rel=""nofollow""}, [Julian](https://github.com/TotallyInformation){rel=""nofollow""}, and [Thorsten](https://github.com/tve){rel=""nofollow""} for their replies as well as all the work they've put into these great projects! ## Methodology To compare these dashboards, I am going to consider each of them based on the following factors: - How easy is it to install? - How easy is it to get your first demo dashboard running? - How extensive is the collection of UI elements? - How good is the support and documentation? - How 'cloud native' is the dashboard? - How active is each project's development? - What are the future development plans? I am assuming the user is a low-code developer. They may have limited experience with coding and are most comfortable working in visual interfaces. So, that's the methodology, let's get on with looking at the strengths of each project. :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} ## How easy is it to install? ### uibuilder - 1st place A search on Google for uibuilder returns the correct custom node. When searching for the custom node in the palette manager there is only one result, this is great as users are very likely to install what they were searching for. Once you've found the correct custom node, the installation takes just a few moments using the palette manager. ### Dashboard - 2nd place As Dashboard is currently the most popular solution to build dashboards in Node-RED, it's very easy to find both in search engines and in the Node-RED interface. A Google search brings up the correct custom node. Finding this custom node in Node-RED's palette manager is not quite as easy, at the time of writing it's the third from top result for the search term 'dashboard'. Some users might not select the intended item from the palette manager on first attempt. However, once you have found the correct custom node, installation is easy and takes just a few moments. ### FlexDash - 3rd place When searching for 'FlexDash node red' on Google, the top result is the Node-RED website for the custom node. The issue with this, and this is also a problem when searching in the palette manager, is the project 'FlexDash' is apparently not what we actually need to install. When reading the readme for the project on Github it says *'You most likely do not want to explicitly install this package, you want to install the [core widgets](https://github.com/flexdash/node-red-fd-corewidgets){rel=""nofollow""}, which will bring in this package and more and will provide a usable whole'.* Credit to the developers for adding in this helpful text but I suspect most users will start off by installing FlexDash then later discover that was not the correct way to proceed. It would be great if the custom node which needs to be installed was the one called 'FlexDash' in my opinion. This problem is compounded by there being no help file at all for 'FlexDash' showing up on the Node-RED web site. That may well be a deliberate attempt to help users get the right custom node installed, but it was still a confusing start for me and I suspect other users will have a similar experience. When setting up FlexDash, one thing that wasn't immediately obvious was that I needed to restart Node-RED before the custom node showed in the palette. This step is [covered in the docs](https://flexdash.github.io/docs/quick-start/#installing-flexdash-in-node-red){rel=""nofollow""} but I suspect a lot of users will get stuck working out why the palette manager says the custom node is installed but nothing new has been added to the palette. There is also an [ongoing discussion](https://github.com/node-red/node-red/issues/569){rel=""nofollow""} about a way to resolve issue by changing how Node-RED deals with dependencies which sounds promising. I believe a few improvements to the install process could make FlexDash a much more popular custom node. ## How easy is it to get your first demo dashboard running? ### FlexDash - 1st place Getting an example dashboard up and running in FlexDash is very easy thanks to the example flows which are included in the package. Simply go to 'Import', 'Examples' then select 'Hello-world' from the example flows. Now deploy and add /flexdash to the end of the URL of your Node-RED editor and you should have your first dashboard running. ### uibuilder - 2nd place It was quite simple to get an example dashboard up and running in uibuilder. As with FlexDash, there are examples you can import. Once we import an example we do start to see the significantly different approach to delivering dashboards with uibuilder to the other two solutions. The examples seem to demonstrate how you could build a dashboard rather than showing specific UI elements such as charts in use. ### Dashboard - 3rd place Getting your first dashboard running in Dashboard is quite easy, once installed you need to drag in a Dashboard UI element then assign that to a UI group and tab. The group and tab can be left as their default options (home) which I suspect most users will work out quickly. You then need to deploy your flow and visit the dashboard using '/ui' on the end of the URL of your Node-RED editor and you are up and running. Dashboard would benefit from some example flows as we see with the other two custom nodes. ## How extensive is the collection of UI elements? ### FlexDash and Dashboard - joint 1st place It's really hard to separate these two, when considering the UI elements they come with. They both have out of the box solutions for charts, gauges, buttons, drop downs, toggles, text etc. I think they both deserve 1st place in this category. ### uibuilder - 3rd place This is possibly a little unfair on uibuilder. Arguably by design, uibuilder does not currently include many UI elements. To add most useful elements (charts, gauges etc) to your dashboard, you will need to set out your design in HTML or look at using one of the supported frontend frameworks. This makes uibuilder more versatile for users who are comfortable using code to set out dashboards but for the low-coders among us it's less ideal. ## How good is the support and documentation? ### All three - joint first place All three projects have an active community and good support documentation. Where as I may have a personal preference about how I like documentation to be set out, I don't think that makes any one project better than the rest. ## How 'cloud native' is the dashboard? In the words of the [Cloud Native Computing Foundation](https://www.cncf.io/){rel=""nofollow""} '*Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach*'. '*These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil*'. So, how well does each project conform to these ideals? ### Dashboard - 1st place Dashboard stores all configuration data within the Node-RED instance. When deploying an existing Node-RED project to a new instance everything just works exactly as it did previously. ### FlexDash - 2nd place As with Dashboard, everything required to define each dashboard is stored within Node-RED. This makes redeployment trivial. Unfortunately, as the Node-RED instance currently needs to be restarted before FlexDash works, it just missed out on joint first place. It would be great to see that issue resolved in future versions. There is also an [ongoing discussion](https://github.com/node-red/node-red/issues/569){rel=""nofollow""} about a way to resolve issue by changing how Node-RED deals with dependencies. ### uibuilder - 3rd place uibuilder uses the filesystem of the host instance to store its configuration. In practice this means that if you migrate the Node-RED project files to a new location you probably will find your dashboard no longer works. This can be mitigated by also migrating the filesystem (for example using persistent storage in Docker) and re-deploying via a Docker registry but it would be great to see uibuilder move towards not being dependant on the filesystem as it will make DevOps tasks that much easier. ## How active is each project's development? ### uibuilder - 1st place ![Image showing the uibuilder Github Commits](https://flowfuse.com/blog/2023/03/images/uibuilder-activity.png "Image showing the uibuilder Github Commits") uibuilder has has consistent commits to the project going back several years with even greater activity since the start of 2022. ### FlexDash - 2nd place ![Image showing the FlexDash Github Commits](https://flowfuse.com/blog/2023/03/images/flexdash-activity.png "Image showing the FlexDash Github Commits") The commits to FlexDash have been regular since mid 2022. ### Dashboard - 3rd place ![Image showing the Dashboard Github Commits](https://flowfuse.com/blog/2023/03/images/dashboard-activity.png "Image showing the Dashboard Github Commits") Dashboard is now in a a maintenance only state. In the words of the project lead, *'Angular 1 (the framework used to build Dashboard) is now unsupported and it's just a matter of time before there is a serious security hole raised against it, for which there will be no fix. Of course we don't use all the features of it so we may be lucky that an exploit doesn't necessarily expose us directly but it will compromise any audits people may wish to do'*. In practice, this means that sooner or later using Dashboard might become a significant security risk. ## What are the future development plans? ### uibuilder - joint firstplace During the writing of this article, uibuilder released a new version with some significant new features. In the words of the project lead when talking about version 6.1.0, '*It feels like uibuilder really is growing up. No more apologising for not being a direct Node-RED Dashboard replacement, uibuilder has its own path*. *You can now create and update visible web page elements direct from Node-RED data without needing to understand all of the intricacies and inconsistencies of HTML. You can create your own utility tools either in Node-RED or in front-end code that leverages the low-code UI features of uibuilder*'. It's great to see projects under active development. My greatest difficulty when using uibuilder, is I found it hard to create the UI elements I needed based on low-code workflows. According to the [development roadmap for uibuilder](https://totallyinformation.github.io/node-red-contrib-uibuilder/#/roadmap){rel=""nofollow""} the project should progress towards being easier and easier for low-coders to make use of. To again quote the project lead, '*In general, the ongoing direction of travel is to enable more zero-code features that will work both in Node-RED flows and in front-end custom code. The low-code feature set is already quite mature now and is well documented enough that it can be used by other tools should anyone wish to do so. I will be making sure that both the zero-code and low-code features are as easy to use as possible both from Node-RED and front-end code for maximum flexibility*'. ### FlexDash - joint firstplace The project Lead for FlexDash had the following to say about the future development of the project. '*FlexDash currently has a fairly rigid overall page structure: there's a tab bar and each tab's content is organized in grids of widgets. The plan is to open this up fully so the user can start from a blank page and place containers which contain widgets. This way almost any layout could be implemented in FlexDash*'. '*I also would like to improve the multi-user capabilities of FlexDash by supporting authentication and making it easier for users to implement flows that present per-user data in the dashboard*'. After using FlexDash over the past couple of weeks and finding it to be already be a strong contender for all my Node-RED dashboard needs, it's great to see it continuing to be improved. ### Dashboard - third place ***Important Update: New Generation of Node-RED Dashboard Released:*** *A new generation of the outdated and unmaintained Node-RED Dashboard has been released to replace it. Introducing [Node-RED Dashboard 2.0](https://flowfuse.com/platform/dashboard/), built on Vue.js, offering significantly more versatility than its predecessor. This new dashboard, managed by FlowFuse, is designed to allow full customization, addressing the limitations of the previous version.* \*Node-RED Dashboard 2.0 retains most of the widgets and concepts from the old version, so transitioning from Node-RED Dashboard 1.0 to 2.0 is easy. For a full feature comparison you can check out the [Migration Guide](https://dashboard.flowfuse.com/user/migration.html){rel=""nofollow""}. Dashboard 2.0 includes various chart types such as line, scatter, bar, gauge, and more, and it's compatible with all the [Vuetify component library](https://vuetifyjs.com/en/components/all/#containment){rel=""nofollow""}, making it easier to build advanced dashboards.\* *Furthermore, the team is continuously working on adding more amazing features to enhance the user experience. For a smooth transition, FlowFuse provides easy-to-follow guides. Refer to [Node-RED Dashboard 2.0 Guides](https://flowfuse.com/blog/dashboard/) for more information.* As mentioned above, Dashboard is no longer in active development. This is due to the framework upon which it was build [(AngularJS)](https://angularjs.org/){rel=""nofollow""} now being unsupported as of the end of 2021. You can read a lot more detail on why ongoing development of Dashboard is not practical in this [thread on the Node-RED forums](https://discourse.nodered.org/t/discussion-about-a-new-dashboard/51119/3){rel=""nofollow""}. There could possibly be new effort put into porting Dashboard over to a new framework but that is a significant amount of work. I suspect it would take hundreds of hours development just to get the feature set back to the same state as the current version so I suspect it won't ever happen. ## Conclusions Personally, I was a little surprised by these results. I have used Dashboard for around 3 years and always found it to be a great tool for putting together quick and informative dashboards. That being said, when attempting to objectively compare it to uibuilder and FlexDash, the other two projects often are individually better in a given category. That coupled with the halt of development for Dashboard due to AngularJS being no longer supported, it's hard to recommend Dashboard for totally new Node-RED users in 2023, especially for commercial projects. If you already use Dashboard, in a non-commercial setting you should probably continue to do so, you might find that its development slows down to a near stall due to the underlying framework now being abandoned but for at least as of right now it's a great solution to build your Node-RED dashboards in. FlexDash is probably the best low-code solution for building dashboards in Node-RED. If you don't get blocked by the confusing install process I believe it's the one to pick up at the time of writing due to it's ongoing support and low-code interface. uibuilder is currently not what I would consider a truly low-code option for creating dashboards but it is moving in that direction. It has some great features and is extremely flexible so it has a good chance of ending up as the most popular solution to build dashboards in Node-RED in the long term. That being said, as of time of writing unless you are a 'coder' you will may struggle to build a dashboard using it. # FlowFuse v1.5 Now Available For FlowFuse 1.5 we have been busy making a lot of UX changes and upgrading our underlying architecture to enable future innovations on the FlowFuse platform. With our recently announced [Terminology Changes](https://flowfuse.com/blog/2023/03/terminology-changes/), we have introduced some new concepts into FlowFuse. - **Application**: A group of Node-RED Instances Each instance can run locally (in FlowFuse) or remotely (on Devices) - **Instances**: We renamed "Projects" to "Instances" to be more inline with the terminology used in the Node-RED community As such, our User Experience has been updated to reflect these changes, and allow for further functionality to be introduced with our plans for [Multiple Instances per Application](https://github.com/FlowFuse/flowfuse/issues/1689){rel=""nofollow""}. ### "Applications" View At the top-level in FlowFuse, you can now see a list of your "Appications". In FlowFuse 1.5, as we still have a 1:1 relationship of Applications to Local Instances, this will be the same as the list of "Projects" that you're used to seeing. !\[Screenshot to show the new "Applications" view]\(./images/screenshot-applications.png "Screenshot to show the new "Applications" view") **"Applications" view in FlowFuse, listing all available Applications** For 1.5, all of your settings, environment variables, etc. are all now at the "Instance" level. Applications will gain a lot more functionality in future releases. ### "Instances" View When clicking on one of your Applications, you will see a list of Node-RED instances bound to that Application. !\[Screenshot to show the new "Instances" view]\(./images/screenshot-instances.png "Screenshot to show the new "Instances" view") **A list of Instances contained within a single Application.** Clicking on this Instance, will open up the "Instance" view, this is an exact replica of the "Project" view you'll be used to seeing in FlowFuse, and contains all of the same functionality: !\[Screenshot to show the new "Instances" view]\(./images/screenshot-instance.png "Screenshot to show the new "Instances" view") **FlowFuse 1.5's "Instance" view. This contains all of the functionality previously found in the "Project" view.** ### Devices & Managing Remote Instances Devices are now bound to "Instances", you'll see these in the "Devices" view, and can be managed and deployed to in exactly the same way as before. Devices will run whatever you've selected as your "Target Snapshot" for this Instance. !["Screenshot to show an Instance's 'Devices' view"](https://flowfuse.com/blog/2023/03/images/screenshot-devices.png "Screenshot to show an Instance's 'Devices' view") **"Devices" view, available for a given Node-RED Instance. This lists all of the connected devices to a given instance, that will automatically update when a new Target Snapshot is set.** ## Node-RED 3.1 Beta Available FlowFuse Cloud is a great place to try out the new Node-RED features, with FlowFuse Cloud now including the [Node-RED 3.1.0-beta.2](https://discourse.nodered.org/t/node-red-3-1-0-beta-2-released/76192){rel=""nofollow""}. If you want to try this version you can [duplicate your application](https://flowfuse.com/docs/user/instance-settings/#copy-instance) or [upgrade your stack](https://flowfuse.com/docs/user/changestack/). ## Other Improvements - Update to audit logs to improve usability \[[#1800](https://github.com/FlowFuse/flowfuse/issues/1800){rel=""nofollow""}] \[[#1785](https://github.com/FlowFuse/flowfuse/issues/1785){rel=""nofollow""}] - Improve how licensing works with overages, for easier scaling of FlowFuse and your Node-RED Instances \[[#1639](https://github.com/FlowFuse/flowfuse/issues/1639){rel=""nofollow""}] \[[#1739](https://github.com/FlowFuse/flowfuse/issues/1739){rel=""nofollow""}] ## Bug Fixes - Device "Last Seen" status shows "never" even though it has previously been seen \[[#1723](https://github.com/FlowFuse/flowfuse/issues/1723){rel=""nofollow""}] - Improved Safe Mode launch for small projects \[[#1579](https://github.com/FlowFuse/flowfuse/issues/1579){rel=""nofollow""}] ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.5. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That's also a great place to send us any feedback or feature requests. You can also get help on [the Node-RED forums](https://discourse.nodered.org/){rel=""nofollow""} As well as in the [forum within our Github project](https://github.com/FlowFuse/flowfuse/discussions){rel=""nofollow""} Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""} You can raise a support ticket by emailing We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. # IBM Cloud removes Node-RED starter application IBM Cloud has [recently announced](https://www.ibm.com/cloud/blog/announcements/deprecation-of-ibm-cloud-starter-kits){rel=""nofollow""} that they will no longer be providing their Cloud App Service Starter Kits, including the [Node-RED Starter Application](https://developer.ibm.com/tutorials/how-to-create-a-node-red-starter-application/){rel=""nofollow""}. ## Node-RED Starter Application If you're looking for an alternative place to get started with Node-RED then FlowFuse, founded by Node-RED co-creator Nick O'Leary, are here to help you with Cloud-hosted Node-RED as well as infrastructure and tooling to scale your Node-RED instances in production. As an ex-IBM Employee myself, over the years, I've been very dependant upon the Node-RED Starter Application in IBM Cloud. I'd used it dozens of times with clients to showcase the value of Node-RED and how easy it is to spin up integrations between hardware devices, APIs and online services. If you're now looking for somewhere to rely on in order to easily spin up new instance of Node-RED, then FlowFuse is the obvious answer. If you're completely new for Node-RED too, we can also help you there with our [Getting Started](https://flowfuse.com/blog/2023/01/getting-started-with-node-red/) guide. You can sign up for a [free FlowFuse Cloud Account](https://app.flowfuse.com/account/create){rel=""nofollow""} where you'll be given one small Node-RED instance for free, for your first month. ## Integrations If you've seen the excellent ["Create a Node-RED starter application"](https://developer.ibm.com/tutorials/how-to-create-a-node-red-starter-application/){rel=""nofollow""} article on IBM Developer, you'll probably be looking to connect up to a Cloudant Instance, or other IBM Cloud Services. Don't worry. All of that is still available through Node-RED on FlowFuse. You can install the relevant nodes in one of two places: 1. **Node-RED Palette Manager:** Click "Menu > Manage Palette > Install". The menu is available via the icon in the top-right of your running Node-RED Instance) ![Screenshot of Node-RED's Manage Palette menu](https://flowfuse.com/blog/2023/03/images/nr-manage-palette-cloudant.png "Screenshot of Node-RED's Manage Palette menu") 2. **FlowFuse Instance Settings:** For a given Instance in FlowFuse, click "Settings > Palette". You can then define the npm module name and versions explicitely in the "Installed Modules" section ![Screenshot of FlowFuse's "Installed Modules" option in Instance > Settings > Palette](https://flowfuse.com/blog/2023/03/images/ff-installed-modules.png "Screenshot of FlowFuse's 'Installed Modules' option in Instance > Settings > Palette") It's also easy to setup [Environment Variables](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/) for Node-RED in FlowFuse for when you integrate with external services like APIs too. ## Security As with IBM Cloud, FlowFuse makes it very easy to secure your Node-RED Applications. [FlowFuse offers three tiers of security](https://flowfuse.com/docs/user/instance-settings/#security) options on your Node-RED Instances to secure any exposed HTTP routes on your Node-RED instance, e.g. REST API endpoints or Node-RED Dashboard. - **None**: Anyone will be able to access the exposed routes. - **Basic Auth**: Setup a single, dedicated username and password combination that is required in order to access the routes. - **FlowFuse Credentials**: Visitors can use their FlowFuse username/password in order to access the endpoints. This also includes SSO if you have that configured for your FlowFoerge Team. ## Migrating Existing Instances If you're looking to move your Node-RED applications from IBM Cloud, then you can do so through one of two options: ### Node-RED Tools Plugin You can use our Node-RED Tools plugin to migrate your flows and credentials over to FlowFuse. You can read the details in our [Migration Guide](https://flowfuse.com/docs/migration/introduction/), which also includes instructions on how to export your environment variables too. ### Manual Import This will only enable you to import your Flows, not the associated credentials. 1. Export your existing `flows.json` from your IBM Cloud-hosted Node-RED instance by choosing "Export > All Flows > Download" within Node-RED 2. Create a new Application on FlowFuse 3. Once created, click the Node-RED instance that has been generated within your Application 4. Click "Settings" 5. Scroll down and select the "Import Instance" option 6. Choose your `flows.json` file that you downloaded earlier. *If you have any questions about the above, or more generally about Node-RED or FlowFuse, then please do reach out and [get in touch](https://flowfuse.com/contact-us).* # Node-RED: The Integration Platform for IIoT Edge Computing & PLCs Node-RED has become a widely adopted integration platform for IoT edge computing and PLCs. Discover why! ## The Integration Platform for IIoT Edge Computing & PLCs Node-RED is a widely adopted open-source low-code development tool that makes it easy to connect and integrate different sources of data. With a visual programming interface and drag-and-drop functionality, [Node-RED](https://flowfuse.com/node-red/) makes it possible for software developers and non-professional software developers to create sophisticated applications. In the manufacturing and industrial automation industry, the focus of the Industrial Internet of Things (IIoT) has been on integrating industrial processes and equipment to enable real-time monitoring, control, and analysis of data. For many use cases, instead of sending all the data to the cloud, the best practice for processing industrial data is to deploy the application to the edge of the network, referred to as edge computing. By processing the data closer to the data source, edge computing has many benefits, including reduced latency, limited downtime, conserving bandwidth, and increased privacy, and security. ## How Node-RED fits into IIoT Edge Computing A key challenge for IIoT edge computing is the wide variety of different hardware platforms, protocols and sources of data and processes. Over the years, the Node-RED community has built [thousands of nodes and flow](https://flows.nodered.org/){rel=""nofollow""}s to support a wider range of these sources of data, including support for [Modbus](https://flows.nodered.org/node/node-red-contrib-modbus){rel=""nofollow""}, [OPC-UA](https://flows.nodered.org/node/node-red-contrib-opcua){rel=""nofollow""}, [S7](https://flows.nodered.org/node/node-red-contrib-s7){rel=""nofollow""}, [MQTT](https://cookbook.nodered.org/mqtt/){rel=""nofollow""}, etc. Node-RED also has nodes for graphics and dashboards to make it trivial to visualize industrial data. Node-RED’s visual programming environment makes it accessible to non-developers. Manufacturing, mechanical, and electrical engineers in the factories are typically the domain experts in understanding the existing systems and often lead IIoT initiatives. Node-RED enables these engineers to quickly innovate and create real value for their organizations. This makes it a popular choice for engineers looking to create edge computing solutions. ## PLC and IoT Gateway Vendors Embrace Node-RED PLC and IoT Gateway vendors are at the forefront of promoting edge computing. They see edge computing as a way to modernize hardware in the factory and for remote asset management. PLC and IoT gateways often sit in front of old legacy systems that don’t have the connectivity or compute platform to enable IIoT applications. Many of these hardware vendors realized they need an application delivery platform for their devices. Traditional OT hardware vendors often implement proprietary software stacks that are often difficult to use and closed to integrating with other hardware and software. Forward thinking hardware vendors realized having an open platform is the future of their industry and customers have begun to demand more open platforms. Node-RED’s ease of use, open community and open source license provided the solution many of these hardware vendors were looking for. ## The Standard for Edge Computing and PLCs Today, Node-RED has been adopted by some of the leading PLC and IoT Gateway vendors. The hardware vendor community appears to have standardized on Node-RED as being the edge computing platform for IIoT. Below is a sample of the vendors offering a Node-RED solution: 1. [Advantech](https://www.advantech.com/en-eu/products/node-red-gateways/sub_fb7246cc-cc10-486f-806b-30bb50a90f28){rel=""nofollow""} Node-RED Field Gateway 2. [Bechhoff](https://infosys.beckhoff.com/english.php?content=../content/1033/tf6720_tc3_iot_data_agent/3260672139.html&id=){rel=""nofollow""} TwinCAT 3. [Bivocom](https://www.bivocom.com/products/iot-gateways/edge-iot-gateway-tg452){rel=""nofollow""} TG452 IoT Edge Gateway 4. [BLIIOT Edge Computing Gateway](https://bliiot.com/products/){rel=""nofollow""} EdgeCom BL302 5. [Bosch CtrlX Core](https://developer.community.boschrexroth.com/t5/Store-and-How-to/ctrlX-CORE-Node-RED-App/ba-p/22366){rel=""nofollow""} 6. [Broadsens](https://www.broadsens.com/wireless-gateway/){rel=""nofollow""} GU200 & GU 200S 7. [Emerson](https://www.emerson.com/documents/automation/product-datasheet-pacedge-software-computing-devices-pacsystems-en-7205588.pdf){rel=""nofollow""} PACEdge 8. [Hilscher Automation](https://github.com/HilscherAutomation/netPI-nodered){rel=""nofollow""} 9. [Opto22](https://developer.opto22.com/nodered/general/){rel=""nofollow""} groov RIO & EPIC 10. [Parallax AV](https://www.parallaxcontrol.com/){rel=""nofollow""} Control System 11. [Particle.io](https://docs.particle.io/reference/cloud-apis/node-red/){rel=""nofollow""} Particle 12. [Pepperl+Fuchs](https://www.pepperl-fuchs.com/usa/en/classid_199.htm?view=productdetails&prodid=93839){rel=""nofollow""} AS-Interface gateway 13. [Raspberry Pi](https://projects.raspberrypi.org/en/projects/getting-started-with-node-red){rel=""nofollow""} 14. [Renesas](https://www.renesas.com/us/en/products/programmable-mixed-signal-asic-ip-products/mixed-signal-asics/communication-asics/ftclick-mikrobus-compatible-interface-module){rel=""nofollow""} FT Click 15. [Revolution Pi](https://revolutionpi.com/revpi-connect/){rel=""nofollow""} RevPi Connect 16. [Schneider Electric](https://shop.exchange.se.com/en-US/apps/59823/ecostruxure-plant-data-expert/features){rel=""nofollow""} ExoStructure Plant Data Expert 17. [Siemens](https://github.com/SIMATICmeetsLinux/IOT2050-NodeRed-OPCUA-Server){rel=""nofollow""} S7 PLC 18. [ST-One](https://st-one.io/en/){rel=""nofollow""} 19. [Tulip](https://support.tulip.co/docs/using-node-red-with-edge-mc){rel=""nofollow""} Edge MC & Edge IO 20. [Wago](https://www.wago.com/us/edge-devices-overview){rel=""nofollow""} Edge Controller & Computer 21. [Weidmueller](https://catalog.weidmueller.com/procat/Group.jsp;jsessionid=C885C404E7B4B798B23B8A9BB2200513?groupId=\(%22group14048963834797%22\)&page=Group){rel=""nofollow""} control web There are several key reason Node-RED is so popular for IIoT edge computing, including: - Easy User-friendly interface that makes it accessible to manufacturing engineers that might not have a lot of programming experience. - Large community of open source nodes that integrate with many different OT hardware and protocols. - Open source community and license making it vendor neutral so competing hardware vendors feel comfortable embracing the platform. ## Conclusion IIoT and edge computing is making software more critical to the manufacturing industry. The flexibility to integrate data from different sources to create innovative data centric solutions is primarily software driven. In partnership with OT hardware vendors, Node-RED’s flexible and easy to use environment provides the platform for manufacturing companies to embrace software to develop IIoT solutions. # Terminology Changes As a new product in the market, we constantly have to make choices on how to name things. Naming things is hard! As you name a thing, say "Project", it might be suitable now, but the product evolves, and may outgrow the name such that it doesn’t fit anymore. We are at this point with FlowFuse, and want to walk you through what we have planned, and why we are changing a couple of things. ### Enter the "Application" In [FlowFuse 1.5](https://flowfuse.com/blog/2023/03/flowforge-1-5-0-released/), we have introduced a new concept called an **Application**. An Application will allow you to organize multiple Node-RED instances into a single managed group. As of the 1.5 release, an Application can still only have a single Node-RED instance, but in future releases, Applications will allow for multiple Node-RED instances and will allow us to implement capabilities such as **DevOps Pipelines** and **High Availability**. ### "Projects" to "Instances" Until now, 'Project' encapsulated both the Node-RED instance that was running in FlowFuse, and the associated devices (remote instances), settings and environment variables. It was an overloaded term, and it caused confusion with our users. To simplify things, and to adhere more to the terminology familiar with the Node-RED community, we are renaming Projects to **Instances**. An **Instance** is a customized version of Node-RED that includes various FlowFuse plugins to integrate it with the FlowFuse platform. It can also be used to manage the environment variables used in your Node-RED flows. Instances can either be: - **Local** - An instance of Node-RED running in FlowFuse. - **Remote** - An instance of Node-RED, managed by FlowFuse, running on a Device. In future releases, environment variables will also be able to be stored at the Application level, and shared across multiple Node-RED Instances. ### Devices FlowFuse can also be used to manage remote Node-RED instances. This is typically useful when you have a number of remote devices that are required to run the same Node-RED instance, and may have variation in configuration or environment variables for example. Devices are registered to an Instance, and can be configured to run [Snapshots](https://flowfuse.com/docs/user/concepts/#snapshot) of the Instance running in FlowFuse. To accomplish this remote management capability, the [FlowFuse Device Agent](https://github.com/FlowFuse/device-agent){rel=""nofollow""} needs to be installed on each device. Devices are registered with a Team, and then the appropriate device(s) are assigned to a Node-RED instance that should be deployed to the device(s). When the Node-RED instance is ready for deployment, a user creates a snapshot of the instance and marks it as a target snapshot for the device. We hope these changes will simplify the FlowFuse terminology for our users and allow us to grow the FlowFuse platform. If you have any feedback or thoughts, please do reach out to us. # The benefits and drawbacks of using Node-RED function nodes Function nodes are an essential part of Node-RED. They allow you to write custom JavaScript functions that can be used in your Node-RED flows. In this blog post, I will discuss some of the benefits and drawbacks of using Function nodes in your next project. ## 5 Benefits of using Function Nodes: :video{ariaLabel="Example showing how to use the function node" autoPlay="true" height="394" loop="true" muted="true" playsInline="true" preload="none" width="642"} 1. **Customisation:** Function nodes allow you to write custom JavaScript functions that can be tailored to your specific needs. You can create complex functions that perform a variety of tasks, the only limit is your programming skills. 2. **Reusability:** Function nodes can be reused in multiple flows, saving you time and effort. You can create a library of custom functions that can be easily accessed and reused in different flows. 3. **Debugging:** Function nodes provide an easy way to debug your code. You can use console.log statements to output debug information to the Node-RED debug panel, making it easier to identify and fix issues. 4. **Performance:** Function nodes can be more performant than using multiple nodes to achieve the same result. By combining multiple tasks into a single function, you can improve performance, assuming your code is efficient. 5. **Flexibility:** Function nodes provide a high degree of flexibility. You can use them to perform tasks that are not possible using a single, standard Node-RED node, such as complex data manipulation. ## 5 Benefits of avoiding Function Nodes: :video{ariaLabel="Example showing how to not use the function node" autoPlay="true" height="394" loop="true" muted="true" playsInline="true" preload="none" width="642"} 1. **Simplicity:** Not using function nodes can make your flows simpler and easier to understand. By using standard Node-RED nodes, you can create flows that are easy to follow and maintain for both you and your team. 2. **Ease of Use:** Standard Node-RED nodes are easy to use and require no programming knowledge. This makes it easier for non-technical users to create and maintain flows. 3. **Modularity:** By using standard Node-RED nodes, you can create modular flows that can be easily modified and extended. This makes it easier to add new functionality to your flows as your needs change. 4. **Community Support:** Standard Node-RED nodes have a large and active community, providing support and resources for users. This can make it easier to find solutions to common problems and share knowledge with others. 5. **Compatibility:** Standard Node-RED nodes are usually compatible with all versions of Node-RED, making it easier to migrate flows between different environments. ## How to Easily Create Function Nodes in FlowFuse FlowFuse offers a robust platform for building, scaling, and securing your Node-RED applications. We are constantly adding new features to make it easy to use in the enterprise where you can rapidly improve your industrial processes. The **"FlowFuse Assistant."** for example is an AI-powered tool that simplifies the creation of Function nodes. You only need to provide a prompt, and the assistant generates the Function nodes for you. For more details on using the FlowFuse Assistant, visit [the Assistants Documentation](https://flowfuse.com/docs/user/expert/). ## Conclusion: Function nodes are particularly valuable for users who possess JavaScript programming skills. They allow for complex tasks, advanced data manipulation, and integration with external APIs, providing a high level of customization and flexibility. However, they require a good understanding of JavaScript to implement effectively and can be more challenging to manage and debug compared to standard Node-RED nodes. On the other hand, standard Node-RED nodes offer a simpler and more accessible approach, making it easy for users without programming expertise to create and maintain flows. They are designed for straightforward tasks and provide modularity, benefiting from a supportive community for troubleshooting and knowledge sharing. Ultimately, the choice between using function nodes and standard nodes will depend on your project's requirements and your familiarity with JavaScript. If you seek deep customization and flexibility, function nodes, enhanced by tools like FlowFuse Assistant, might be the best choice. For those who value simplicity and ease of use, standard Node-RED nodes are a great fit. # Node-RED Tips - Subflows, Link Nodes, and the Range Node There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. ### 1. Subflows Subflows are a great way to reuse sections of your flows. Once you have created a subflow, it can easily be dropped into your workspace one or more times. #### Why use subflows? Without using a subflow, you can copy and paste a flow into each place you need to use it. This takes up quite a bit of workspace, and makes it harder to update your flow in the future as you'll have to update each copy. ![Duplication of the flow](https://flowfuse.com/blog/2023/04/images/no-subflow.png "Duplication of the flow") If we instead put the flow into a subflow we'll save a lot of workspace and it will be easier to update the reused sections of the flow if we need to in the future. #### Creating a subflow You can create a subflow using the burger menu in the top right corner of Node-RED, select Subflows, then Create Subflow. Lay out your subflow, making sure you create an input and output. You can even have more than one output if you want. ![Contents of the subflow](https://flowfuse.com/blog/2023/04/images/subflow.png "Contents of the subflow") You can now drop the subflow into your workspace as needed, saving space and making it easier to manage changes to your flow. ![Using the subflow to reduce duplication of flows](https://flowfuse.com/blog/2023/04/images/using-the-subflow.png "Using the subflow to reduce duplication of flows") ### 2. Link Nodes Link Nodes allow you to separate your flows into distinct sections. The wires between the link nodes are not visible until you select that part of the flow. You can also link flows on different tabs together. Formatting your flows into distinct sections using link nodes can make it easier to read and update your work. To use the link node, drag a link in and out node into your flow's workspace. Now draw a wire as you usually would to link to two nodes together. You should see a link between the nodes but it only shows when you have the link nodes selected. :video{ariaLabel="Linking two link nodes together" autoPlay="true" height="164" loop="true" muted="true" playsInline="true" preload="none" width="532"} In this example below, the first and second flows have the same nodes and functionality. In the second image of the workspace I've split the flow into specific groups of nodes. ![A flow without link nodes](https://flowfuse.com/blog/2023/04/images/flow-without-link-nodes.png "A flow without link nodes") It's easier to read and understand the flow once it's split up using the link nodes and groups. ![The same flow as above, now split up using link nodes](https://flowfuse.com/blog/2023/04/images/flow-with-link-nodes.png "The same flow as above, now split up using link nodes") ### 3. Range Node Sometimes you might need to map one numbering scale onto another. For example, where a user has selected a value between 0 and 10 but you want to use and store their response as a percentage. The Range node makes this task very easy. ![Example of using the range node](https://flowfuse.com/blog/2023/04/images/flow-using-range.png "Example of using the range node") To configure the node, set it up as follows: ![Configuration of the range node](https://flowfuse.com/blog/2023/04/images/range-config.png "Configuration of the range node") You should now see that the input values are translated to the appropriate value out of 100. :video{ariaLabel="The range note in use" autoPlay="true" height="164" loop="true" muted="true" playsInline="true" preload="none" width="534"} We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). You can also read some of our previous Node-RED tips using the links below. [Node-RED Tips - Importing, Exporting, and Grouping Flows](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/):br[Node-RED Tips - Smooth, Catch, and Maths](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-4/):br[Node-RED Tips - Exec, Filter, and Debug](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-3/):br[Node-RED Tips - Deploying, Debugging, and Delaying](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-2/):br[Node-RED Tips - Wiring Shortcuts](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-1/) # Community News April 2023 Welcome to the FlowFuse newsletter for April 2023, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. ## Upcoming events ### Node-RED Ask Me Anything Back by popular demand, FlowFuse is hosting a monthly Node-RED Ask Me Anything session on April 13th. This is a great opportunity to ask Nick O’Leary, co-creator of Node-RED & FlowFuse CTO, and Rob Marcer, Node-RED FlowFuse Developer Educator your questions about Node-RED. ### Connect, Integrate, Visual Industrial Production Metrics with Node-RED Join Steve McLaughlin from FlowFuse as he showcases how easy it is to use Node-RED to visualize popular production metrics using Node-RED. [Register today](https://flowfuse.com/webinars/2023/industrial-data-node-red/). ## From our Blog [Comparing Node-RED Dashboards Solutions](https://flowfuse.com/blog/2023/03/comparing-node-red-dashboards/) - A popular article comparing Node-RED Dashboard, uibuilder and FlexDash. [IBM Cloud removes Node-RED starter application](https://flowfuse.com/blog/2023/03/ibmcloud-starter-removed/) - IBM has discontinued their Node-RED Starter Application, discover how to migrate to FlowFuse. [The benefits and drawbacks of using Node-RED function nodes](https://flowfuse.com/blog/2023/03/why-should-you-use-node-red-function-nodes/) - Node-RED function nodes provide a great deal of flexibility in Node-RED. Discover the benefits and drawbacks of using them. [FlowFuse 15. Now Available](https://flowfuse.com/blog/2023/03/flowforge-1-5-0-released/) - FlowFuse 1.5 included updates to the UI and architecture to allow for future features. Node-RED Quick Tips - Rob Marcer, FlowFuse Developer Educator has a weekly series of Node-RED hints and tips - [Node-RED Tips - Importing, Exporting, and Grouping Flows](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/) - [Node-RED Tips - Smooth, Catch, and Math](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-4/) ## From the Community ### Quantum for Node-RED Discover how you can incorporate [quantum technologies](https://theailaboratory.wordpress.com/2023/03/24/quantum-for-everyone/){rel=""nofollow""} into your Node-RED flows. ### Image recognition within Node-RED Kazuhito Yokoi, a researcher at Hitachi, has published an [interesting article](https://kazuhitoyokoi.medium.com/sharing-node-red-flow-of-image-recognition-application-on-github-4d667cdea9f7){rel=""nofollow""} detailing how to incorporate TensorFlow into a Node-RED application to do image recognition. ### Custom Node Spotlight - node-red-contrib-web-worldmap If you would like to include a map in your next project Worldmap is a really good place to start. You can pass coordinates in to set the map to a location or you can use the built in search tool to find a location. As a user manipulates the map a stream of updated coordinates can be passed back to your flows to trigger additional actions. It's a really useful tool, take a [look here](https://flows.nodered.org/node/node-red-contrib-web-worldmap){rel=""nofollow""}. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - **[Developer Advocate - Manufacturing & Industrial Automation](https://boards.greenhouse.io/flowfuse/jobs/4798023004){rel=""nofollow""}** # FlowFuse v1.6 Now Available The new FlowFuse 1.6 adds new support for multi-instance Node-RED within a single application and support for logging from remote devices. ## FlowFuse Applications Can Now Support Multi-Instance Node-RED FlowFuse 1.6 expands the scope of applications to now allow for multiple instances of Node-RED. For complex Node-RED applications, it is common to have different flows interacting with other flows or flows deployed to different target environments. The ability to associate all these different flows with a single application makes it easier for the development, test and deployment of these types of complex applications. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: OHChdWeRI9Q --- :: ## Access Node-RED logs from remote devices FlowFuse makes it easy to deploy Node-RED out to remote devices. However, once Node-RED has been deployed to the remote device it is often difficult to troubleshoot or debug. Now with FlowFuse 1.6, you can get access to the Node-RED logs from remote devices. This makes it much easier to understand and debug the behavior of a remote device. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: yW1zxwiCmto --- :: ## Other Improvements Update email address verification [#813](https://github.com/FlowFuse/flowfuse/issues/813){rel=""nofollow""} Reminder email about trial doesn't include a link to FF Cloud [#1815](https://github.com/FlowFuse/flowfuse/issues/1815){rel=""nofollow""} Sign-up coupons improvement [#1788](https://github.com/FlowFuse/flowfuse/issues/1788){rel=""nofollow""} New FF\_Instance\_\* envvars inline with new terminology [#1844](https://github.com/FlowFuse/flowfuse/issues/1844){rel=""nofollow""} Deprecate FF\_PROJECT\_\* envvars [#1844](https://github.com/FlowFuse/flowfuse/issues/1844){rel=""nofollow""} Integrate with PostHog events [#1922](https://github.com/FlowFuse/flowfuse/pull/1922){rel=""nofollow""} Introduce search bar to docs/handbook [#620](https://github.com/FlowFuse/website/pull/620){rel=""nofollow""} ## Bug Fixes Deleting instances from the instance list fails [#1859](https://github.com/FlowFuse/flowfuse/issues/1859){rel=""nofollow""} Removing old projects with missing subscriptions fails [#1837](https://github.com/FlowFuse/flowfuse/issues/1837){rel=""nofollow""} Changing to a team as a member shows unauthorized error [#1845](https://github.com/FlowFuse/flowfuse/issues/1845){rel=""nofollow""} Application Overview: “Open Editor” shouldn’t show (or should be disabled) if in “Starting” state [#1931](https://github.com/FlowFuse/flowfuse/issues/1931){rel=""nofollow""} ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.6. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That's also a great place to send us any feedback or feature requests. You can also get help on [the Node-RED forums](https://discourse.nodered.org/){rel=""nofollow""} As well as in the [forum within our Github project](https://github.com/FlowFuse/flowfuse/discussions){rel=""nofollow""} Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""} You can raise a support ticket by emailing We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. # FlowFuse's visit to Hannover Messe 2023 "Do you use Node-RED?" This simple question became our favorite conversation opener as ZJ and I attended Hannover Messe, the world's leading industrial trade fair. To our delight, the answer was almost always "Yes." This allowed us to dive into truly fruitful discussions with Node-RED experts, exploring the latest industry trends, connecting with potential partners, and sharing our vision for the future of Node-RED in the manufacturing sector. During our visit, we had the pleasure of engaging with several vendors, including Bosch Rexroth, Siemens, Wago, Weidmüller, RevolutionPi, and more. It was genuinely inspiring to see the widespread adoption and usage of Node-RED across the entire industry. See also our [arcticle](https://flowfuse.com/blog/2023/03/integration-platform-for-edge-computing/#the-standard-for-edge-computing-and-plcs) about the adoption of Node-RED for PLCs and Gateways. FlowFuse's innovative solutions, such as remote deployment of Node-RED instances, seamless updates, effortless rollbacks, and built-in security, are already addressing many challenges faced by Node-RED users in the industry today. The positive feedback we received at Hannover Messe has bolstered our commitment to making Node-RED more accessible and production-ready for industrial and enterprise scenarios. As we forge ahead, we are eager to collaborate with more partners, learn from industry leaders, and remain at the forefront of Node-RED development. Together, we can build a more connected, efficient, and innovative manufacturing industry. With each new booth we visited and every conversation we had, it became increasingly clear that Node-RED and FlowFuse are well on their way to becoming an integral part of the industrial landscape. # Node-RED Community Health It is often a challenge to measure the health of an open source project, like Node-RED. Individuals can download and use Node-RED without any indication or feedback to their ongoing satisfaction or usage. However, it is still interesting to look at a variety of metrics to understand the size of the Node-RED community. **GitHub Stars** A popular method for demonstrating popularity of an open source project are GitHub stars. Node-RED has over 16K stars and when [compared to other low-code platforms](https://synodus.com/blog/low-code/open-source-low-code-platforms/){rel=""nofollow""} on GitHub has a strong showing. FWIW, GitHub stars are open to gaming so it is not a great long-term indicator of community health and engagement. **Node-RED Library** At the core of the Node-RED community is the library of nodes and flows that have been developed by community members. The current library has over [4300 nodes available](https://flows.nodered.org/search?type=node&sort=downloads){rel=""nofollow""}, some of the more popular nodes are downloaded more than 10K times per week. The large library of community nodes supports a wide range of protocols, data sources, data stores, and much more. This makes Node-RED more relevant and useful to many more potential users. **Node-RED Website Traffic** In the last year, Node-RED has had over 1.9 million unique visitors to the [nodered.org](https://nodered.org/){rel=""nofollow""} website, over 160K on a monthly basis. The most popular pages, after the home page, are the getting started pages. This demonstrates a strong interest in learning more about how to use Node-RED. **NPM Downloads & Docker Pull Requests** Node-RED is installed by lots of people using Docker and NPM. The project averages over 500K/month pull requests from [Docker Hub](https://hub.docker.com/r/nodered/node-red){rel=""nofollow""} and 100K/month pull requests on [NPM](https://npm-stat.com/charts.html?package=node-red&from=2017-03-22&to=2023-03-22){rel=""nofollow""}. **Node-RED Forums** The [Node-RED community forum](https://discourse.nodered.org/){rel=""nofollow""} is the go to place to get community support. It is a very popular community with over 900K page views per month. The amazing thing is that a very large percentage of questions receive replies from other community members. A great sign of a healthy community. Overall, the Node-RED community is large, healthy and engaged. # Securing Node-RED (2026) Node-RED is very easy to get up and running. Whether you run it locally, in Docker, on a Raspberry Pi, or on a service such as FlowFuse Cloud you can have a project up and running in minutes. One thing that can get overlooked is the security of Node-RED. From personal experience, the first few times I installed Node-RED I was more focussed on the possibilities of what I could do with this new tool than I was keeping my projects secure. In this article I’m going to look at some easy ways to make your Node-RED project more secure, even when first learning about it in a hobby environment. ## Protecting access from your LAN to Node-RED Once you have an instance of Node-RED running it can usually be accessed from anywhere on your LAN (local area network) by pointing a web browser to the relevant IP address and port. `http://192.168.0.3:1880` With a URL similar to the one above, depending on your specific network and Node-RED configuration, anyone on your LAN can view but more importantly edit your flows. This can be really useful when you are first learning about Node-RED but it’s always a good idea to get into the habit of locking down access to the editor, even if you trust everyone who can access your LAN. One of the easiest ways to protect your flows is to add a username and password to your Node-RED instance. :cta-image{alt="Power Workplace relies on FlowFuse for scalability, reliability and security audits - book a demo" cta="demo" src="https://flowfuse.com/images/cta/power-workplace-book-demo.png"} The first step is to find your Node-RED settings.js file. It's not always in the same place but on a default Debian Linux installation it can be found in this directory. `cd ~/.node-red` If you list that directory you should now see something like this: ![Where your settings file should show](https://flowfuse.com/blog/2023/04/images/ls.png "Where your settings.js should show") We now need to edit settings.js, I'm going to use my favourite text editor, [Nano](https://www.nano-editor.org/){rel=""nofollow""} to do that. `nano settings.js` We now need to find and edit the following section of the settings file: ![The settings file before being edited](https://flowfuse.com/blog/2023/04/images/without-password.png "The settings file before being edited") For this example, I'm going to add a password and uncomment the relevant section of the settings file, you could also change the username for additional security. To create the password we'll need to use a command line tool which is included in Node-RED. Open a second terminal then run this command: `node-red admin hash-pw` Put in your new password, I'll use the password 'flowforge' in this example. The tool returns your password in a hashed format: ![The Node-RED tool outputs the hashed password](https://flowfuse.com/blog/2023/04/images/password.png "The Node-RED tool outputs the hashed password") We can now return to the other terminal window, uncomment the section then paste in the new password, this is how it looks for me: ![The settings file with the relevant section uncommented and the password set](https://flowfuse.com/blog/2023/04/images/with-password.png "The settings file with the relevant section uncommented and the password set") We can now save and exit out of the settings file. The last step is to restart Node-RED, I'm using Debian so the command is: `node-red-restart` Now, when we try to access Node-RED I will need to provide a username and password. :video{ariaLabel="Using the username and password to login to Node-RED" autoPlay="true" height="476" loop="true" muted="true" playsInline="true" preload="none" width="708"} You might also want to consider turning off the editor interface once you are happy with your flows. This can make it a little harder to make changes to your project but it also gives you peace of mind that nobody has accidentally or deliberately changed your flows. You can turn off the editor interface as follows. Edit your settings.js file as explained above, look for the following section: ![The setting file before turning off the editor](https://flowfuse.com/blog/2023/04/images/editor-on.png "The setting file before turning off the editor") All you need to do is uncomment the bottom line then change the value from false to true, once done it should look something like this: ![The setting file after turning off the editor](https://flowfuse.com/blog/2023/04/images/editor-off.png "The setting file after turning off the editor") Now restart Node-RED as covered above, then try accessing your Node-RED instance again. You will no longer be able to edit or view your flows. Using these two features, we now have much better control over who can access the design interface for Node-RED. ## Traffic to your Node-RED instance is unencrypted Hopefully, we all know the importance of encrypting your connections between devices to stop people intercepting your traffic. This isn't a huge concern when working on your home LAN but what if you want to access your Node-RED instance from a remote location? There are two obvious options, HTTPS, and a VPN (Virtual Private Network). We could setup your Node-RED traffic to run over HTTPS, this solution ensures that all traffic to and from your Node-RED is encrypted. The downside to this approach is it's quite complex to set up. We will need to have a domain name, open up ports on our LAN's firewall, use a HTTPS certificate provider and then make sure we remember to renew the certificates as needed. It's doable if you are comfortable with those concepts (I covered how to do this as part of my blog [hosting FlowFuse on Google Cloud](https://flowfuse.com/blog/2022/12/flowforge-gcp-https-set-up/)) but there is an easier way to get started, using a VPN. A VPN provides a lot of security advantages depending on which you are using and how it is configured. To secure my traffic I'm going to use a great service call [Tailscale](https://tailscale.com/){rel=""nofollow""} which is free for personal projects. I'm going to install Tailscale on the Raspberry Pi I'm running Node-RED on as well as any other devices I want to access my project from. Once that's done I can access Node-RED from anywhere with internet access but more importantly the traffic to and from my devices is also encrypted. Before we start, it's important to remember that a VPN is only as secure as the company who runs it. You should always consider if you trust the VPN provider as they could potentially access your devices. I trust Tailscale but please do your own research before using a VPN provider. The first step we need to take is creating a Tailscale account, [you can sign up for free here](https://login.tailscale.com/start){rel=""nofollow""}. We next need to add our devices to our VPN using their software, I'm installing Tailscale on my Apple laptop, Google phone as well as the Raspberry Pi I'm running Node-RED on. The install process is really easy, even on the Pi running Raspbian the steps you need to take are well explained in the [Tailscale docs](https://tailscale.com/download/linux/debian-bullseye){rel=""nofollow""}. For the Pi, these are the commands we need to run. 1. Add Tailscale to the Apt package manager. `curl -fsSL https://pkgs.tailscale.com/stable/debian/bullseye.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null` `curl -fsSL https://pkgs.tailscale.com/stable/debian/bullseye.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list` `sudo apt-get update` 2. Install Tailscale `sudo apt-get install tailscale` 3. Start Tailscale and connect your device `sudo tailscale up` After running the last command, we need to follow the on screen prompts to link our devices to your VPN. One last thing which you might want to consider doing, every few months you will need to reconnect your devices to your VPN, if you are only going to be accessing your Node-RED device over the VPN you should consider [disabling your Tailscale key expiry](https://tailscale.com/kb/1028/key-expiry/){rel=""nofollow""}. OK, now we've got our devices all connected you should see something like this in the Tailscale dashboard. ![Tailscale dashboard showing my three devices](https://flowfuse.com/blog/2023/04/images/tailscale.png "Tailscale dashboard showing my three devices") I can now access Node-RED on my Pi from my laptop and phone by pointing a browser to the correct IP address (as shown in the image above) with the port for Node-RED: `http://100.71.28.60:1880` I’ve now secured all traffic between my devices and Node-RED project, I can access Node-RED from anywhere on the internet. ![Accessing Node-RED via the VPN](https://flowfuse.com/blog/2023/04/images/nr-via-vpn.png "Accessing Node-RED via the VPN") If you follow these steps you should be on the right path to running a more secure Node-RED instance. There is a lot more you can do and I recommend you read the [relevant docs on the Node-RED](https://nodered.org/docs/user-guide/runtime/securing-node-red){rel=""nofollow""} website to gain some more ideas. ## What about hosting Node-RED on a cloud solution such as FlowFuse? In this article, I've focussed on hosting Node-RED on a Pi on your own LAN but if you use FlowFuse Cloud to host Node-RED the solutions discussed above are either ready out of the box or are not needed. By default, the Node-RED editor is secured using your FlowFuse user credentials. You can also enable SSO to enhance account security and easily grant access to team members. With role-based access control, you can further protect your flows by managing who can view or edit them. All traffic to FlowFuse and your Node-RED instances is protected by HTTPS. FlowFuse has set up the domain name and manages the certificates, so you can spend time on your flows rather than configuring security. Additionally, remote device access is secured through encrypted tunnels, providing comprehensive protection for your deployments. FlowFuse has a [free trial](https://app.flowfuse.com/account/create){rel=""nofollow""} if you'd like to see how we've made secure hosting of Node-RED easy. ## Conclusion How ever you host Node-RED, it's a great idea to get into good security practices as early as possible to ensure that no unsecured Node-RED instances are exposed to the internet. I hope some of the tips above help you get started down the path to creating more secure Node-RED projects. # Bringing High Availability to Node-RED Many companies look to deploy Node-RED into use cases that require the application to have a high degree of availability, reliability, and scalability. Following up our [previous post on the subject](https://flowfuse.com/blog/2023/02/highly-available-node-red/), in this post I’m going to look at some of the technical details of achieving HA, the approaches available and what that means for the work we’re doing at FlowFuse and upstream in Node-RED. Everyone we speak to has a different set of requirements for this topic. To help with the discussion, I’m going to look at two ways of approaching it: - The **hot-spare approach** where you have a second instance of the application ready to take over when the primary fails. This achieves availability but doesn’t contribute to scalability. - The **load-balanced approach** where you have a second active instance of the application and work is shared between them. If either fails, the other continues running. A side-effect of this approach is a higher potential through-put and scalability; although in practice you need to ensure capacity to tolerate an instance failing. To consider which approach is most appropriate in the context of Node-RED, we need to look at the benefits and complications of each approach. It comes down to two factors; statefulness and how work is routed. ### Statefulness There are two types of state to consider when thinking about a Node-RED flow: **explicit** and **implicit** state. Explicit state is what is programmed into the flow. For example, a flow may store state in Context or use an external database service. Within FlowFuse we provide two types of context - the default in-memory context store and a database-backed persistent store. Currently the database-backed store includes a memory-caching layer to provide better performance and interoperability. That gets tricky when you want to have multiple instances sharing the same store. The context API doesn’t provide a way to atomically update values - so you can get into classic concurrency issues around two applications trying to update the same value. The other type of state is that which is implicitly maintained in a flow - even if the user hasn’t explicitly configured it. For example, the Smooth node can be used to calculate a running average value of messages passing through it. The node does that by keeping in memory the recent values so it can recalculate the average with each update. If you have multiple instances, then the node will be calculating the average for just the message its instances sees. Another example of implicit state is the Batch node that can be used to group messages into batches. Again - it will only be able to do that for the selection of messages the instance receives. It very much depends on the requirements of a flow and what nodes it uses, as to how the state can be handled. In the hot-spare approach, as only one instance is active at any time, a lot of the explicit state handling will work as expected. However the implicit state remains bound to the individual Node-RED instances. In the load-balanced approach, care has to be taken to ensure any state generated by the flow is done in a way that copes with multiple instances accessing it at the same time. A key take-away from this being that a flow has to be created with HA and/or scaling in mind. ### Routing work Node-RED makes it easy to integrate with lots of different sources of events. A couple of the most common being HTTP and MQTT. When considering how to handle multiple instances of an application we need to think about how work is routed to those instances. HTTP is the most well understood; you put a load-balancing proxy in front of the Node-RED instances and it takes care of sharing out the incoming requests. In the hot-spare scenario, the proxy needs to know which instance is active - that requires some coordination within the platform to track that properly. MQTT is commonly used with Node-RED, but unlike HTTP which is in-bound, MQTT works by having Node-RED create an out-bound connection to a broker and then subscribing to the topics of interest. In the early days of MQTT that would mean each instance would subscribe to the same set of topics and receive every message. That doesn’t really fit any HA model. With the publication of MQTTv5, the concept of Shared Subscriptions was added; the ability for a group of clients to connect, subscribe to the same topic and have the broker distribute messages between them. At this point you do get load balancing across your Node-RED instances - as long as the MQTT nodes are suitably configured. There are lots of other nodes that can be used to trigger flows, whether by listening for events on an API, connecting to locally attached hardware and many things in between. Typically, those that are more cloud-aligned, such as messaging systems like Kafka and AMQP will have very well established ways of doing load balancing. Managing out-bound connections gets more complicated in the hot-spare scenario. If we only had to deal with in-bound connections, the hot-spare instance can just sit there waiting for work to be passed its way. But once you have out-bound connections, then you have a problem. The hot-spare instance should only create its out-bound connections when it becomes the active instance. In real terms, that means the Node-RED flows should only be started when the instance becomes active. With our goal to minimize the Mean Time To Recovery (MTTR), we need to find a way to get that spare instance running as quickly as possible; if it takes just as long to start the spare instance as it does to restart the failed primary instance, then it isn’t much of an improvement. The key here is that Node-RED allows you to start the runtime without the flows running. That gets everything loaded and the runtime ready ahead of time. It can then start the flows at a moment's notice with a simple call to the runtime admin API. ### Detecting failure A key requirement of the hot-spare approach to HA is knowing when to failover to the spare. This requires close monitoring of the active instance to know whether it's still working. How quickly you can detect failure is key to reducing the time to recovery. This is where you have to think about the different ways an instance could fail - has it crashed, has it hung, has it got ‘stuck’? Detecting failure usually involves some combination of heartbeat ‘pings’ between the instances to check each is able to respond to requests. The spare instance then needs to be able to decide for itself whether it should become the active instance - and do so safely. You do not want to accidentally have two instances active at the same time. This can get quite complicated to achieve safely, but there are a number of approaches that can be used. We’ll be exploring them as we continue our journey towards HA. ### Editing Flows Within the Node-RED architecture, each instance also serves up its own editor. This is what you get when you point your web browser at it. In a HA world, once you have multiple instances running behind an HTTP load balancer, there is a tricky question of how you edit the flows. If each request hits a different instance, just loading the editor will result in different bits coming from different instances. That can typically be solved at the load balancer level by creating sticky-sessions; ensuring for a given client, each request is routed to a consistent instance. That solves part of the issue, but the next challenge is what to do when the Deploy button is pressed. That is how new flows are passed from the editor to the runtime. When you have multiple instances, we need to make sure that they all get updated. That is quite a tricky problem to solve with the current Node-RED APIs - and something we’ll be working on both in FlowFuse and in the upstream Node-RED project to resolve. That said, a more immediate solution could well be to take advantage of separate development/production instances. You develop in a single instance and, when happy with what you’ve got, roll it out to your HA-ready production instance. This bypasses the need to edit the flows in the HA environment at all. Whichever method is used, there is a question of how you minimize downtime whilst deploying an update. In a purely in-bound environment, solutions can be built where the new application is deployed alongside the old version and, when everything is ready, the in-bound events are redirected to the new version. But that isn’t feasible when you have out-bound connections to deal with as well. For some users, having a scheduled maintenance window for doing updates will be completely acceptable. As with the hot-spare approach to failover, a similar method could be used that starts new instances of Node-RED alongside the old, but with the flows all stopped. Then, once everything is ready, the old instances are stopped and the new instances started - minimizing the downtime, although not completely removing it. ### Continuing the HA journey at FlowFuse So the question is how are we going to apply all of this to what we’re building at FlowFuse. We cannot do everything at once, so we have to prioritize which scenarios we’re going to address first. Consequently, drawing from customer feedback, we have chosen to start with the scaling side of high availability - allowing multiple copies of an instance to be run with appropriate load balancing put in front of it. We are building FlowFuse as an open platform with the ability to run on top of Docker Compose and Kubernetes. As we get into some of these HA features, we will need to look carefully at where we can lean on these underlying technologies - we don’t want to reinvent the wheel here. Our initial focus is going to be when running in a Kubernetes environment - just as we do with our hosted FlowFuse Cloud platform. Kubernetes provides lots of the building blocks for creating a scalable and highly available solution, but it certainly doesn’t do all of the work for you. We've identified our initial set of tasks and changes to how we'll run Node-RED instance with the k8s environment. You can follow our progress with this [issue](https://github.com/FlowFuse/flowfuse/issues/2156){rel=""nofollow""} on our backlog. I hope this post has given some useful insight into the problems we’re looking to solve at FlowFuse. As it's such an important requirement for many users we’ll keep you updated as we make progress. # Chat GPT in Node-RED Function Nodes Recently we [posted a demo of ChatGPT integration in a Node-RED function node](https://www.linkedin.com/posts/flowforge_chatgpt-with-node-red-function-nodes-activity-7052725869684953088-2yOA?utm_source=share&utm_medium=member_desktop){rel=""nofollow""} onto our social media accounts. We have now [open-sourced](https://github.com/FlowFuse/node-red-function-gpt) this for all to play with, and **welcome any and all contributions**. ## How it Works - Prompt Engineering OpenAI make a collection of their [Generative AI models](https://platform.openai.com/docs/models){rel=""nofollow""} available via an API. We are wrapping OpenAI's [node.js module](https://www.npmjs.com/package/openai){rel=""nofollow""}, and in particular using the `openai.createChatCompletion()` functionality. For this API, you provide a chat history, and ChatGPT will respond with the next entry in that conversation. In order to "train" ChatGPT for our use case of populating Node-RED function nodes, we first tried a collection of prompts, defining specific requirements for the contents, e.g. *"Always write Javascript"*, *"Never include the wrapping function definition"*, *"Assume the input is always msg"*. It turns out though, that we were over-engineering it, we were not getting reliable results and ended up realising that ChatGPT's existing knowledge of Node-RED was sufficient such that we could use that as a prompt: Here's what we settled on: ```javascript messages: [ {role: "system", content: "always respond with content for a Node-RED function node, and don't add any commentary, always use const or let instead of var. Always return msg, unless told otherwise."}, {role: "user", content: prompt} ], ``` Here we send a `system` prompt in order to setup ChatGPT, and then follow that immediately with whatever the user has typed. From our (limited) testing, this has given us fairly reliable results. Breaking this prompt down: - ***"Always respond with content for a Node-RED function node"***: Ensured no surrounding `function () {}` definition and set expectations that the function would deal with a `msg` and likely `msg.payload` object. - ***"Don't add any commentary"***: ChatGPT likes to, well, chat. It would always return raw text justifying decisions, etc. Here, we just wanted the code. - ***"Always use const or let instead of var"***: This was Steve being picky. - ***"Always return msg, unless told otherwise"***: We found this wasn't mostly required, but occasionally it would try to return a different variable, and we'd lose context of `msg.payload`, or other data stored in `msg`. So this just made sure we had the consistency. The response from this API call is then populated into the contents of the active tab in the function node: ![Screenshot 2023-04-21 at 16 08 47](https://user-images.githubusercontent.com/99246719/233671631-fefa36c1-6db4-4392-a057-314c16fd91b7.png){width="1728"} In order to use it yourself, you will need a [valid API Key from OpenAI](https://platform.openai.com/account/api-keys){rel=""nofollow""}. ## Additional Features This was built in about a day by Steve and Joe, and we had plenty of ideas on what we'd like to add to it. We've [open-sourced](https://github.com/FlowFuse/node-red-function-gpt){rel=""nofollow""} it, and will add these as issues to the repo, but if anyone want so take a stab at contributing - that'd be most welcome! - **Insert at Cursor ([issue](https://github.com/FlowFuse/node-red-function-gpt/issues/11){rel=""nofollow""}):** Currently, the Ask GPT call will replace *all* of the content of that tab. Would be great to have the code insert wherever the cursor last was in order to add to existing code. - **Retain Conversation History ([issue](https://github.com/FlowFuse/node-red-function-gpt/issues/12){rel=""nofollow""}):** Each time a new prompt is provided by the Node-RED user, we send a fresh conversation to OpenAI, meaning that knowledge of previously asked questions are not retained. - **Client side ChatGPT Config ([issue](https://github.com/FlowFuse/node-red-function-gpt/issues/13){rel=""nofollow""}):** Currently, when you add a new "function-gpt" node you need to select the ChatGTP Config node and click "Deploy" before you can ask it a question. Our ChatGPT interaction operates server-side (to protect your API key), so Node-RED needs that in the runtime first, before a call to ChatGPT can be made. Ideally, we'd be smarter here and pass client-side creds along with the call such that we can use any changes made by the user at the time of the call. ## FlowFuse Assistant - No API Keys Required! Great news! You no longer need to manage OpenAI API keys or configure ChatGPT nodes. The [FlowFuse Assistant](https://flowfuse.com/docs/user/expert/) is now built directly into Node-RED on FlowFuse Cloud, making AI-powered development even easier. Available on FlowFuse Cloud, the Assistant offers: - **Quick Function Node Creation**: Add function nodes to your flow without dragging from the palette - **In-line Code Generation**: Generate JavaScript code for function nodes, JSON for JSON editors, and Vue.js for [FlowFuse Dashboard](https://flowfuse.com/platform/dashboard/) ui-template widgets - **Flow Explainer**: Select nodes and click "Explain Flows" to understand what they do FlowFuse Assistant helps developers work faster and smarter with Node-RED. [Start your free trial](https://app.flowfuse.com/account/create){rel=""nofollow""} to experience AI-powered Node-RED development on FlowFuse Cloud. # Community News May 2023 Welcome to the FlowFuse newsletter for May 2023, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. ## Upcoming events ### Ask Me Anything about Debugging Node-RED Our monthly Node-RED AMA session will have a special focus on debugging. Nick and Rob will lead us through some useful debug workflows to show how they approach debugging Node-RED applications. During the live coding sessions there will be opportunities for attendees to ask questions in real-time. Join us to learn from the experts on the tips and tricks for debugging Node-RED flows. ### Getting Started with Node-RED Dashboard How can you use Node-RED to create dashboards and interactive graphs of your data? The answer is the [Node-RED Dashboard](https://flowfuse.com/platform/dashboard/) node, the most popular node in the Node-RED community. In this webinar, Rob Marcer will take you through the steps of how to get started with the Node-RED Dashboard. [Register today](https://flowfuse.com/webinars/2023/getting-started-nodered-dashboard/). ## From our Blog [Chat GPT in Node-RED Function Nodes](https://flowfuse.com/blog/2023/05/chatgpt-nodered-fcn-node/) - Use Chat GPT to write Node-RED functions directly in the Node-RED interface. [Securing Node-RED](https://flowfuse.com/blog/2023/04/securing-node-red-in-production/) - A look at how you can secure Node-RED deployments. [Node-RED Community Health](https://flowfuse.com/blog/2023/04/nodered-community-health/) - Some key community metrics for the Node-RED community. [FlowFuse's visit to Hannover Messe 2023](https://flowfuse.com/blog/2023/04/hannover-messe/) - Our CEO and Product Manager visited Hannover Messe in Germany; one of the largest trade shows for manufacturing. [FlowFuse 1.6 Now Available](https://flowfuse.com/blog/2023/04/flowforge-1-6-released/) - FlowFuse 1.6 included support for multi-instance Node-RED within a single application as well as support for logging from remote devices. [Node-RED Tips - Subflows, Link Nodes, and the Range Node](https://flowfuse.com/blog/2023/04/3-quick-node-red-tips-6/) ## From the Community Jsonata is a very useful and often underutilised tool built into Node-RED. Steve over at [Steve's Node-RED Guide](https://stevesnoderedguide.com){rel=""nofollow""} has published a great beginners guide. If you are new to Jsonata and want to learn more we recommend you [take a look](https://stevesnoderedguide.com/node-red-and-jsonata-for-beginners){rel=""nofollow""}. ### Custom Node Spotlight - node-red-contrib-queue-gate [Queue Gate](https://flows.nodered.org/node/node-red-contrib-queue-gate){rel=""nofollow""} is a handy custom node which allows you to control the flow of messages. You might wish to queue up all the messages in a flow and then release them all, once an hour. Maybe you want to release just one message at a time and wait until the prior message completed a section of your flow. Queue Gate makes message queuing really easy without the need to use an external queue solution. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - **[Developer Advocate - Manufacturing & Industrial Automation](https://boards.greenhouse.io/flowfuse/jobs/4798023004){rel=""nofollow""}** - **[Sales Representative](https://boards.greenhouse.io/flowfuse/jobs/4843566004){rel=""nofollow""}** # Running the FlowFuse Device Agent as a service on a Raspberry Pi FlowFuse's device agent allows you to manage and run your Node-RED instances on your own hardware such as a Raspberry Pi. This can be very useful where an application you've written needs to run flows with direct access to hardware sensors. In this article I'm going to explain the steps to configure our device agent to run as a service in Raspbian OS, or any other OS that uses systemd. ## Why run the device agent as a service? The standard process for running FlowFuse's device agent is to start it on the command line using the command `flowforge-device-agent`. This works fine for testing but for long-term installations it's useful to run the device agent as a service. Once running as a service, the device agent will continue to run even if your device is restarted or your SSH connection to your Pi fails. ## Set up steps ### Create the Service File The first step is creating the systemd unit file for your service. You can start by creating a new file in the `/etc/systemd/system` directory with a .service file extension: `sudo nano /etc/systemd/system/flowforge-device-agent.service` ### Define the Service In the service file, you'll need to define the following parameters: - `Description`: A brief description of what the service does. - `ExecStart`: The command(s) to execute to start the service. - `User and Group`: The user and group that the service runs as. - `Type`: Whether the service is a simple or a forking type. We've created the content you'll need for this file and shared it via [this GitHub page](https://github.com/FlowFuse/device-agent/blob/main/service/flowfuse-device.service){rel=""nofollow""}. Copy the code from that page into the nano window you created in step 1, then save and exit out of nano. ### Starting the service on boot (optional) If you want Node-RED to run when the Pi is turned on, or re-booted, you can enable the service to autostart by running the command: `sudo systemctl enable flowforge-device-agent.service` To disable the service, run the command: `sudo systemctl disable flowforge-device-agent.service` ### Using your new service You can now start your service with the start command: `sudo systemctl start flowforge-device-agent` You can check the current status with the status command: `sudo systemctl status flowforge-device-agent` Finally, if you need to stop your agent you can do so with the command: `sudo systemctl stop flowforge-device-agent` ## Further reading If you'd like to learn about using services via the systemctl command you can access the help text by running `systemctl -h` from your Pi terminal. # FlowFuse 1.7 Now Available with Remote Node-RED Editor Access FlowFuse 1.7 adds new support for accessing the Node-RED Editor on Devices via FlowFuse. ## Further improving fleet management and maintenance of remote Node-RED instances We are excited to introduce a new feature that will simplify the process of debugging and developing flows for devices. Our latest feature, "Editing Flows on Devices" allows users to access the editor directly on their device without the need for complex network configurations or firewalls. This feature will significantly improve the user experience, making it easier and more efficient to work with devices. This update is a part of our ongoing commitment to making FlowFuse the best possible solution for developing your Node-RED flows, no matter where they're running. In fact, as part of our last release 1.6, we already introduced the feature: ["Access Node-RED logs from remote devices"](https://flowfuse.com/blog/2023/04/flowforge-1-6-released/#access-node-red-logs-from-remote-devices). This feature made it easy for users to troubleshoot and debug. Building on that, we've taken the next step, and it's now possible to access the Device Editor. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: zS6P3RR86vE --- :: ## Device Status Visualisation This FlowFuse version upgrades device monitoring. It's made easier to manage your devices effectively, especially when there's many of them. Now we offer an intuitive, user-friendly method for users to keep an eye on their devices' status and evaluate the health of their team's devices overall. Creating an overview of your fleet's health, however large your fleet might be. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: S--viuPhrS8 --- :: ## Auto Restart for Hung Node-RED Instances This enhancement ensures a more robust and reliable experience when working with Node-RED flows. The launcher now actively monitors the Node-RED process to detect if it has become unresponsive or hung, in addition to the existing checks for start-up and unexpected process exits. This advancement takes us one step further in improving the availability of our Node-RED instances. ## Ongoing Topics ### High Availability We're actively progressing on the topic of enhancing High Availability in FlowFuse for Node-RED. Our initial tasks and modifications have been identified, specifically pertaining to the operation of Node-RED instances within the k8s environment. These adjustments are aimed at constructing a more resilient system. Stay tuned for a comprehensive update regarding our advancements in High Availability. ### SOC2 Certification Dedicated to upholding the highest levels of security and privacy, our company acknowledges the significance of industry-standard certifications like SOC2 in fostering trust with our customers and partners. We aim to achieve SOC2 Type 1 certification by the end of Q2 and subsequently maintain a continuous SOC2 Type 2 certification. We will keep you informed on our progress as we reach essential milestones in our SOC2 certification journey. Rest assured, our commitment to delivering the utmost security and privacy for our customers and partners remains unwavering. ### AWS Marketplace onboarding We are excited to provide an update on our ongoing task of onboarding Node-RED instances to AWS Marketplace via FlowFuse Cloud, which we have started in this Iteration. By offering Node-RED instances through AWS Marketplace, we aim to simplify the deployment process for our customers. One of the significant challenges we are currently addressing is handling our current payment system in parallel with a new method. This will ensure a seamless billing experience for our customers, as they will be able to manage their Node-RED instance subscriptions through their existing AWS accounts. ## Contributors We'd like the thank the following for their contributions to this release: - [@andreikop](https://github.com/andreikop){rel=""nofollow""} for their work on the [flowforge-driver-k8s #80](https://github.com/FlowFuse/flowforge-driver-k8s/pull/80){rel=""nofollow""} and [flowforge/helm #125](https://github.com/FlowFuse/helm/pull/125){rel=""nofollow""} - [@elenaviter](https://github.com/elenaviter){rel=""nofollow""} for their work on [flowforge/helm #126](https://github.com/FlowFuse/helm/pull/126){rel=""nofollow""} As an open-source project, we welcome community involvement in what we're building. If you're interested in contributing, checkout our [guide in the docs](https://flowfuse.com/docs/contribute/). ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our \[Product Roadmap Page/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Bug Fixes Incorrect number of days displayed when adding a new license [#1895](https://github.com/FlowFuse/flowfuse/issues/1895){rel=""nofollow""} Users were unable to upgrade modules in Manage Palette, even after restarting Node-RED. [#2005](https://github.com/FlowFuse/flowfuse/issues/2005){rel=""nofollow""} Enable 'Delete Team' Button [#2031](https://github.com/FlowFuse/flowfuse/issues/2031){rel=""nofollow""} Triggering a Node-RED restart using the action button resulted in two instances of Node-RED running in the container, causing one instance to crash due to port 1880 being already in use. [#2031](https://github.com/FlowFuse/flowfuse/issues/1860){rel=""nofollow""} Error deleting instance with missing subscription [#2080](https://github.com/FlowFuse/flowfuse/issues/2080){rel=""nofollow""} Snapshot Rollback no longer working [#2026](https://github.com/FlowFuse/flowfuse/issues/2026){rel=""nofollow""} Users receiving an unauthorized error when attempting to switch to a team in which they are a member [#1845](https://github.com/FlowFuse/flowfuse/issues/1845){rel=""nofollow""} Cannot select "Member" option when inviting a team member [#2084](https://github.com/FlowFuse/flowfuse/issues/2084){rel=""nofollow""} ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.7. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. If you hit any problems with the platform please raise an [issue on GitHub](https://github.com/FlowFuse/flowfuse/issues){rel=""nofollow""}. That's also a great place to send us any feedback or feature requests. You can also get help on [the Node-RED forums](https://discourse.nodered.org/){rel=""nofollow""} As well as in the [forum within our Github project](https://github.com/FlowFuse/flowfuse/discussions){rel=""nofollow""} Chat with us on the `#flowfuse` channel on the [Node-RED Slack workspace](https://nodered.org/slack){rel=""nofollow""} You can raise a support ticket by emailing We've also added a live chat widget to our website, you can access it using the icon on the bottom right corner of our website. We'd love to hear from you. # Best Practices Integrating a Modbus Device With Node-RED (2026) The world of industrial automation is slow to adopt new technology. With legacy equipment already working and in place, paralyzing down-time costs, and fears of introducing instability into a plant, technology change has a cautious pace. Node-RED provides a way to extend the capabilities of the simpler, proven technology, allowing connections with modern systems. However, the same fundamentals that have made industrial equipment dependable, must be incorporated into your Node-RED architecture. And, inversely, the same abstraction and simplicity that makes a low-code, Node-RED environment fast, and easy to work with, can also make it difficult to interface with a lower-level system. Modbus is a widely adopted protocol for accessing data from existing legacy manufacturing equipment. Node-RED makes it very easy to connect to Modbus enabled equipment. However, there are some best practices we have developed to maintain system integrity when integrating Modbus devices with Node-RED: :cta-image{alt="Aperia Technologies stopped reprogramming controllers station by station with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/aperia-book-demo.png"} ### Add Watchdogs to Node-RED Flows To keep your automation system running with peace of mind and little human intervention, use a watchdog timer. A watchdog timer is typically used to detect and recover from system malfunctions. In Node-RED a watchdog timer can be tied to a broadcast messaging system to get push alerts directly to cell phones, as well as to an auto-reset to try to get your Flows back online automatically. A simple implementation of this is to just put two Trigger nodes in a loop, the first one to send an alert when it hasn’t seen any recent events, and the second to to reset the first one. ![Example watchdog flow](https://flowfuse.com/blog/2023/05/images/integrating-modbus-3.png "Example watchdog flow") Each of the trigger nodes are setup with similar parameters, only the delay values differ. ![Trigger node configuration](https://flowfuse.com/blog/2023/05/images/integrating-modbus-14.png "Trigger node configuration") Sending a payload of {"connectorType":"TCP"} to the Modbus Flex Connector is enough to reset the connection without needing to send all the other parameters. ![Change node configuration](https://flowfuse.com/blog/2023/05/images/integrating-modbus-10.png "Change node configuration") ### Choosing a Safe Poll Rate Be careful when adding new traffic to an industrial network, which might not be able to handle the extra load. Some networks have low-bandwidth, especially at large plants that might be using antiquated IP devices and long-distance, Wi-Fi bridges in electrically noisy environments. Furthermore, some PLCs and other Modbus devices have limits as to how many other devices can connect to them, and a new connection might not work, or worse, bump off an old connection. A PLC’s general operation is to poll its inputs every cycle and react accordingly, running logic and triggering outputs as quickly as possible. The general use-case for Node-RED with a PLC is to create an HMI or broader, SCADA system. A poll rate of every second suffices for a simple HMI. For dashboards published to a greater audience, rather than just the operator, poll rates of several minutes to hours might be adequate. If this is a new integration, it’s better to start slow and make sure the current infrastructure can handle the extra load. ![Modbus node configuration](https://flowfuse.com/blog/2023/05/images/integrating-modbus-7.png "Modbus node configuration") ### Coil/Register Grouping Modbus works more efficiently when it is reading and writing addresses as groups. Creating banks of consecutively numbered coils or registers can help with this. If Modbus is already in use, perhaps for an existing HMI, but for your Node-RED dashboard you just want a selection of these addresses and they are too scattered to be read all at once, pick a starting address numbered far higher than what you will use for any HMI work and create a new bank of coils or registers just for the Node-RED dashboard. The PLC can handle this easily and it greatly simplifies the polling for your Node-RED dashboard. On the Node-RED side of the connection, it’s a good idea to parse and to filter this data as soon as it enters the flow. Below, the first node creates an appropriate message for each coil, with a topic name and a true/false payload. This message is then filtered by the “block unless value changes” mode in the filter node and finally a switch (by topic) node separates out each message. In this example, every Tag coming in from the PLC only triggers downstream nodes when there is a change and each Tag has its own output to connect a wire, and subsequent nodes to. ![Using a switch node to separate the messages](https://flowfuse.com/blog/2023/05/images/integrating-modbus-5.png "Using a switch node to separate the messages") The filter node blocks redundant data from triggering subsequent nodes. ![Filter node configuration](https://flowfuse.com/blog/2023/05/images/integrating-modbus-12.png "Filter node configuration") The function node, “modbusMapArray,” creates messages that are much more user-friendly in the Node-RED environment. ![Modbus function node](https://flowfuse.com/blog/2023/05/images/integrating-modbus-1.png "Modbus function node") Quick tip: If your PLC environment has limits on the number of addresses allowed and you want to read a lot of coils, you can work with registers bitwise and stuff 16 coils into one register. ### Reading Data Types No matter what data type the PLC is sending over Modbus, it’s going to be sent using the 16-bit registers. For example, to send 1234.5678 as a 32-bit Float (little-endian), the payload from the PLC will be a seemingly unhelpful array, [21035,17562]. I can simulate this with the [Productivity Suite Programming Software](https://www.automationdirect.com/adc/overview/catalog/software_products/programmable_controller_software/productivity_suite_programming_software){rel=""nofollow""} from Automation Direct set to simulator mode. Below, I have created a 32-bit float “Tag,” named “mySampleFloat32,” using modbus registers 40001 and 40002, and set the “Init Value” to 1234.5678. ![Productivity Suite Programming Software](https://flowfuse.com/blog/2023/05/images/integrating-modbus-11.png "Productivity Suite Programming Software") Node-RED is typically used at a much higher level, but luckily there is still a way to work with this low-level data. Node-RED uses the Buffer Class to work with this type of data stream, but it’s a little tricky. First the 16-bit registers have to be broken into 8-bit chunks, here we use msg.responseBuffer.buffer to retrieve each octet. Once our buffer is properly filled, there are many built-in functions to reconstruct the data into the actual number you are looking for. ![Using msg.responseBuffer.buffer to retrieve each octet](https://flowfuse.com/blog/2023/05/images/integrating-modbus-4.png "Using msg.responseBuffer.buffer to retrieve each octet") Notice below that to read these 2 registers at 400001 and 400002 I have set my Modbus-Read node to start at “Address” 0 and ready “Quantity” 2 registers. Unfortunately, there are two different standards for writing Modbus addresses and my PLC uses the traditional convention (400001 to 465536) and this Modbus node uses the hexadecimal convention (4x0000 to 4xFFFF). ![Modbus node configuration](https://flowfuse.com/blog/2023/05/images/integrating-modbus-8.png "Modbus node configuration") In this example the byte order from msg.responseBuffer.buffer doesn’t quite match the data type, so we have to rebuild the buffer. ![Mapping the data in a function node](https://flowfuse.com/blog/2023/05/images/integrating-modbus-13.png "Mapping the data in a function node") Amazingly after all that work we get the response: 1234.5677490234375. This is the shortcoming of the 32-bit float data type. Although it can handle a huge range of values, they aren’t very accurate. For this reason, many times PLCs will send a number as an integer, say 12345678, and the documentation will prescribe 4 decimal place accuracy to bring the number back to 1234.5678. Find out more ways to work with a Buffer at {rel=""nofollow""}. ### General Architecture Although you can off-load some of the higher-level logic from the PLC into Node-RED, it’s important to remember that Node-RED augments, but doesn’t create a replacement for a PLC’s IDE and the IEC 61131-3 suite of languages. Make a conscious distinction between the type of work the PLC should handle and what you expect from Node-RED. Any real-time responses to inputs should strictly be handled by the PLC. ### Security Connecting Node-RED to your PLC also creates a larger attack surface for cyber threats. Make sure that you follow the guidelines found on the Node-RED.org site at [Securing Node-RED](https://nodered.org/docs/user-guide/runtime/securing-node-red){rel=""nofollow""}. Node-RED’s strength is its ability to make connections where they weren’t possible before, but this can be taken advantage of by a hacker. For instance, maybe it’s tempting to make Node-RED a transparent gateway and make a RESTful API fully exposing a modbus-flex-write node. This is amazingly easy and powerful with Node-RED, but anyone who can access your IP could send `http://:1880/careful?value=true&fc=15&unitid=1&address=0&quantity=10` and remotely turn on and off whatever they wanted. ![Example endpoint flow](https://flowfuse.com/blog/2023/05/images/integrating-modbus-6.png "Example endpoint flow") Instead, a better practice would be to more narrowly define what you want to accomplish and only allow Node-RED to do exactly that. In this case you might send `http://:1880/honkTheLunchHorn?honk=true` ![Locking down the endpoint](https://flowfuse.com/blog/2023/05/images/integrating-modbus-2.png "Locking down the endpoint") ### Final Thoughts If not done right, there could be some hard lessons, so it’s best to monitor the processes to help track down bugs. Add a log, keep your eyes out, and as a community let’s work to create stable systems. [Opto 22 customers rely on this same Node-RED-to-Modbus pattern in production](https://flowfuse.com/customer-stories/opto22-embraces-node-red/), transforming Modbus registry data from groov EPIC controllers into human-readable, contextual data for other applications. ![Logging failures](https://flowfuse.com/blog/2023/05/images/integrating-modbus-9.png "Logging failures") How readily upper management gives an “okay” to this new technology comes with how well it is implemented. There will be some growing pains, but by the end, you will have supercharged your plant, bringing it into the 21st century. # Node-RED Community Survey Results The Node-RED community recently published the results of their [2023 Community Survey](https://nodered.org/about/community/survey/2023/){rel=""nofollow""}, building upon their [2019 survey](https://nodered.org/about/community/survey/2019/){rel=""nofollow""}. The findings reveal some interesting trends within the Node-RED community that are worth highlighting. 1. **Passionate and Experienced Community**. The Node-RED community has shown a remarkable increase in experience. In 2019, only 28.3% of users had been utilizing Node-RED for over two years. However, in 2023, this number has grown to an impressive 65.2%. This indicates that Node-RED has become a go-to tool for many individuals, demonstrating their continued loyalty to the platform. Moreover, the community highly regards Node-RED, with 94% of respondents rating it as a 4 or 5 on a scale of 1 to 5. :br Developer tools can fall out of fashion but it is clear Node-RED is providing value and is being used by developers. 2. **Increasing Adoption in Industrial and Manufacturing Automation**. The survey also revealed a significant increase in Node-RED usage within the manufacturing and industrial automation industries. Several data points support this finding, including a rise from 31.5% to 40.3% in respondents identifying themselves as working in the manufacturing industry. Additionally, there has been an increase in the use of Node-RED applications for Industrial IoT/PLC devices, which grew from 24% to 35.8% in 2023. Furthermore, the adoption of popular manufacturing industry protocols like [OPC-UA](https://opcfoundation.org/){rel=""nofollow""} and [Modbus](https://modbus.org/){rel=""nofollow""} has also increased, with OPC-UA usage rising from 9.3% to 16.7% and Modbus usage increasing from 15.8% to 27.6%. 3. **InfluxDB dominance in the Node-RED community**.[ InfluxDB](https://www.influxdata.com/){rel=""nofollow""} has emerged as the leading database within the Node-RED community. Its usage has grown significantly, from 24.2% in 2019 to 43.9% in 2023. On the other hand, MySQL, the next most popular database, experienced a slight decrease in usage, dropping from 32.4% to 31.4%. :br It is evident that the MING stack (MQTT/Mosquitto, InfluxDB, Node-RED, and Grafana) is gaining momentum and becoming a preferred choice for developers. 4. **Limitations to Node-RED Adoption**. This year's survey also explored factors that might limit the adoption of Node-RED. Approximately 27.9% of respondents stated that they perceived no additional need for Node-RED. However, the next most common responses were related to the perception that Node-RED is only suitable for proof-of-concept (POC) projects (19.9%), the lack of specific Node-RED features (13.3%), and the absence of professional support (10.1%). :br It is important for the Node-RED community to demonstrate the platform's usage in production environments. Although changing the perception of Node-RED as solely a POC tool may take time, FlowFuse is committed to helping shift this perspective. Moreover, FlowFuse aims to address other identified issues by developing the FlowFuse platform, which will provide the necessary features to create reliable, secure, and dependable Node-RED applications. FlowFuse will also offer professional support to all its customers, ensuring that users have the assistance they need. Thank you to everyone that completed the Node-RED Survey. The insights from the survey will help the community to build a better Node-RED. Check out the detailed results on the [nodered.org website](https://nodered.org/about/community/survey/2023/){rel=""nofollow""}. # Persisting chart data in Node-RED Dashboard 1 Node-RED makes it easy to create HMI (Human Machine Interfaces) using [Node-RED Dashboard](https://flows.nodered.org/node/node-red-dashboard){rel=""nofollow""}. One of the most useful features of Dashboard 1 is the ability to store historic data passed to a chart within the chart node itself. This makes your flows far simpler than would be the case if you needed to send the entire data set to the chart for each update. ::div{.blog-update-notes} **UPDATE:** Since this article was published, Node-RED Dashboard (1.0) has been [deprecated](https://discourse.nodered.org/t/announcement-node-red-dashboard-v1-deprecation-notice/89006). Instead, it is recommended to use [FlowFuse Dashboard (Dashboard 2.0)](https://flowfuse.com/platform/dashboard/) which is a more modern and feature-rich dashboard solution for Node-RED. :: ### The Importance of Persisting Chart Data Storing the data in the chart node is fine to show prototypes of HMIs, but where it's vital the correct data is always shown we are going to need a backup. Data can easily be lost when you move your flow to a new device, restart your instance, or simply when upgrading Node-RED. How can we store our chart data so we can be confident it will be there each time a user views your HMI? ### Example Dashboard In this example, we are passing in a random number between one and 10 each second. With each new value received the chart updates and as mentioned about, the values are also stored in the chart node. If you'd like to see and edit the flows I've created, you can copy and paste the JSON below into your Node-RED import feature. ::render-flow ```json [{"id":"c6825b1001216b89","type":"inject","z":"668c56888fd0f960","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":220,"wires":[["6b609d978540fb2a"]]},{"id":"6b609d978540fb2a","type":"Number","z":"668c56888fd0f960","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":270,"y":220,"wires":[["794846db6dc8cef8"]]},{"id":"794846db6dc8cef8","type":"ui_chart","z":"668c56888fd0f960","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":430,"y":220,"wires":[["ad53848ee4b0d91e"]]},{"id":"ad53848ee4b0d91e","type":"debug","z":"668c56888fd0f960","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":550,"y":220,"wires":[]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: x ### How can we store and recall the chart data? The chart node has a really useful feature which allows us to access all the data currently shown in the chart. Each time the chart receives new data, it's added to the existing values then the whole data set is sent out the outbound port of the chart node. Now that we have a way to easily access the chart data in a single payload, we next need to store that data somewhere safer. I'm going to explain 3 potential solutions, which I use on a regular basis. #### 1. Node-RED file-out and file-in nodes Node-RED can read and write data to a local filesystem. Being that we already have the chart data in a single payload, we just need to write that payload to a file for later use, which we can do using the file-out node. This example flow shows how to use the file-out node to write the chart data to your local filesystem. ::render-flow ```json [{"id":"ead9df683d29fb8a","type":"inject","z":"668c56888fd0f960","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":560,"wires":[["ef5359b8bd3f78b3"]]},{"id":"ef5359b8bd3f78b3","type":"Number","z":"668c56888fd0f960","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":270,"y":560,"wires":[["69ad440cd8d1ce30"]]},{"id":"69ad440cd8d1ce30","type":"ui_chart","z":"668c56888fd0f960","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":430,"y":560,"wires":[["e4e7758028477505","d9d6a2e34767f568"]]},{"id":"e4e7758028477505","type":"debug","z":"668c56888fd0f960","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":550,"y":560,"wires":[]},{"id":"d9d6a2e34767f568","type":"json","z":"668c56888fd0f960","name":"","property":"payload","action":"","pretty":false,"x":550,"y":600,"wires":[["b5b020fb17f615df"]]},{"id":"b5b020fb17f615df","type":"file","z":"668c56888fd0f960","name":"","filename":"example.json","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":690,"y":600,"wires":[["a6d9eab41d4dcf97"]]},{"id":"a6d9eab41d4dcf97","type":"debug","z":"668c56888fd0f960","name":"debug 92","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":840,"y":600,"wires":[]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: As the chart node sends the full data set each time new data is added, we overwrite the content of the file rather than append the new values. The next step is to pull the data back from the filesystem to your Node-RED instance. Node-RED makes this very easy using the file-in node. ::render-flow ```json [{"id":"ead9df683d29fb8a","type":"inject","z":"668c56888fd0f960","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":560,"wires":[["ef5359b8bd3f78b3"]]},{"id":"ef5359b8bd3f78b3","type":"Number","z":"668c56888fd0f960","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":270,"y":560,"wires":[["69ad440cd8d1ce30"]]},{"id":"69ad440cd8d1ce30","type":"ui_chart","z":"668c56888fd0f960","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":430,"y":560,"wires":[["e4e7758028477505","d9d6a2e34767f568"]]},{"id":"e4e7758028477505","type":"debug","z":"668c56888fd0f960","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":550,"y":560,"wires":[]},{"id":"d9d6a2e34767f568","type":"json","z":"668c56888fd0f960","name":"","property":"payload","action":"","pretty":false,"x":550,"y":600,"wires":[["b5b020fb17f615df"]]},{"id":"b5b020fb17f615df","type":"file","z":"668c56888fd0f960","name":"","filename":"example.json","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":690,"y":600,"wires":[["a6d9eab41d4dcf97"]]},{"id":"a6d9eab41d4dcf97","type":"debug","z":"668c56888fd0f960","name":"debug 92","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":840,"y":600,"wires":[]},{"id":"e9cb9350f1aaeb38","type":"inject","z":"668c56888fd0f960","name":"import data","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":660,"wires":[["600f014947f73d8f"]]},{"id":"600f014947f73d8f","type":"file in","z":"668c56888fd0f960","name":"","filename":"example.json","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":270,"y":660,"wires":[["972118c0e114f47b","69ad440cd8d1ce30"]]},{"id":"972118c0e114f47b","type":"debug","z":"668c56888fd0f960","name":"debug 93","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":660,"wires":[]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: When you press the 'import data' trigger node, the data is loaded in from the filesystem and shown in the chart. You may want to automate that task to run each you deploy your Node-RED instance. ![Import data on deploy](https://flowfuse.com/blog/2023/05/images/inject-on-deploy.png "Import data on deploy") Bear in mind that your data is stored in your filesystem, if your storage drive fails you will lose your data, you might want to consider taking backups and storing elsewhere for emergencies. #### 2. FlowFuse's persistent context FlowFuse Cloud and premium self hosted version provides persistent context storage as part of its Node-RED instances. This allows you to create, read, update, and delete data as needed, even if you have restarted a Node-RED instance. This flow shows chart data being sent to persistent context so we can access it later. The process is very similar to using the file-out and file-in nodes. ::render-flow ```json [{"id":"c6825b1001216b89","type":"inject","z":"4767c2f7095bee53","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":100,"wires":[["6b609d978540fb2a"]]},{"id":"794846db6dc8cef8","type":"ui_chart","z":"4767c2f7095bee53","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":490,"y":100,"wires":[["ad53848ee4b0d91e","938c7d878545e623"]]},{"id":"ad53848ee4b0d91e","type":"debug","z":"4767c2f7095bee53","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":100,"wires":[]},{"id":"6b609d978540fb2a","type":"Number","z":"4767c2f7095bee53","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":330,"y":100,"wires":[["794846db6dc8cef8"]]},{"id":"938c7d878545e623","type":"change","z":"4767c2f7095bee53","name":"","rules":[{"t":"set","p":"#:(persistent)::chart-data","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":140,"wires":[["3792cc96a748e75a"]]},{"id":"3792cc96a748e75a","type":"debug","z":"4767c2f7095bee53","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":840,"y":140,"wires":[]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: We now need to have a method to load the data back into our chart. We will again use a manual 'import data' trigger to load the full set of data from the persistent context, and then push it back into the chart. ::render-flow ```json [{"id":"c6825b1001216b89","type":"inject","z":"4767c2f7095bee53","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":100,"wires":[["6b609d978540fb2a"]]},{"id":"794846db6dc8cef8","type":"ui_chart","z":"4767c2f7095bee53","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":490,"y":100,"wires":[["ad53848ee4b0d91e","938c7d878545e623"]]},{"id":"ad53848ee4b0d91e","type":"debug","z":"4767c2f7095bee53","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":100,"wires":[]},{"id":"6b609d978540fb2a","type":"Number","z":"4767c2f7095bee53","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":330,"y":100,"wires":[["794846db6dc8cef8"]]},{"id":"938c7d878545e623","type":"change","z":"4767c2f7095bee53","name":"","rules":[{"t":"set","p":"#:(persistent)::chart-data","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":140,"wires":[["3792cc96a748e75a"]]},{"id":"3792cc96a748e75a","type":"debug","z":"4767c2f7095bee53","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":840,"y":140,"wires":[]},{"id":"3379276c77b4691c","type":"inject","z":"4767c2f7095bee53","name":"import data","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":150,"y":180,"wires":[["ddd1ef41321fb4a6"]]},{"id":"ddd1ef41321fb4a6","type":"change","z":"4767c2f7095bee53","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"#:(persistent)::chart-data","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":180,"wires":[["794846db6dc8cef8","fa65b958e34bc971"]]},{"id":"fa65b958e34bc971","type":"debug","z":"4767c2f7095bee53","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":480,"y":180,"wires":[]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: You may have noticed that when we are pushing duplicate data into the chart it automatically checks to see if the data is already stored. If the data points are already in the chart the new data is disregard. This saves us writing an extra section of the flow to delete the data before we load it in. #### 3. Expose the data via an API then manually import it In some cases you may want to copy your chart data to somewhere outside of your Node-RED instances. You can do this by creating a simple API which allows an outside system to request the chart data. You can also manually go to the URL of the API in a web browser to get your data. This example flow allows a user or system to access the chart data via a URL. ::render-flow ```json [{"id":"6523e86042252710","type":"inject","z":"4767c2f7095bee53","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":150,"y":540,"wires":[["d51aba5f9a808592"]]},{"id":"427c60f2c4f523b7","type":"ui_chart","z":"4767c2f7095bee53","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":470,"y":540,"wires":[["ba84cd1820120139","d9087436f4769691"]]},{"id":"ba84cd1820120139","type":"debug","z":"4767c2f7095bee53","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":540,"wires":[]},{"id":"d51aba5f9a808592","type":"Number","z":"4767c2f7095bee53","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":310,"y":540,"wires":[["427c60f2c4f523b7"]]},{"id":"d9087436f4769691","type":"change","z":"4767c2f7095bee53","name":"","rules":[{"t":"set","p":"chart-data","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":580,"wires":[[]]},{"id":"4dd97b29430ad2ba","type":"http in","z":"4767c2f7095bee53","name":"","url":"/data","method":"get","upload":false,"swaggerDoc":"","x":200,"y":640,"wires":[["b4f0eb085cc91834"]]},{"id":"7ca49e5465699355","type":"http response","z":"4767c2f7095bee53","name":"","statusCode":"","headers":{},"x":670,"y":640,"wires":[]},{"id":"b4f0eb085cc91834","type":"change","z":"4767c2f7095bee53","name":"Get the chart data from flow.chart-data","rules":[{"t":"set","p":"payload","pt":"msg","to":"chart-data","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":640,"wires":[["7ca49e5465699355"]]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: We can now access the data by simply visiting the URL of the API. ![The chart data accessed via the API in a web browser](https://flowfuse.com/blog/2023/05/images/data-in-browser.png "The chart data accessed via the API in a web browser") Bear in mind that you should secure the API as appropriate for the data. If you don't put security around the API, anyone on the same network as your Node-RED instance can access your chart data. Potentially that could give access to our data to the whole internet, so where needed take steps to keep your data safe. You can read more about securing Node-RED in our [blog post here](https://flowfuse.com/blog/2023/04/securing-node-red-in-production/). We now need to get that data back into a Node-RED instance. We can do that by editing a node and pasting in the data we got from the API. The flow below shows where you can paste in your data, you will then need to deploy and manually trigger 'import data'. ![Inject the data in JSON format](https://flowfuse.com/blog/2023/05/images/inject-the-json.png "Inject the data in JSON format") ::render-flow ```json [{"id":"6523e86042252710","type":"inject","z":"4767c2f7095bee53","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":150,"y":540,"wires":[["d51aba5f9a808592"]]},{"id":"427c60f2c4f523b7","type":"ui_chart","z":"4767c2f7095bee53","name":"","group":"af1535b39b74f94a","order":0,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":470,"y":540,"wires":[["ba84cd1820120139","d9087436f4769691"]]},{"id":"ba84cd1820120139","type":"debug","z":"4767c2f7095bee53","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":540,"wires":[]},{"id":"d51aba5f9a808592","type":"Number","z":"4767c2f7095bee53","name":"Random Number","minimum":"1","maximum":"10","roundTo":"0","Floor":true,"x":310,"y":540,"wires":[["427c60f2c4f523b7"]]},{"id":"d9087436f4769691","type":"change","z":"4767c2f7095bee53","name":"","rules":[{"t":"set","p":"chart-data","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":580,"wires":[[]]},{"id":"4dd97b29430ad2ba","type":"http in","z":"4767c2f7095bee53","name":"","url":"/data","method":"get","upload":false,"swaggerDoc":"","x":200,"y":660,"wires":[["b4f0eb085cc91834"]]},{"id":"7ca49e5465699355","type":"http response","z":"4767c2f7095bee53","name":"","statusCode":"","headers":{},"x":670,"y":660,"wires":[]},{"id":"b4f0eb085cc91834","type":"change","z":"4767c2f7095bee53","name":"Get the chart data from flow.chart-data","rules":[{"t":"set","p":"payload","pt":"msg","to":"chart-data","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":660,"wires":[["7ca49e5465699355"]]},{"id":"d13d16b33ec638b2","type":"inject","z":"4767c2f7095bee53","name":"import data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"[{\"series\":[\"\"],\"data\":[[{\"x\":1684841975036,\"y\":8},{\"x\":1684841976037,\"y\":6},{\"x\":1684841977038,\"y\":7},{\"x\":1684841978037,\"y\":7}]],\"labels\":[\"\"]}]","payloadType":"json","x":170,"y":600,"wires":[["427c60f2c4f523b7"]]},{"id":"af1535b39b74f94a","type":"ui_group","name":"Example","tab":"14f1442eb7525190","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"14f1442eb7525190","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}] ``` :: Each of these solutions has strengths and weaknesses but there are many other ways to persist your chart data. You should consider which approach is the best fit for your needs. To be as confident as possible that you data is safe, you may decide to push your data to dedicate external storage such as a database or backup solution. ### Conclusion Node-RED Dashboard 1 allows you to easily make informative HMIs, but it's important to make sure the chart data you are showing is stored safely. The approaches we have discussed above should give you a good start in ensuring your charts are populated with the correct data, even if your Node-RED instance crashes or you need to move it to a new hosting location. # Node-RED Tips - Dashboard Edition There is usually more than one way to complete a given task in software, and Node-RED is no exception. In each of this series of blog posts, we are going to share three useful tips to save yourself time when working on your flows. In this Node-RED Tips article, we are going to focus on [Node-RED Dashboard](https://flows.nodered.org/node/node-red-dashboard){rel=""nofollow""}. Dashboard is a great tool for creating HMI (Human Machine Interfaces), it's also the most popular custom node for Node-RED with thousands of downloads per week. ### 1. Responsive layouts (almost) Responsive design is the ability for a webpage to change its content to best fit the features of a device used to view the page. For example, when viewing a graph on a mobile phone or a laptop the available screen space differs significantly in size as well as aspect-ratio. Dashboard doesn't offer the feature to change graph sizes based on the screen of a viewing device. That being said, there is one trick you can use to make your dashboards a lot more useful on small and large screens alike. Place your content into Dashboard 'groups', those groups can make use of wider screens by sitting side by side where the screen is big enough while stacking vertically on smaller devices. The image below shows what happens when you change the screen size for this dashboard. :video{ariaLabel="Changing the aspect ratio of the screen" autoPlay="true" height="437" loop="true" muted="true" playsInline="true" preload="none" width="600"} If you'd like to try this out on your own Node-RED, you can import the flow below. ::render-flow{:height='500'} ```json [{"id":"e351b1251dfbc2f7","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"d388b48fcbed93a1","type":"ui_gauge","z":"e351b1251dfbc2f7","name":"","group":"ba1ff527abfa5261","order":2,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":80,"wires":[]},{"id":"80780894450cfb6d","type":"ui_button","z":"e351b1251dfbc2f7","name":"","group":"ba1ff527abfa5261","order":1,"width":0,"height":0,"passthru":false,"label":"Update","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":80,"y":80,"wires":[["49e78ff51c3a1ea3"]]},{"id":"07b44990e5d5b5f6","type":"ui_chart","z":"e351b1251dfbc2f7","name":"","group":"ba1ff527abfa5261","order":3,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":350,"y":120,"wires":[[]]},{"id":"1482bcf69325aa92","type":"inject","z":"e351b1251dfbc2f7","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":90,"y":120,"wires":[["49e78ff51c3a1ea3"]]},{"id":"49e78ff51c3a1ea3","type":"random","z":"e351b1251dfbc2f7","name":"","low":"0","high":"100","inte":"true","property":"payload","x":220,"y":100,"wires":[["d388b48fcbed93a1","07b44990e5d5b5f6"]]},{"id":"38996d8eb9f6535d","type":"ui_gauge","z":"e351b1251dfbc2f7","name":"","group":"f6052a3dccc77ea3","order":2,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":240,"wires":[]},{"id":"cf5599fbda28e614","type":"ui_button","z":"e351b1251dfbc2f7","name":"","group":"f6052a3dccc77ea3","order":1,"width":0,"height":0,"passthru":false,"label":"Update","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":80,"y":240,"wires":[["f093c63e17a620ba"]]},{"id":"936438ebf9eef986","type":"ui_chart","z":"e351b1251dfbc2f7","name":"","group":"f6052a3dccc77ea3","order":3,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":350,"y":280,"wires":[[]]},{"id":"60a40faa335f943e","type":"inject","z":"e351b1251dfbc2f7","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":90,"y":280,"wires":[["f093c63e17a620ba"]]},{"id":"f093c63e17a620ba","type":"random","z":"e351b1251dfbc2f7","name":"","low":"0","high":"100","inte":"true","property":"payload","x":220,"y":260,"wires":[["38996d8eb9f6535d","936438ebf9eef986"]]},{"id":"e284b90164e648e6","type":"ui_gauge","z":"e351b1251dfbc2f7","name":"","group":"1e4a72d62ed7564c","order":2,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":380,"wires":[]},{"id":"cabe7a8150dbfaf6","type":"ui_button","z":"e351b1251dfbc2f7","name":"","group":"1e4a72d62ed7564c","order":1,"width":0,"height":0,"passthru":false,"label":"Update","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":80,"y":380,"wires":[["7b9862186954c4b3"]]},{"id":"ac116116c56e6c3c","type":"ui_chart","z":"e351b1251dfbc2f7","name":"","group":"1e4a72d62ed7564c","order":3,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":350,"y":420,"wires":[[]]},{"id":"bb27ef41380ec1d2","type":"inject","z":"e351b1251dfbc2f7","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":90,"y":420,"wires":[["7b9862186954c4b3"]]},{"id":"7b9862186954c4b3","type":"random","z":"e351b1251dfbc2f7","name":"","low":"0","high":"100","inte":"true","property":"payload","x":220,"y":400,"wires":[["e284b90164e648e6","ac116116c56e6c3c"]]},{"id":"e891f555bad51f01","type":"ui_gauge","z":"e351b1251dfbc2f7","name":"","group":"fcc4481a9e329266","order":2,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":500,"wires":[]},{"id":"fa090cd1a0e97885","type":"ui_button","z":"e351b1251dfbc2f7","name":"","group":"fcc4481a9e329266","order":1,"width":0,"height":0,"passthru":false,"label":"Update","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":80,"y":500,"wires":[["bb4e945a2a8e681c"]]},{"id":"1c3a8aa84dfc85fe","type":"ui_chart","z":"e351b1251dfbc2f7","name":"","group":"fcc4481a9e329266","order":3,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":350,"y":540,"wires":[[]]},{"id":"796f79538c15dd66","type":"inject","z":"e351b1251dfbc2f7","name":"","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":90,"y":540,"wires":[["bb4e945a2a8e681c"]]},{"id":"bb4e945a2a8e681c","type":"random","z":"e351b1251dfbc2f7","name":"","low":"0","high":"100","inte":"true","property":"payload","x":220,"y":520,"wires":[["e891f555bad51f01","1c3a8aa84dfc85fe"]]},{"id":"ba1ff527abfa5261","type":"ui_group","name":"Machine 1","tab":"39383a7a648193dd","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"f6052a3dccc77ea3","type":"ui_group","name":"Machine 2","tab":"39383a7a648193dd","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"1e4a72d62ed7564c","type":"ui_group","name":"Machine 3","tab":"39383a7a648193dd","order":3,"disp":true,"width":"6","collapse":false,"className":""},{"id":"fcc4481a9e329266","type":"ui_group","name":"Machine 4","tab":"39383a7a648193dd","order":4,"disp":true,"width":"6","collapse":false,"className":""},{"id":"39383a7a648193dd","type":"ui_tab","name":"Node-RED Tips","icon":"dashboard","disabled":false,"hidden":false}] ``` :: ### 2. Add more than one series of data to a line chart Being able to add more than one series of data to a single chart can make the data far more useful. One great way to use this is to compare the same data from different sensors. In this example I'm going to show the external and internal temperature at a location on the same chart. ![Graphing two series on the same line chart](https://flowfuse.com/blog/2023/06/images/temp-graph.png "Graphing two series on the same line chart") To do this you need to give a different msg.topic to each series, you can add that using a change node before passing the data to the chart. If you'd like to view this chart on your own Node-RED, you can import the flow below. ::render-flow ```json [{"id":"3eb08d4843164efc","type":"ui_chart","z":"58569b35dacd54f3","name":"","group":"41e847ff22249c0e","order":3,"width":"12","height":"5","label":"Celsius","chartType":"line","legend":"true","xformat":"dd HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"604800","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":280,"y":180,"wires":[[]]},{"id":"c4a13220356f76d3","type":"inject","z":"58569b35dacd54f3","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"series\":[\"inside\",\"outside\"],\"data\":[[{\"x\":1685015544647,\"y\":20.8},{\"x\":1685015844687,\"y\":20.8},{\"x\":1685016144791,\"y\":20.8},{\"x\":1685016444933,\"y\":20.8},{\"x\":1685016745032,\"y\":20.8},{\"x\":1685017045123,\"y\":20.8},{\"x\":1685017345223,\"y\":20.8},{\"x\":1685017645339,\"y\":20.8},{\"x\":1685017945424,\"y\":20.8},{\"x\":1685018245500,\"y\":20.8},{\"x\":1685018545670,\"y\":20.8},{\"x\":1685018860580,\"y\":20.8},{\"x\":1685019160814,\"y\":20.8},{\"x\":1685019460832,\"y\":20.8},{\"x\":1685019760943,\"y\":20.8},{\"x\":1685020061037,\"y\":20.8},{\"x\":1685020361249,\"y\":20.8},{\"x\":1685020661264,\"y\":20.8},{\"x\":1685020961381,\"y\":20.8},{\"x\":1685021261473,\"y\":20.8},{\"x\":1685021561721,\"y\":20.8},{\"x\":1685021876580,\"y\":20.6},{\"x\":1685022176696,\"y\":20.7},{\"x\":1685022476852,\"y\":20.8},{\"x\":1685022776957,\"y\":20.8},{\"x\":1685023077032,\"y\":20.8},{\"x\":1685023377120,\"y\":20.8},{\"x\":1685023677234,\"y\":20.8},{\"x\":1685023977339,\"y\":20.8},{\"x\":1685024277455,\"y\":20.8},{\"x\":1685024577576,\"y\":20.8},{\"x\":1685024892578,\"y\":20.8},{\"x\":1685025192616,\"y\":20.8},{\"x\":1685025492757,\"y\":20.8},{\"x\":1685025792847,\"y\":20.7},{\"x\":1685026092949,\"y\":20.8},{\"x\":1685026393052,\"y\":20.8},{\"x\":1685026693174,\"y\":20.8},{\"x\":1685026993234,\"y\":20.8},{\"x\":1685027293387,\"y\":20.8},{\"x\":1685027593514,\"y\":20.8},{\"x\":1685027908550,\"y\":20.8},{\"x\":1685028208624,\"y\":20.8},{\"x\":1685028508708,\"y\":20.8},{\"x\":1685028808828,\"y\":20.8},{\"x\":1685029108909,\"y\":20.8},{\"x\":1685029409026,\"y\":20.8},{\"x\":1685029709096,\"y\":20.8},{\"x\":1685030009262,\"y\":20.8},{\"x\":1685030309361,\"y\":20.8},{\"x\":1685030609541,\"y\":20.8},{\"x\":1685030924565,\"y\":20.8},{\"x\":1685031239588,\"y\":20.8},{\"x\":1685031539702,\"y\":20.8},{\"x\":1685031839784,\"y\":20.8},{\"x\":1685032139922,\"y\":20.8},{\"x\":1685032440027,\"y\":20.8},{\"x\":1685032740134,\"y\":20.6},{\"x\":1685033040252,\"y\":20.7},{\"x\":1685033340317,\"y\":20.5},{\"x\":1685033640450,\"y\":20.5},{\"x\":1685033940507,\"y\":20.6},{\"x\":1685034240592,\"y\":20.5},{\"x\":1685034540633,\"y\":20.4},{\"x\":1685034840784,\"y\":20.4},{\"x\":1685035140888,\"y\":20.5},{\"x\":1685035441026,\"y\":20.4},{\"x\":1685035741107,\"y\":20.5},{\"x\":1685036041205,\"y\":20.5},{\"x\":1685036341328,\"y\":20.5},{\"x\":1685036641457,\"y\":20.5},{\"x\":1685036941583,\"y\":20.4},{\"x\":1685037256578,\"y\":20.5},{\"x\":1685037556715,\"y\":20.5},{\"x\":1685037856773,\"y\":20.5},{\"x\":1685038156902,\"y\":20.5},{\"x\":1685038457023,\"y\":20.5},{\"x\":1685038757090,\"y\":20.5},{\"x\":1685039057219,\"y\":20.5},{\"x\":1685039357278,\"y\":20.5},{\"x\":1685039657449,\"y\":20.5},{\"x\":1685039957553,\"y\":20.5},{\"x\":1685040257570,\"y\":20.5},{\"x\":1685040557615,\"y\":20.5},{\"x\":1685040857735,\"y\":20.4},{\"x\":1685041157843,\"y\":20.4},{\"x\":1685041457971,\"y\":20.5},{\"x\":1685041758072,\"y\":20.5},{\"x\":1685042058154,\"y\":20.5},{\"x\":1685042358273,\"y\":20.5},{\"x\":1685042658392,\"y\":20.5},{\"x\":1685042958486,\"y\":20.6},{\"x\":1685043258518,\"y\":20.6},{\"x\":1685043558568,\"y\":20.6},{\"x\":1685043858688,\"y\":20.5},{\"x\":1685044158769,\"y\":20.5},{\"x\":1685044458908,\"y\":20.5},{\"x\":1685044758984,\"y\":20.5},{\"x\":1685045059095,\"y\":20.5},{\"x\":1685045359169,\"y\":20.5},{\"x\":1685045659320,\"y\":20.5},{\"x\":1685045959367,\"y\":20.5},{\"x\":1685046259469,\"y\":20.7},{\"x\":1685046559497,\"y\":20.6},{\"x\":1685046859589,\"y\":20.5},{\"x\":1685047159647,\"y\":20.6},{\"x\":1685047459714,\"y\":20.6},{\"x\":1685047759793,\"y\":20.7},{\"x\":1685048059921,\"y\":20.7},{\"x\":1685048359994,\"y\":20.6},{\"x\":1685048659997,\"y\":20.7},{\"x\":1685048960056,\"y\":20.7},{\"x\":1685049260070,\"y\":20.8},{\"x\":1685049560116,\"y\":20.8},{\"x\":1685049860167,\"y\":20.8},{\"x\":1685050160212,\"y\":20.8},{\"x\":1685050460281,\"y\":20.8},{\"x\":1685050760368,\"y\":20.8},{\"x\":1685051060400,\"y\":20.7},{\"x\":1685051360435,\"y\":20.8},{\"x\":1685051660553,\"y\":20.8},{\"x\":1685051960585,\"y\":20.8},{\"x\":1685052260665,\"y\":20.8},{\"x\":1685052560675,\"y\":20.8},{\"x\":1685052860700,\"y\":20.7},{\"x\":1685053160749,\"y\":20.8},{\"x\":1685053460790,\"y\":20.8},{\"x\":1685053760855,\"y\":20.7},{\"x\":1685054060931,\"y\":20.7},{\"x\":1685054361019,\"y\":20.7},{\"x\":1685054661030,\"y\":20.7},{\"x\":1685054961121,\"y\":20.7},{\"x\":1685055261227,\"y\":20.7},{\"x\":1685055576250,\"y\":20.7},{\"x\":1685055891289,\"y\":20.5},{\"x\":1685056191290,\"y\":20.6},{\"x\":1685056491397,\"y\":20.7},{\"x\":1685056791444,\"y\":20.7},{\"x\":1685057091489,\"y\":20.7},{\"x\":1685057391516,\"y\":20.7},{\"x\":1685057691607,\"y\":20.7},{\"x\":1685057991660,\"y\":20.7},{\"x\":1685058291741,\"y\":20.6},{\"x\":1685058606760,\"y\":20.5},{\"x\":1685058906775,\"y\":20.5},{\"x\":1685059206866,\"y\":20.5},{\"x\":1685059506900,\"y\":20.5},{\"x\":1685059806979,\"y\":20.4},{\"x\":1685060107035,\"y\":20.5},{\"x\":1685060407099,\"y\":20.5},{\"x\":1685060707132,\"y\":20.4},{\"x\":1685061007199,\"y\":20.5},{\"x\":1685061307220,\"y\":20.4},{\"x\":1685061607228,\"y\":20.5},{\"x\":1685061907286,\"y\":20.6},{\"x\":1685062207289,\"y\":20.7},{\"x\":1685062507315,\"y\":20.6},{\"x\":1685062807355,\"y\":20.7},{\"x\":1685063107395,\"y\":20.7},{\"x\":1685063407413,\"y\":20.7},{\"x\":1685063707465,\"y\":20.7},{\"x\":1685064007492,\"y\":20.7},{\"x\":1685064307565,\"y\":20.7},{\"x\":1685064607587,\"y\":20.7},{\"x\":1685064907609,\"y\":20.7},{\"x\":1685065207618,\"y\":20.5},{\"x\":1685065507642,\"y\":20.4},{\"x\":1685065807670,\"y\":20.4},{\"x\":1685066107679,\"y\":20.4},{\"x\":1685066407758,\"y\":20.5},{\"x\":1685066707760,\"y\":20.3},{\"x\":1685067007798,\"y\":20.4},{\"x\":1685067307865,\"y\":20.4},{\"x\":1685067622903,\"y\":20.4},{\"x\":1685067937922,\"y\":20.4},{\"x\":1685068237945,\"y\":20.4},{\"x\":1685068537971,\"y\":20.3},{\"x\":1685068838009,\"y\":20.3},{\"x\":1685069138060,\"y\":20.2},{\"x\":1685069453065,\"y\":20.3},{\"x\":1685069753104,\"y\":20.3},{\"x\":1685070053108,\"y\":20.2},{\"x\":1685070353140,\"y\":20.3},{\"x\":1685070653170,\"y\":20.2},{\"x\":1685070953219,\"y\":20.2},{\"x\":1685071253232,\"y\":20.2},{\"x\":1685071553258,\"y\":20},{\"x\":1685071853260,\"y\":20},{\"x\":1685072153324,\"y\":20.2},{\"x\":1685072453329,\"y\":20.2},{\"x\":1685072753343,\"y\":19.9},{\"x\":1685073053359,\"y\":20},{\"x\":1685073353402,\"y\":19.9},{\"x\":1685073653411,\"y\":19.9},{\"x\":1685073953453,\"y\":20},{\"x\":1685074253462,\"y\":20},{\"x\":1685074553509,\"y\":20},{\"x\":1685074853529,\"y\":20},{\"x\":1685075153554,\"y\":20},{\"x\":1685075453556,\"y\":20},{\"x\":1685075753590,\"y\":20},{\"x\":1685076053642,\"y\":20.2},{\"x\":1685076368644,\"y\":20},{\"x\":1685076668676,\"y\":20.2},{\"x\":1685076968685,\"y\":20.3},{\"x\":1685077268752,\"y\":20.3},{\"x\":1685077583729,\"y\":20.3},{\"x\":1685077883800,\"y\":20.2},{\"x\":1685078198760,\"y\":20.2},{\"x\":1685078498762,\"y\":20.4},{\"x\":1685078798782,\"y\":20.4},{\"x\":1685079098837,\"y\":20.5},{\"x\":1685079398903,\"y\":20.5},{\"x\":1685079698965,\"y\":20.6},{\"x\":1685079999013,\"y\":20.7},{\"x\":1685080299091,\"y\":20.8},{\"x\":1685080599234,\"y\":20.8},{\"x\":1685080899276,\"y\":20.8},{\"x\":1685081199340,\"y\":20.8},{\"x\":1685081499403,\"y\":20.8},{\"x\":1685081799441,\"y\":20.8},{\"x\":1685082099467,\"y\":20.9},{\"x\":1685082399584,\"y\":20.8},{\"x\":1685082699624,\"y\":21},{\"x\":1685082999670,\"y\":21},{\"x\":1685083299724,\"y\":21.2},{\"x\":1685083599810,\"y\":21.2},{\"x\":1685083899884,\"y\":21.2},{\"x\":1685084199935,\"y\":21.2},{\"x\":1685084500001,\"y\":21.3},{\"x\":1685084800063,\"y\":21.4},{\"x\":1685085115072,\"y\":21.5},{\"x\":1685085415170,\"y\":21.5},{\"x\":1685085715226,\"y\":21.5},{\"x\":1685086015289,\"y\":21.5},{\"x\":1685086315365,\"y\":21.5},{\"x\":1685086615447,\"y\":21.5},{\"x\":1685086915501,\"y\":21.7},{\"x\":1685087215603,\"y\":21.7},{\"x\":1685087515642,\"y\":21.6},{\"x\":1685087815732,\"y\":21.6},{\"x\":1685088115759,\"y\":21.7},{\"x\":1685088415793,\"y\":21.9},{\"x\":1685088715840,\"y\":21.9},{\"x\":1685089015891,\"y\":21.9},{\"x\":1685089315950,\"y\":21.9},{\"x\":1685089616045,\"y\":21.9},{\"x\":1685089916094,\"y\":21.9},{\"x\":1685090216153,\"y\":21.9},{\"x\":1685090516202,\"y\":21.9},{\"x\":1685090816299,\"y\":21.9},{\"x\":1685091116304,\"y\":22},{\"x\":1685091416326,\"y\":22},{\"x\":1685091716409,\"y\":22},{\"x\":1685092016500,\"y\":22},{\"x\":1685092316555,\"y\":21.9},{\"x\":1685092616597,\"y\":21.9},{\"x\":1685092916687,\"y\":21.9},{\"x\":1685093216749,\"y\":21.9},{\"x\":1685093516770,\"y\":21.9},{\"x\":1685093816833,\"y\":21.8},{\"x\":1685094116862,\"y\":21.8},{\"x\":1685094416941,\"y\":21.9},{\"x\":1685094717014,\"y\":21.8},{\"x\":1685095017079,\"y\":21.8},{\"x\":1685095317146,\"y\":21.9},{\"x\":1685095617198,\"y\":21.9},{\"x\":1685095917267,\"y\":22},{\"x\":1685096217380,\"y\":22},{\"x\":1685096517436,\"y\":22.2},{\"x\":1685096817481,\"y\":22.1},{\"x\":1685097117519,\"y\":21.9},{\"x\":1685097417582,\"y\":21.9},{\"x\":1685097717607,\"y\":21.9},{\"x\":1685098017734,\"y\":21.9},{\"x\":1685098317791,\"y\":21.9},{\"x\":1685098617850,\"y\":21.9},{\"x\":1685098917939,\"y\":21.9},{\"x\":1685099218020,\"y\":21.9},{\"x\":1685099518088,\"y\":21.9},{\"x\":1685099818190,\"y\":21.9},{\"x\":1685100118269,\"y\":21.9},{\"x\":1685100418304,\"y\":21.9},{\"x\":1685100718355,\"y\":21.9},{\"x\":1685101018437,\"y\":22},{\"x\":1685101318544,\"y\":22.1},{\"x\":1685101618646,\"y\":22},{\"x\":1685101918720,\"y\":22},{\"x\":1685102218803,\"y\":22},{\"x\":1685102518914,\"y\":21.9},{\"x\":1685102818997,\"y\":21.9},{\"x\":1685103119093,\"y\":21.9},{\"x\":1685103419119,\"y\":21.9},{\"x\":1685103719183,\"y\":21.9},{\"x\":1685104019263,\"y\":21.9},{\"x\":1685104319361,\"y\":21.9},{\"x\":1685104619467,\"y\":21.9},{\"x\":1685104919565,\"y\":21.9},{\"x\":1685105219692,\"y\":21.9},{\"x\":1685105261657,\"y\":21.9},{\"x\":1685105275726,\"y\":21.9},{\"x\":1685105316848,\"y\":21.9},{\"x\":1685105388676,\"y\":21.9},{\"x\":1685105475001,\"y\":21.9},{\"x\":1685105501466,\"y\":21.9},{\"x\":1685105576628,\"y\":21.9},{\"x\":1685105627162,\"y\":21.9},{\"x\":1685105653330,\"y\":21.9},{\"x\":1685105684112,\"y\":21.9},{\"x\":1685105722222,\"y\":21.9},{\"x\":1685105769629,\"y\":21.9},{\"x\":1685105825666,\"y\":21.9},{\"x\":1685105889137,\"y\":21.9},{\"x\":1685105910507,\"y\":21.9},{\"x\":1685105970749,\"y\":21.9},{\"x\":1685106270828,\"y\":21.9},{\"x\":1685106570844,\"y\":21.9},{\"x\":1685106870845,\"y\":21.9},{\"x\":1685107170872,\"y\":21.9},{\"x\":1685107485846,\"y\":21.9},{\"x\":1685107785884,\"y\":21.9},{\"x\":1685108100859,\"y\":21.9},{\"x\":1685108400859,\"y\":21.9},{\"x\":1685108700881,\"y\":21.9},{\"x\":1685109000885,\"y\":21.9},{\"x\":1685109300898,\"y\":21.9},{\"x\":1685109615915,\"y\":21.8},{\"x\":1685109930873,\"y\":21.9},{\"x\":1685110230928,\"y\":21.9},{\"x\":1685110545931,\"y\":21.7},{\"x\":1685110845948,\"y\":21.7},{\"x\":1685111145989,\"y\":21.7},{\"x\":1685111460988,\"y\":21.7},{\"x\":1685111761062,\"y\":21.8},{\"x\":1685112061067,\"y\":21.6},{\"x\":1685112361107,\"y\":21.7},{\"x\":1685112661141,\"y\":21.7},{\"x\":1685112961145,\"y\":21.8},{\"x\":1685113261191,\"y\":21.8},{\"x\":1685113561195,\"y\":21.8},{\"x\":1685113861234,\"y\":21.8},{\"x\":1685114161272,\"y\":21.8},{\"x\":1685114461288,\"y\":21.7},{\"x\":1685114761326,\"y\":21.7},{\"x\":1685115061359,\"y\":21.7},{\"x\":1685115361389,\"y\":21.7},{\"x\":1685115661413,\"y\":21.6},{\"x\":1685115961522,\"y\":21.7},{\"x\":1685116276440,\"y\":21.7},{\"x\":1685116576471,\"y\":21.5},{\"x\":1685116876500,\"y\":21.5},{\"x\":1685117176511,\"y\":21.5},{\"x\":1685117476538,\"y\":21.6},{\"x\":1685117776599,\"y\":21.6},{\"x\":1685118091580,\"y\":21.8},{\"x\":1685118391620,\"y\":21.6},{\"x\":1685118691646,\"y\":21.7},{\"x\":1685118991656,\"y\":21.7},{\"x\":1685119291661,\"y\":21.5},{\"x\":1685119591684,\"y\":21.5},{\"x\":1685119891711,\"y\":21.5},{\"x\":1685120191732,\"y\":21.5},{\"x\":1685120491763,\"y\":21.5},{\"x\":1685120791797,\"y\":21.4},{\"x\":1685121091832,\"y\":21.2},{\"x\":1685121406802,\"y\":21.2},{\"x\":1685121706853,\"y\":21.1},{\"x\":1685122021885,\"y\":21.1},{\"x\":1685122336872,\"y\":21.2},{\"x\":1685122636878,\"y\":21.2},{\"x\":1685122936903,\"y\":21.1},{\"x\":1685123236933,\"y\":21},{\"x\":1685123536988,\"y\":21},{\"x\":1685123837007,\"y\":21.1},{\"x\":1685124137007,\"y\":21},{\"x\":1685124437098,\"y\":21},{\"x\":1685124737193,\"y\":21},{\"x\":1685125037270,\"y\":21},{\"x\":1685125337370,\"y\":20.9},{\"x\":1685125637493,\"y\":20.9},{\"x\":1685125937530,\"y\":21},{\"x\":1685126237627,\"y\":20.9},{\"x\":1685126537712,\"y\":20.8},{\"x\":1685126837733,\"y\":20.8},{\"x\":1685127137746,\"y\":20.8},{\"x\":1685127437870,\"y\":20.8},{\"x\":1685127737940,\"y\":20.8},{\"x\":1685128038026,\"y\":20.8},{\"x\":1685128338109,\"y\":20.8},{\"x\":1685128638215,\"y\":20.8},{\"x\":1685128938321,\"y\":20.8},{\"x\":1685129238409,\"y\":20.8},{\"x\":1685129538484,\"y\":20.8},{\"x\":1685129838514,\"y\":20.8},{\"x\":1685130138614,\"y\":20.8},{\"x\":1685130438644,\"y\":20.8},{\"x\":1685130738716,\"y\":20.8},{\"x\":1685131038856,\"y\":20.8},{\"x\":1685131338912,\"y\":20.8},{\"x\":1685131639028,\"y\":20.8},{\"x\":1685131939140,\"y\":20.8},{\"x\":1685132239222,\"y\":20.8},{\"x\":1685132539344,\"y\":20.8},{\"x\":1685132839390,\"y\":20.8},{\"x\":1685133154381,\"y\":20.8},{\"x\":1685133454402,\"y\":20.8},{\"x\":1685133754494,\"y\":20.8},{\"x\":1685134054557,\"y\":20.8},{\"x\":1685134354644,\"y\":20.8},{\"x\":1685134654753,\"y\":20.8},{\"x\":1685134954810,\"y\":20.8},{\"x\":1685135254894,\"y\":20.7},{\"x\":1685135554961,\"y\":20.8},{\"x\":1685135854990,\"y\":20.8},{\"x\":1685136155013,\"y\":20.8},{\"x\":1685136455070,\"y\":20.8},{\"x\":1685136755172,\"y\":20.8},{\"x\":1685137055242,\"y\":20.8},{\"x\":1685137355339,\"y\":20.8},{\"x\":1685137655440,\"y\":20.7},{\"x\":1685137955504,\"y\":20.7},{\"x\":1685138255592,\"y\":20.6},{\"x\":1685138555675,\"y\":20.6},{\"x\":1685138855742,\"y\":20.6},{\"x\":1685139155750,\"y\":20.6},{\"x\":1685139455841,\"y\":20.5},{\"x\":1685139755904,\"y\":20.6},{\"x\":1685140055961,\"y\":20.5},{\"x\":1685140356100,\"y\":20.6},{\"x\":1685140656187,\"y\":20.5},{\"x\":1685140956268,\"y\":20.4},{\"x\":1685141256384,\"y\":20.4},{\"x\":1685141556467,\"y\":20.4},{\"x\":1685141856547,\"y\":20.4},{\"x\":1685142156630,\"y\":20.3},{\"x\":1685142456678,\"y\":20.2},{\"x\":1685142756713,\"y\":20.3},{\"x\":1685143056833,\"y\":20.3},{\"x\":1685143356922,\"y\":20.3},{\"x\":1685143657019,\"y\":20.3},{\"x\":1685143957116,\"y\":20.2},{\"x\":1685144257203,\"y\":20.2},{\"x\":1685144557262,\"y\":20.1},{\"x\":1685144857393,\"y\":20.2},{\"x\":1685145157511,\"y\":20.2},{\"x\":1685145457515,\"y\":20.2},{\"x\":1685145757583,\"y\":20},{\"x\":1685146057687,\"y\":20},{\"x\":1685146357780,\"y\":20.1},{\"x\":1685146657878,\"y\":19.9},{\"x\":1685146957969,\"y\":19.9},{\"x\":1685147258066,\"y\":20},{\"x\":1685147558163,\"y\":20},{\"x\":1685147858255,\"y\":19.9},{\"x\":1685148158303,\"y\":19.9},{\"x\":1685148458377,\"y\":19.9},{\"x\":1685148758392,\"y\":19.9},{\"x\":1685149058441,\"y\":19.9},{\"x\":1685149358550,\"y\":19.9},{\"x\":1685149658652,\"y\":19.5},{\"x\":1685149958735,\"y\":19.5},{\"x\":1685150258816,\"y\":19.5},{\"x\":1685150558892,\"y\":19.5},{\"x\":1685150858996,\"y\":19.5},{\"x\":1685151159109,\"y\":19.5},{\"x\":1685151459166,\"y\":19.5},{\"x\":1685151759230,\"y\":19.5},{\"x\":1685152059291,\"y\":19.5},{\"x\":1685152359350,\"y\":19.5},{\"x\":1685152659430,\"y\":19.5},{\"x\":1685152959526,\"y\":19.5},{\"x\":1685153259622,\"y\":19.5},{\"x\":1685153559728,\"y\":19.5},{\"x\":1685153859883,\"y\":19.5},{\"x\":1685154159899,\"y\":19.5},{\"x\":1685154459974,\"y\":19.5},{\"x\":1685154760016,\"y\":19.5},{\"x\":1685155060096,\"y\":19.5},{\"x\":1685155360142,\"y\":19.5},{\"x\":1685155660284,\"y\":19.5},{\"x\":1685155960372,\"y\":19.5},{\"x\":1685156260469,\"y\":19.5},{\"x\":1685156560559,\"y\":19.5},{\"x\":1685156860664,\"y\":19.5},{\"x\":1685157160732,\"y\":19.5},{\"x\":1685157460827,\"y\":19.5},{\"x\":1685157760925,\"y\":19.5},{\"x\":1685158061041,\"y\":19.4},{\"x\":1685158361093,\"y\":19.4},{\"x\":1685158661174,\"y\":19.4},{\"x\":1685158961238,\"y\":19.4},{\"x\":1685159261356,\"y\":19.4},{\"x\":1685159561460,\"y\":19.4},{\"x\":1685159861591,\"y\":19.4},{\"x\":1685160161622,\"y\":19.5},{\"x\":1685160461727,\"y\":19.4},{\"x\":1685160761798,\"y\":19.3},{\"x\":1685161061899,\"y\":19.3},{\"x\":1685161361960,\"y\":19.3},{\"x\":1685161662030,\"y\":19.3},{\"x\":1685161962083,\"y\":19.2},{\"x\":1685162262205,\"y\":19.2},{\"x\":1685162562329,\"y\":19.2},{\"x\":1685162862420,\"y\":19.2},{\"x\":1685163162514,\"y\":19.3},{\"x\":1685163462630,\"y\":19.4},{\"x\":1685163762689,\"y\":19.5},{\"x\":1685164062785,\"y\":19.5},{\"x\":1685164362862,\"y\":19.5},{\"x\":1685164662929,\"y\":19.5},{\"x\":1685164962989,\"y\":19.5},{\"x\":1685165263080,\"y\":19.5},{\"x\":1685165563213,\"y\":19.5},{\"x\":1685165863284,\"y\":19.5},{\"x\":1685166163398,\"y\":19.7},{\"x\":1685166463432,\"y\":20},{\"x\":1685166763562,\"y\":20},{\"x\":1685167063657,\"y\":20.3},{\"x\":1685167363752,\"y\":20.3},{\"x\":1685167663818,\"y\":20.2},{\"x\":1685167963868,\"y\":20.4},{\"x\":1685168263959,\"y\":20.5},{\"x\":1685168564052,\"y\":20.7},{\"x\":1685168864152,\"y\":20.8},{\"x\":1685169164297,\"y\":20.8},{\"x\":1685169464371,\"y\":20.8},{\"x\":1685169764428,\"y\":20.9},{\"x\":1685170064553,\"y\":21},{\"x\":1685170364630,\"y\":20.8},{\"x\":1685170664689,\"y\":20.8},{\"x\":1685170964744,\"y\":20.9},{\"x\":1685171264839,\"y\":21},{\"x\":1685171564944,\"y\":21},{\"x\":1685171865046,\"y\":21.1},{\"x\":1685172165136,\"y\":21.1},{\"x\":1685172465216,\"y\":21.2},{\"x\":1685172765270,\"y\":21.5},{\"x\":1685173065378,\"y\":21.5},{\"x\":1685173365491,\"y\":21.5},{\"x\":1685173665574,\"y\":21.5},{\"x\":1685173965634,\"y\":21.5},{\"x\":1685174265651,\"y\":21.5},{\"x\":1685174565770,\"y\":21.5},{\"x\":1685174865842,\"y\":21.5},{\"x\":1685175165962,\"y\":21.5},{\"x\":1685175466056,\"y\":21.5},{\"x\":1685175766151,\"y\":21.6},{\"x\":1685176066247,\"y\":21.7},{\"x\":1685176366347,\"y\":21.6},{\"x\":1685176666421,\"y\":21.6},{\"x\":1685176966481,\"y\":21.7},{\"x\":1685177266535,\"y\":21.7},{\"x\":1685177566618,\"y\":21.7},{\"x\":1685177866715,\"y\":21.8},{\"x\":1685178166786,\"y\":21.7},{\"x\":1685178466868,\"y\":21.7},{\"x\":1685178766964,\"y\":21.8},{\"x\":1685179067013,\"y\":21.9},{\"x\":1685179367161,\"y\":21.9},{\"x\":1685179667226,\"y\":21.9},{\"x\":1685179967298,\"y\":21.9},{\"x\":1685180267304,\"y\":21.9},{\"x\":1685180567400,\"y\":21.9},{\"x\":1685180867474,\"y\":21.9},{\"x\":1685181167605,\"y\":21.9},{\"x\":1685181467661,\"y\":21.9},{\"x\":1685181767781,\"y\":21.9},{\"x\":1685182067847,\"y\":21.9},{\"x\":1685182367916,\"y\":21.9},{\"x\":1685182668010,\"y\":21.9},{\"x\":1685182968067,\"y\":21.9},{\"x\":1685183268142,\"y\":21.9},{\"x\":1685183568218,\"y\":21.9},{\"x\":1685183868264,\"y\":21.9},{\"x\":1685184168356,\"y\":22},{\"x\":1685184468449,\"y\":22},{\"x\":1685184768557,\"y\":22},{\"x\":1685185068624,\"y\":22.2},{\"x\":1685185368711,\"y\":22.1},{\"x\":1685185668784,\"y\":22.1},{\"x\":1685185968847,\"y\":22},{\"x\":1685186268953,\"y\":21.9},{\"x\":1685186568982,\"y\":21.9},{\"x\":1685186868996,\"y\":21.9},{\"x\":1685187169094,\"y\":21.9},{\"x\":1685187469146,\"y\":21.9},{\"x\":1685187769224,\"y\":21.9},{\"x\":1685188069250,\"y\":21.9},{\"x\":1685188369327,\"y\":21.9},{\"x\":1685188669411,\"y\":21.7},{\"x\":1685188969475,\"y\":21.7},{\"x\":1685189269500,\"y\":21.5},{\"x\":1685189569548,\"y\":21.5},{\"x\":1685189869583,\"y\":21.6},{\"x\":1685190169645,\"y\":21.7},{\"x\":1685190469717,\"y\":21.7},{\"x\":1685190769801,\"y\":21.7},{\"x\":1685191069848,\"y\":21.7},{\"x\":1685191369885,\"y\":21.7},{\"x\":1685191669980,\"y\":21.7},{\"x\":1685191970014,\"y\":21.7},{\"x\":1685192270055,\"y\":21.6},{\"x\":1685192570106,\"y\":21.7},{\"x\":1685192870150,\"y\":21.7},{\"x\":1685193170235,\"y\":21.6},{\"x\":1685193470249,\"y\":21.5},{\"x\":1685193770302,\"y\":21.5},{\"x\":1685194070311,\"y\":21.6},{\"x\":1685194370328,\"y\":21.7},{\"x\":1685194670393,\"y\":21.7},{\"x\":1685194985377,\"y\":21.7},{\"x\":1685195285440,\"y\":21.7},{\"x\":1685195585471,\"y\":21.7},{\"x\":1685195885473,\"y\":21.7},{\"x\":1685196185514,\"y\":21.6},{\"x\":1685196485562,\"y\":21.7},{\"x\":1685196800615,\"y\":21.7},{\"x\":1685197115572,\"y\":21.7},{\"x\":1685197415601,\"y\":21.7},{\"x\":1685197715640,\"y\":21.7},{\"x\":1685198015652,\"y\":21.7},{\"x\":1685198315706,\"y\":21.7},{\"x\":1685198615725,\"y\":21.7},{\"x\":1685198915775,\"y\":21.8},{\"x\":1685199215786,\"y\":21.8},{\"x\":1685199515831,\"y\":21.9},{\"x\":1685199815856,\"y\":21.8},{\"x\":1685200115858,\"y\":21.9},{\"x\":1685200415875,\"y\":21.8},{\"x\":1685200715913,\"y\":21.8},{\"x\":1685201015918,\"y\":21.8},{\"x\":1685201315964,\"y\":21.9},{\"x\":1685201616032,\"y\":21.9},{\"x\":1685201916044,\"y\":21.9},{\"x\":1685202231051,\"y\":21.9},{\"x\":1685202531062,\"y\":21.9},{\"x\":1685202831089,\"y\":21.8},{\"x\":1685203131095,\"y\":21.8},{\"x\":1685203431195,\"y\":21.8},{\"x\":1685203746138,\"y\":21.8},{\"x\":1685204046168,\"y\":21.8},{\"x\":1685204346182,\"y\":21.7},{\"x\":1685204646203,\"y\":21.8},{\"x\":1685204946241,\"y\":21.9},{\"x\":1685205246243,\"y\":21.9},{\"x\":1685205546262,\"y\":21.9},{\"x\":1685205846296,\"y\":21.9},{\"x\":1685206146312,\"y\":21.9},{\"x\":1685206446335,\"y\":21.9},{\"x\":1685206746375,\"y\":21.9},{\"x\":1685207046395,\"y\":21.9},{\"x\":1685207346549,\"y\":21.9},{\"x\":1685207661544,\"y\":21.9},{\"x\":1685207961647,\"y\":21.9},{\"x\":1685208261700,\"y\":21.9},{\"x\":1685208561766,\"y\":21.9},{\"x\":1685208861855,\"y\":21.9},{\"x\":1685209161943,\"y\":21.9},{\"x\":1685209462007,\"y\":21.9},{\"x\":1685209762097,\"y\":21.9},{\"x\":1685210077101,\"y\":21.9},{\"x\":1685210377152,\"y\":21.9},{\"x\":1685210677231,\"y\":21.9},{\"x\":1685210977296,\"y\":21.9},{\"x\":1685211277406,\"y\":21.9},{\"x\":1685211577537,\"y\":21.9},{\"x\":1685211877620,\"y\":21.9},{\"x\":1685212177660,\"y\":21.9},{\"x\":1685212477797,\"y\":21.9},{\"x\":1685212777837,\"y\":21.9},{\"x\":1685213077881,\"y\":21.9},{\"x\":1685213377932,\"y\":21.9},{\"x\":1685213677997,\"y\":21.9},{\"x\":1685213978205,\"y\":21.9},{\"x\":1685214293200,\"y\":21.9},{\"x\":1685214593270,\"y\":21.9},{\"x\":1685214893375,\"y\":21.9},{\"x\":1685215193482,\"y\":21.9},{\"x\":1685215493529,\"y\":21.8},{\"x\":1685215793531,\"y\":21.9},{\"x\":1685216093643,\"y\":21.9},{\"x\":1685216393738,\"y\":22},{\"x\":1685216693784,\"y\":21.9},{\"x\":1685216993880,\"y\":21.9},{\"x\":1685217293962,\"y\":21.7},{\"x\":1685217594100,\"y\":21.7},{\"x\":1685217894151,\"y\":21.7},{\"x\":1685218194256,\"y\":21.6},{\"x\":1685218494376,\"y\":21.5},{\"x\":1685218794467,\"y\":21.5},{\"x\":1685219094477,\"y\":21.5},{\"x\":1685219394675,\"y\":21.5},{\"x\":1685219709688,\"y\":21.5},{\"x\":1685220009789,\"y\":21.5},{\"x\":1685220309889,\"y\":21.5},{\"x\":1685220609986,\"y\":21.4},{\"x\":1685220910080,\"y\":21.5},{\"x\":1685221210236,\"y\":21.5},{\"x\":1685221510267,\"y\":21.5},{\"x\":1685221810331,\"y\":21.5},{\"x\":1685222110381,\"y\":21.4},{\"x\":1685222410481,\"y\":21.4},{\"x\":1685222710598,\"y\":21.3},{\"x\":1685223010661,\"y\":21.5},{\"x\":1685223310759,\"y\":21.5},{\"x\":1685223610881,\"y\":21.5},{\"x\":1685223911009,\"y\":21.5},{\"x\":1685224211106,\"y\":21.5},{\"x\":1685224511191,\"y\":21.5},{\"x\":1685224811272,\"y\":21.4},{\"x\":1685225111309,\"y\":21.4},{\"x\":1685225411398,\"y\":21.3},{\"x\":1685225711438,\"y\":21.2},{\"x\":1685226011552,\"y\":21.2},{\"x\":1685226311659,\"y\":21.2},{\"x\":1685226611773,\"y\":21.2},{\"x\":1685226911876,\"y\":21.2},{\"x\":1685227211995,\"y\":21.2},{\"x\":1685227512070,\"y\":21.2},{\"x\":1685227812175,\"y\":21.2},{\"x\":1685228112250,\"y\":21.2},{\"x\":1685228412314,\"y\":21},{\"x\":1685228712352,\"y\":21},{\"x\":1685229012470,\"y\":21},{\"x\":1685229312587,\"y\":21},{\"x\":1685229612662,\"y\":21.1},{\"x\":1685229912736,\"y\":21},{\"x\":1685230212847,\"y\":20.9},{\"x\":1685230512904,\"y\":21},{\"x\":1685230813034,\"y\":21},{\"x\":1685231113081,\"y\":21},{\"x\":1685231413122,\"y\":20.8},{\"x\":1685231713183,\"y\":20.8},{\"x\":1685232013342,\"y\":20.8},{\"x\":1685232328321,\"y\":20.8},{\"x\":1685232628419,\"y\":20.8},{\"x\":1685232928528,\"y\":20.8},{\"x\":1685233228604,\"y\":20.8},{\"x\":1685233528687,\"y\":20.8},{\"x\":1685233828743,\"y\":20.8},{\"x\":1685234128809,\"y\":20.8},{\"x\":1685234428883,\"y\":20.8},{\"x\":1685234728963,\"y\":20.8},{\"x\":1685235028977,\"y\":20.8},{\"x\":1685235329064,\"y\":20.8},{\"x\":1685235629163,\"y\":20.8},{\"x\":1685235929251,\"y\":20.8},{\"x\":1685236229341,\"y\":20.8},{\"x\":1685236529423,\"y\":20.7},{\"x\":1685236829513,\"y\":20.7},{\"x\":1685237129612,\"y\":20.8},{\"x\":1685237429697,\"y\":20.7},{\"x\":1685237729771,\"y\":20.7},{\"x\":1685238029832,\"y\":20.5},{\"x\":1685238329902,\"y\":20.5},{\"x\":1685238629974,\"y\":20.5},{\"x\":1685238930096,\"y\":20.5},{\"x\":1685239230144,\"y\":20.6},{\"x\":1685239530225,\"y\":20.6},{\"x\":1685239830331,\"y\":20.6},{\"x\":1685240130366,\"y\":20.6},{\"x\":1685240430509,\"y\":20.5},{\"x\":1685240730597,\"y\":20.5},{\"x\":1685241030697,\"y\":20.4},{\"x\":1685241330716,\"y\":20.4},{\"x\":1685241630792,\"y\":20.4},{\"x\":1685241930839,\"y\":20.4},{\"x\":1685242230957,\"y\":20.4},{\"x\":1685242531061,\"y\":20.4},{\"x\":1685242831116,\"y\":20.4},{\"x\":1685243131245,\"y\":20.4},{\"x\":1685243431358,\"y\":20.4},{\"x\":1685243731422,\"y\":20.3},{\"x\":1685244031484,\"y\":20.3},{\"x\":1685244331554,\"y\":20.2},{\"x\":1685244631614,\"y\":20.3},{\"x\":1685244931700,\"y\":20.3},{\"x\":1685245231790,\"y\":20.3},{\"x\":1685245531908,\"y\":20.3},{\"x\":1685245832059,\"y\":20.3},{\"x\":1685246132107,\"y\":20.3},{\"x\":1685246432206,\"y\":20.3},{\"x\":1685246732334,\"y\":20.3},{\"x\":1685247032445,\"y\":20.3},{\"x\":1685247332472,\"y\":20.2},{\"x\":1685247632538,\"y\":20.2},{\"x\":1685247932668,\"y\":20.2},{\"x\":1685248232731,\"y\":20.2},{\"x\":1685248532806,\"y\":20.2},{\"x\":1685248832930,\"y\":20.2},{\"x\":1685249133015,\"y\":20.2},{\"x\":1685249433093,\"y\":20.2},{\"x\":1685249733189,\"y\":20.2},{\"x\":1685250033291,\"y\":20.1},{\"x\":1685250333378,\"y\":20},{\"x\":1685250633446,\"y\":20},{\"x\":1685250933493,\"y\":20},{\"x\":1685251233569,\"y\":20},{\"x\":1685251533627,\"y\":20},{\"x\":1685251833774,\"y\":20.1},{\"x\":1685252133852,\"y\":20.2},{\"x\":1685252433955,\"y\":20.2},{\"x\":1685252734091,\"y\":20.2},{\"x\":1685253034179,\"y\":20},{\"x\":1685253334257,\"y\":19.9},{\"x\":1685253634325,\"y\":20},{\"x\":1685253934367,\"y\":20.2},{\"x\":1685254234489,\"y\":20.2},{\"x\":1685254534587,\"y\":20.2},{\"x\":1685254834680,\"y\":20.2},{\"x\":1685255134805,\"y\":20.3},{\"x\":1685255434885,\"y\":20.3},{\"x\":1685255735002,\"y\":20.3},{\"x\":1685256035085,\"y\":20.5},{\"x\":1685256335179,\"y\":20.5},{\"x\":1685256635254,\"y\":20.3},{\"x\":1685256935309,\"y\":20.3},{\"x\":1685257235338,\"y\":20.3},{\"x\":1685257535440,\"y\":20.3},{\"x\":1685257835520,\"y\":20.4},{\"x\":1685258135635,\"y\":20.5},{\"x\":1685258435709,\"y\":20.5},{\"x\":1685258735790,\"y\":20.5},{\"x\":1685259035904,\"y\":20.5},{\"x\":1685259335965,\"y\":20.4},{\"x\":1685259636005,\"y\":20.4},{\"x\":1685259936052,\"y\":20.4},{\"x\":1685260236148,\"y\":20.5},{\"x\":1685260536249,\"y\":20.4},{\"x\":1685260836320,\"y\":20.5},{\"x\":1685261136489,\"y\":20.5},{\"x\":1685261436489,\"y\":20.6},{\"x\":1685261736607,\"y\":20.5},{\"x\":1685262036710,\"y\":20.5},{\"x\":1685262336806,\"y\":20.4},{\"x\":1685262636943,\"y\":20.4},{\"x\":1685262936991,\"y\":20.3},{\"x\":1685263237019,\"y\":20.3},{\"x\":1685263537113,\"y\":20.3},{\"x\":1685263837223,\"y\":20.3},{\"x\":1685264137303,\"y\":20.3},{\"x\":1685264437449,\"y\":20.4},{\"x\":1685264737520,\"y\":20.3},{\"x\":1685265037596,\"y\":20.4},{\"x\":1685265337653,\"y\":20.3},{\"x\":1685265637725,\"y\":20.3},{\"x\":1685265937773,\"y\":20.3},{\"x\":1685266237884,\"y\":20.3},{\"x\":1685266537917,\"y\":20.3},{\"x\":1685266837940,\"y\":20.3},{\"x\":1685267138006,\"y\":20.3},{\"x\":1685267438089,\"y\":20.3},{\"x\":1685267738177,\"y\":20.3},{\"x\":1685268038239,\"y\":20.4},{\"x\":1685268338288,\"y\":20.3},{\"x\":1685268638340,\"y\":20.4},{\"x\":1685268938413,\"y\":20.3},{\"x\":1685269238479,\"y\":20.4},{\"x\":1685269538567,\"y\":20.4},{\"x\":1685269838641,\"y\":20.4},{\"x\":1685270138681,\"y\":20.3},{\"x\":1685270438761,\"y\":20.3},{\"x\":1685270738813,\"y\":20.3},{\"x\":1685271038911,\"y\":20.3},{\"x\":1685271338993,\"y\":20},{\"x\":1685271638994,\"y\":20},{\"x\":1685271939107,\"y\":20.2},{\"x\":1685272239155,\"y\":20.3},{\"x\":1685272539230,\"y\":20.3},{\"x\":1685272839299,\"y\":20.3},{\"x\":1685273139339,\"y\":20.3},{\"x\":1685273439382,\"y\":20.2},{\"x\":1685273739450,\"y\":19.9},{\"x\":1685274039516,\"y\":19.9},{\"x\":1685274339570,\"y\":20.2},{\"x\":1685274639651,\"y\":20.3},{\"x\":1685274939715,\"y\":20.5},{\"x\":1685275239718,\"y\":20.5},{\"x\":1685275539821,\"y\":20.6},{\"x\":1685275839883,\"y\":20.5},{\"x\":1685276139936,\"y\":20.3},{\"x\":1685276439971,\"y\":20.3},{\"x\":1685276740019,\"y\":20.3},{\"x\":1685277040061,\"y\":20.2},{\"x\":1685277340154,\"y\":20},{\"x\":1685277640177,\"y\":19.9},{\"x\":1685277940231,\"y\":20},{\"x\":1685278240249,\"y\":20},{\"x\":1685278540299,\"y\":19.9},{\"x\":1685278840376,\"y\":19.9},{\"x\":1685279140399,\"y\":19.5},{\"x\":1685279440422,\"y\":19.7},{\"x\":1685279740489,\"y\":19.8},{\"x\":1685280040542,\"y\":19.8},{\"x\":1685280340547,\"y\":19.5},{\"x\":1685280640612,\"y\":19.5},{\"x\":1685280940692,\"y\":19.5},{\"x\":1685280995978,\"y\":19.5},{\"x\":1685281186232,\"y\":19.5},{\"x\":1685281372604,\"y\":19.5},{\"x\":1685281687601,\"y\":19.5},{\"x\":1685281710226,\"y\":19.5},{\"x\":1685281749591,\"y\":19.5},{\"x\":1685281762017,\"y\":19.5},{\"x\":1685281799605,\"y\":19.5},{\"x\":1685282023556,\"y\":19.5},{\"x\":1685282044430,\"y\":19.5},{\"x\":1685282166915,\"y\":19.5},{\"x\":1685282208131,\"y\":19.5},{\"x\":1685282217923,\"y\":19.5},{\"x\":1685282260379,\"y\":19.5},{\"x\":1685282339652,\"y\":19.5},{\"x\":1685282364558,\"y\":19.5},{\"x\":1685282412077,\"y\":19.5},{\"x\":1685282588188,\"y\":19.5},{\"x\":1685282614967,\"y\":19.5},{\"x\":1685282852605,\"y\":19.5},{\"x\":1685282960718,\"y\":19.5},{\"x\":1685282980565,\"y\":19.5},{\"x\":1685283037810,\"y\":19.5},{\"x\":1685283067001,\"y\":19.5},{\"x\":1685283122144,\"y\":19.5},{\"x\":1685283272482,\"y\":19.5},{\"x\":1685283343031,\"y\":19.5},{\"x\":1685283445211,\"y\":19.5},{\"x\":1685283473050,\"y\":19.5},{\"x\":1685283503945,\"y\":19.4},{\"x\":1685283559122,\"y\":19.5},{\"x\":1685283661335,\"y\":19.5},{\"x\":1685283679196,\"y\":19.5},{\"x\":1685283753141,\"y\":19.5},{\"x\":1685283789923,\"y\":19.4},{\"x\":1685283796674,\"y\":19.4},{\"x\":1685284111596,\"y\":19.5},{\"x\":1685284411624,\"y\":19.5},{\"x\":1685284726634,\"y\":20},{\"x\":1685285041633,\"y\":19.9},{\"x\":1685285341650,\"y\":19.6},{\"x\":1685285641691,\"y\":19.8},{\"x\":1685285941711,\"y\":20},{\"x\":1685286241759,\"y\":20},{\"x\":1685286541841,\"y\":19.7},{\"x\":1685286856821,\"y\":20},{\"x\":1685287156833,\"y\":20.3},{\"x\":1685287456865,\"y\":20.4},{\"x\":1685287756902,\"y\":20.3},{\"x\":1685288056933,\"y\":20.3},{\"x\":1685288357007,\"y\":20.3},{\"x\":1685288671988,\"y\":20.1},{\"x\":1685288972004,\"y\":20.2},{\"x\":1685289272052,\"y\":20},{\"x\":1685289572077,\"y\":20},{\"x\":1685289872089,\"y\":20},{\"x\":1685290172161,\"y\":19.9},{\"x\":1685290487147,\"y\":20},{\"x\":1685290787163,\"y\":20},{\"x\":1685291087218,\"y\":19.9},{\"x\":1685291387247,\"y\":19.9},{\"x\":1685291687274,\"y\":20},{\"x\":1685291987283,\"y\":20.2},{\"x\":1685292287334,\"y\":20},{\"x\":1685292587380,\"y\":19.9},{\"x\":1685292887389,\"y\":19.9},{\"x\":1685293187427,\"y\":19.8},{\"x\":1685293502456,\"y\":19.7},{\"x\":1685293817480,\"y\":19.5},{\"x\":1685294117492,\"y\":19.6},{\"x\":1685294417507,\"y\":19.9},{\"x\":1685294717548,\"y\":20},{\"x\":1685295017585,\"y\":19.9},{\"x\":1685295317622,\"y\":19.6},{\"x\":1685295617633,\"y\":20},{\"x\":1685295917642,\"y\":19.7},{\"x\":1685296217701,\"y\":19.9},{\"x\":1685296532704,\"y\":19.5},{\"x\":1685296832750,\"y\":19.5},{\"x\":1685297132754,\"y\":19.5},{\"x\":1685297432771,\"y\":19.5},{\"x\":1685297732796,\"y\":19.5},{\"x\":1685298032828,\"y\":19.5},{\"x\":1685298332882,\"y\":19.5},{\"x\":1685298647889,\"y\":19.5},{\"x\":1685298947921,\"y\":19.5},{\"x\":1685299247969,\"y\":19.5},{\"x\":1685299547997,\"y\":19.5},{\"x\":1685299848026,\"y\":19.5},{\"x\":1685300163050,\"y\":19.5},{\"x\":1685300463065,\"y\":19.5},{\"x\":1685300763075,\"y\":19.5},{\"x\":1685301063109,\"y\":19.5},{\"x\":1685301363164,\"y\":19.5},{\"x\":1685301678208,\"y\":19.5},{\"x\":1685301993216,\"y\":19.5},{\"x\":1685302293269,\"y\":19.5},{\"x\":1685302593314,\"y\":19.5},{\"x\":1685302908310,\"y\":19.5},{\"x\":1685303208329,\"y\":19.5},{\"x\":1685303508359,\"y\":19.5},{\"x\":1685303808369,\"y\":19.5},{\"x\":1685304108395,\"y\":19.5},{\"x\":1685304408424,\"y\":19.3},{\"x\":1685304708455,\"y\":19.4},{\"x\":1685305008461,\"y\":19.4},{\"x\":1685305308490,\"y\":19.4},{\"x\":1685305608530,\"y\":19.5},{\"x\":1685305908539,\"y\":19.5},{\"x\":1685306208562,\"y\":19.4},{\"x\":1685306508616,\"y\":19.5},{\"x\":1685306823616,\"y\":19.5},{\"x\":1685307123649,\"y\":19.5},{\"x\":1685307423661,\"y\":19.5},{\"x\":1685307723699,\"y\":19.5},{\"x\":1685308023733,\"y\":19.5},{\"x\":1685308323779,\"y\":19.5},{\"x\":1685308638772,\"y\":19.5},{\"x\":1685308938793,\"y\":19.5},{\"x\":1685309238818,\"y\":19.5},{\"x\":1685309538833,\"y\":19.5},{\"x\":1685309838872,\"y\":19.5},{\"x\":1685310138910,\"y\":19.5},{\"x\":1685310453907,\"y\":19.5},{\"x\":1685310768906,\"y\":19.5},{\"x\":1685311068915,\"y\":19.5},{\"x\":1685311368965,\"y\":19.5},{\"x\":1685311668966,\"y\":19.5},{\"x\":1685311968998,\"y\":19.5},{\"x\":1685312269016,\"y\":19.5},{\"x\":1685312569034,\"y\":19.5},{\"x\":1685312869061,\"y\":19.5},{\"x\":1685313169072,\"y\":19.5},{\"x\":1685313484093,\"y\":19.5},{\"x\":1685313784097,\"y\":19.6},{\"x\":1685314084113,\"y\":19.5},{\"x\":1685314384134,\"y\":19.7},{\"x\":1685314684165,\"y\":19.9},{\"x\":1685314984191,\"y\":19.9},{\"x\":1685315299157,\"y\":19.9},{\"x\":1685315599168,\"y\":20},{\"x\":1685315899192,\"y\":19.9},{\"x\":1685316199221,\"y\":20.1},{\"x\":1685316514243,\"y\":20},{\"x\":1685316814264,\"y\":20.2},{\"x\":1685317129218,\"y\":20.2},{\"x\":1685317444217,\"y\":20.2},{\"x\":1685317744298,\"y\":20.3},{\"x\":1685318059259,\"y\":20.3},{\"x\":1685318359275,\"y\":20.3},{\"x\":1685318659355,\"y\":20.3},{\"x\":1685318959404,\"y\":20.3},{\"x\":1685319259419,\"y\":20.2},{\"x\":1685319559443,\"y\":20.3},{\"x\":1685319859535,\"y\":20.3},{\"x\":1685320159597,\"y\":20.3},{\"x\":1685320459678,\"y\":20.3},{\"x\":1685320759851,\"y\":20.3},{\"x\":1685321074857,\"y\":20.2},{\"x\":1685321374890,\"y\":20.3},{\"x\":1685321674930,\"y\":20.3},{\"x\":1685321974980,\"y\":20.3},{\"x\":1685322274993,\"y\":20.3},{\"x\":1685322575024,\"y\":20.3},{\"x\":1685322875122,\"y\":20.3},{\"x\":1685323175196,\"y\":20.3},{\"x\":1685323475244,\"y\":20.3},{\"x\":1685323775309,\"y\":20.4},{\"x\":1685324075375,\"y\":20.3},{\"x\":1685324375402,\"y\":20.4},{\"x\":1685324675425,\"y\":20.5},{\"x\":1685324975485,\"y\":20.5},{\"x\":1685325275523,\"y\":20.5},{\"x\":1685325575582,\"y\":20.5},{\"x\":1685325875647,\"y\":20.5},{\"x\":1685326175713,\"y\":20.5},{\"x\":1685326475768,\"y\":20.4},{\"x\":1685326775825,\"y\":20.4},{\"x\":1685327075908,\"y\":20.4},{\"x\":1685327376002,\"y\":20.4},{\"x\":1685327676015,\"y\":20.4},{\"x\":1685327976059,\"y\":20.4},{\"x\":1685328276184,\"y\":20.4},{\"x\":1685328591156,\"y\":20.4},{\"x\":1685328891332,\"y\":20.4},{\"x\":1685329206306,\"y\":20.4},{\"x\":1685329506397,\"y\":20.4},{\"x\":1685329806440,\"y\":20.3},{\"x\":1685330106549,\"y\":20.3},{\"x\":1685330406587,\"y\":20.3},{\"x\":1685330706687,\"y\":20.2},{\"x\":1685331006741,\"y\":20.3},{\"x\":1685331306759,\"y\":20.3},{\"x\":1685331606822,\"y\":20.2},{\"x\":1685331906872,\"y\":20.2},{\"x\":1685332206974,\"y\":20.2},{\"x\":1685332507043,\"y\":20.3},{\"x\":1685332807216,\"y\":20.2},{\"x\":1685333122210,\"y\":20.2},{\"x\":1685333422298,\"y\":20.2},{\"x\":1685333722396,\"y\":20.2},{\"x\":1685334022464,\"y\":20.2},{\"x\":1685334322536,\"y\":20.2},{\"x\":1685334622591,\"y\":20.2},{\"x\":1685334922644,\"y\":20.2},{\"x\":1685335222714,\"y\":20.1},{\"x\":1685335522841,\"y\":20},{\"x\":1685335822919,\"y\":20},{\"x\":1685336122954,\"y\":20},{\"x\":1685336423087,\"y\":19.9},{\"x\":1685336723138,\"y\":20},{\"x\":1685337023185,\"y\":19.9},{\"x\":1685337323274,\"y\":19.9},{\"x\":1685337623330,\"y\":19.9},{\"x\":1685337923414,\"y\":20},{\"x\":1685338223520,\"y\":19.9},{\"x\":1685338523616,\"y\":20},{\"x\":1685338823724,\"y\":19.9},{\"x\":1685339123797,\"y\":20},{\"x\":1685339423870,\"y\":19.9},{\"x\":1685339723950,\"y\":19.9},{\"x\":1685340024030,\"y\":19.9},{\"x\":1685340324090,\"y\":20},{\"x\":1685340624115,\"y\":20},{\"x\":1685340924164,\"y\":20},{\"x\":1685341224265,\"y\":20},{\"x\":1685341524353,\"y\":20},{\"x\":1685341824453,\"y\":20},{\"x\":1685342124531,\"y\":20.2},{\"x\":1685342424622,\"y\":19.9},{\"x\":1685342724753,\"y\":20},{\"x\":1685343024799,\"y\":20.2},{\"x\":1685343324830,\"y\":20},{\"x\":1685343624890,\"y\":19.9},{\"x\":1685343924971,\"y\":20.2},{\"x\":1685344225070,\"y\":20.2},{\"x\":1685344525149,\"y\":20.3},{\"x\":1685344825216,\"y\":20.2},{\"x\":1685345125344,\"y\":20.2},{\"x\":1685345425362,\"y\":20.3},{\"x\":1685345725441,\"y\":20.3},{\"x\":1685346025536,\"y\":20.3},{\"x\":1685346325591,\"y\":20},{\"x\":1685346625679,\"y\":20.2},{\"x\":1685346925731,\"y\":20.2},{\"x\":1685347225748,\"y\":20.3},{\"x\":1685347525839,\"y\":20.3},{\"x\":1685347825931,\"y\":20.3},{\"x\":1685348126031,\"y\":20.3},{\"x\":1685348426089,\"y\":20.3},{\"x\":1685348726163,\"y\":20.3},{\"x\":1685349026225,\"y\":20.3},{\"x\":1685349326308,\"y\":20.3},{\"x\":1685349626392,\"y\":20},{\"x\":1685349926466,\"y\":20},{\"x\":1685350226553,\"y\":20.2},{\"x\":1685350526651,\"y\":19.9},{\"x\":1685350826811,\"y\":20},{\"x\":1685351141763,\"y\":19.9},{\"x\":1685351441827,\"y\":20.2},{\"x\":1685351741927,\"y\":20.2},{\"x\":1685352042064,\"y\":20.1},{\"x\":1685352342100,\"y\":20.3},{\"x\":1685352642170,\"y\":20},{\"x\":1685352942245,\"y\":19.9},{\"x\":1685353242314,\"y\":19.9},{\"x\":1685353542387,\"y\":20},{\"x\":1685353842461,\"y\":20},{\"x\":1685354142491,\"y\":20},{\"x\":1685354442557,\"y\":19.9},{\"x\":1685354742628,\"y\":20},{\"x\":1685355042729,\"y\":20.3},{\"x\":1685355342838,\"y\":20.2},{\"x\":1685355642921,\"y\":20.3},{\"x\":1685355942977,\"y\":20},{\"x\":1685356243149,\"y\":19.9},{\"x\":1685356558148,\"y\":19.5},{\"x\":1685356858211,\"y\":19.9},{\"x\":1685357158249,\"y\":20},{\"x\":1685357458282,\"y\":19.6},{\"x\":1685357758424,\"y\":20},{\"x\":1685358058471,\"y\":19.5},{\"x\":1685358358569,\"y\":19.5},{\"x\":1685358658635,\"y\":20},{\"x\":1685358958716,\"y\":20.1},{\"x\":1685359258767,\"y\":20},{\"x\":1685359558873,\"y\":19.9},{\"x\":1685359858954,\"y\":20.2},{\"x\":1685360159014,\"y\":20.2},{\"x\":1685360459077,\"y\":20},{\"x\":1685360759117,\"y\":19.9},{\"x\":1685361059194,\"y\":19.8},{\"x\":1685361359285,\"y\":20},{\"x\":1685361659377,\"y\":20},{\"x\":1685361959454,\"y\":20.1},{\"x\":1685362259508,\"y\":20.2},{\"x\":1685362559586,\"y\":19.9},{\"x\":1685362859674,\"y\":20},{\"x\":1685363159756,\"y\":20},{\"x\":1685363459810,\"y\":19.5},{\"x\":1685363759869,\"y\":19.6},{\"x\":1685364059953,\"y\":19.5},{\"x\":1685364360040,\"y\":19.9},{\"x\":1685364660157,\"y\":19.9},{\"x\":1685364960261,\"y\":19.9},{\"x\":1685365260272,\"y\":19.9},{\"x\":1685365560367,\"y\":20},{\"x\":1685365860483,\"y\":19.9},{\"x\":1685366160569,\"y\":19.9},{\"x\":1685366460633,\"y\":19.5},{\"x\":1685366760670,\"y\":19.5},{\"x\":1685367060781,\"y\":19.5},{\"x\":1685367360928,\"y\":19.5},{\"x\":1685367660996,\"y\":19.5},{\"x\":1685367961125,\"y\":19.5},{\"x\":1685368261267,\"y\":19.5},{\"x\":1685368561326,\"y\":19.5},{\"x\":1685368861443,\"y\":19.5},{\"x\":1685369161528,\"y\":19.5},{\"x\":1685369461571,\"y\":19.5},{\"x\":1685369761618,\"y\":19.5},{\"x\":1685370061811,\"y\":19.5},{\"x\":1685370361876,\"y\":19.5},{\"x\":1685370661975,\"y\":19.5},{\"x\":1685370962096,\"y\":19.5},{\"x\":1685371262161,\"y\":19.5},{\"x\":1685371562287,\"y\":19.5},{\"x\":1685371862386,\"y\":19.5},{\"x\":1685372162471,\"y\":19.5},{\"x\":1685372462563,\"y\":19.4},{\"x\":1685372762583,\"y\":19.2},{\"x\":1685373062692,\"y\":19.2},{\"x\":1685373362813,\"y\":19.1},{\"x\":1685373662925,\"y\":19.3},{\"x\":1685373963012,\"y\":19.2},{\"x\":1685374263073,\"y\":19.2},{\"x\":1685374563193,\"y\":19},{\"x\":1685374863306,\"y\":19.2},{\"x\":1685375163412,\"y\":19.3},{\"x\":1685375463446,\"y\":19.4},{\"x\":1685375763533,\"y\":19.4},{\"x\":1685376063611,\"y\":19.2},{\"x\":1685376363690,\"y\":19.1},{\"x\":1685376663795,\"y\":19},{\"x\":1685376963909,\"y\":19.2},{\"x\":1685377263998,\"y\":19.3},{\"x\":1685377564112,\"y\":19.1},{\"x\":1685377864184,\"y\":19.2},{\"x\":1685378164260,\"y\":19.1},{\"x\":1685378464309,\"y\":19},{\"x\":1685378764426,\"y\":19},{\"x\":1685379064528,\"y\":19},{\"x\":1685379364568,\"y\":19.2},{\"x\":1685379664627,\"y\":19.1},{\"x\":1685379964711,\"y\":19},{\"x\":1685380264778,\"y\":19.1},{\"x\":1685380564838,\"y\":19.2},{\"x\":1685380864899,\"y\":19},{\"x\":1685381164922,\"y\":19},{\"x\":1685381464995,\"y\":18.9},{\"x\":1685381765085,\"y\":18.9},{\"x\":1685382065145,\"y\":18.9},{\"x\":1685382365238,\"y\":18.9},{\"x\":1685382665304,\"y\":18.9},{\"x\":1685382965394,\"y\":19},{\"x\":1685383265458,\"y\":19},{\"x\":1685383565523,\"y\":18.9},{\"x\":1685383865573,\"y\":18.9},{\"x\":1685384165635,\"y\":18.7},{\"x\":1685384465670,\"y\":18.7},{\"x\":1685384765740,\"y\":18.7},{\"x\":1685385065812,\"y\":18.9},{\"x\":1685385365909,\"y\":18.8},{\"x\":1685385665940,\"y\":18.7},{\"x\":1685385966015,\"y\":18.9},{\"x\":1685386266095,\"y\":18.9},{\"x\":1685386566159,\"y\":18.8},{\"x\":1685386866204,\"y\":18.5},{\"x\":1685387166270,\"y\":18.5},{\"x\":1685387466325,\"y\":18.7},{\"x\":1685387766371,\"y\":18.5},{\"x\":1685388066417,\"y\":18.7},{\"x\":1685388366441,\"y\":18.7},{\"x\":1685388666495,\"y\":18.7},{\"x\":1685388966561,\"y\":18.4},{\"x\":1685389266600,\"y\":18.4},{\"x\":1685389566607,\"y\":18.4},{\"x\":1685389866660,\"y\":18.4},{\"x\":1685390166677,\"y\":18.5},{\"x\":1685390466746,\"y\":18.5},{\"x\":1685390781720,\"y\":18.6},{\"x\":1685391081770,\"y\":18.6},{\"x\":1685391381808,\"y\":18.4},{\"x\":1685391681836,\"y\":18.4},{\"x\":1685391981922,\"y\":18.4},{\"x\":1685392281964,\"y\":18.4},{\"x\":1685392582032,\"y\":18.4},{\"x\":1685392897029,\"y\":18.4},{\"x\":1685393197037,\"y\":18.4},{\"x\":1685393497068,\"y\":18.4},{\"x\":1685393797107,\"y\":18.4},{\"x\":1685394097146,\"y\":18.4},{\"x\":1685394397195,\"y\":18.4},{\"x\":1685394697240,\"y\":18.4},{\"x\":1685394997269,\"y\":18.4},{\"x\":1685395297315,\"y\":18.4},{\"x\":1685395597344,\"y\":18.4},{\"x\":1685395897378,\"y\":18.4},{\"x\":1685396197391,\"y\":18.4},{\"x\":1685396497411,\"y\":18.4},{\"x\":1685396797418,\"y\":18.3},{\"x\":1685397097514,\"y\":18.4},{\"x\":1685397397526,\"y\":18.2},{\"x\":1685397697553,\"y\":18.1},{\"x\":1685397997603,\"y\":18.1},{\"x\":1685398297622,\"y\":18.1},{\"x\":1685398597700,\"y\":18.1},{\"x\":1685398897737,\"y\":18.2},{\"x\":1685399212765,\"y\":18.2},{\"x\":1685399527768,\"y\":18},{\"x\":1685399827776,\"y\":18.1},{\"x\":1685400127841,\"y\":17.9},{\"x\":1685400442841,\"y\":18},{\"x\":1685400742935,\"y\":17.9},{\"x\":1685401057950,\"y\":18},{\"x\":1685401357960,\"y\":18},{\"x\":1685401657984,\"y\":18.1},{\"x\":1685401957995,\"y\":18},{\"x\":1685402258067,\"y\":18.1},{\"x\":1685402573081,\"y\":18},{\"x\":1685402873096,\"y\":17.9},{\"x\":1685403173117,\"y\":17.9},{\"x\":1685403473129,\"y\":17.9},{\"x\":1685403773148,\"y\":17.9},{\"x\":1685404073175,\"y\":18},{\"x\":1685404373229,\"y\":17.9},{\"x\":1685404688249,\"y\":17.9},{\"x\":1685404988317,\"y\":17.9},{\"x\":1685405288409,\"y\":17.9},{\"x\":1685405588488,\"y\":17.9},{\"x\":1685405888522,\"y\":17.9},{\"x\":1685406188569,\"y\":17.9},{\"x\":1685406488623,\"y\":17.9},{\"x\":1685406788688,\"y\":17.9},{\"x\":1685407088721,\"y\":17.9},{\"x\":1685407388765,\"y\":17.9},{\"x\":1685407688838,\"y\":17.9},{\"x\":1685407988885,\"y\":17.9},{\"x\":1685408288915,\"y\":17.9},{\"x\":1685408588993,\"y\":17.9},{\"x\":1685408889074,\"y\":17.9},{\"x\":1685409189147,\"y\":17.9},{\"x\":1685409489206,\"y\":17.9},{\"x\":1685409789249,\"y\":17.9},{\"x\":1685410089310,\"y\":17.9},{\"x\":1685410389345,\"y\":17.9},{\"x\":1685410704354,\"y\":17.9},{\"x\":1685411004452,\"y\":17.9},{\"x\":1685411304516,\"y\":17.9},{\"x\":1685411604552,\"y\":17.9},{\"x\":1685411904625,\"y\":17.8},{\"x\":1685412204887,\"y\":17.8},{\"x\":1685412519762,\"y\":17.7},{\"x\":1685412819867,\"y\":17.7},{\"x\":1685413119886,\"y\":17.8},{\"x\":1685413419976,\"y\":17.7},{\"x\":1685413719986,\"y\":17.7},{\"x\":1685414020028,\"y\":17.7},{\"x\":1685414320085,\"y\":17.7},{\"x\":1685414620140,\"y\":17.7},{\"x\":1685414920229,\"y\":17.7},{\"x\":1685415220270,\"y\":17.7},{\"x\":1685415520407,\"y\":17.7},{\"x\":1685415820425,\"y\":17.7},{\"x\":1685416120509,\"y\":17.7},{\"x\":1685416420519,\"y\":17.7},{\"x\":1685416720605,\"y\":17.7},{\"x\":1685417020626,\"y\":17.7},{\"x\":1685417320672,\"y\":17.7},{\"x\":1685417620774,\"y\":17.7},{\"x\":1685417920843,\"y\":17.7},{\"x\":1685418220900,\"y\":17.8},{\"x\":1685418520971,\"y\":17.7},{\"x\":1685418821022,\"y\":17.7},{\"x\":1685419121073,\"y\":17.8},{\"x\":1685419421171,\"y\":17.9},{\"x\":1685419721181,\"y\":17.7},{\"x\":1685420021225,\"y\":17.7},{\"x\":1685420321304,\"y\":17.6},{\"x\":1685420621355,\"y\":17.7},{\"x\":1685420921447,\"y\":17.7},{\"x\":1685421221522,\"y\":17.7},{\"x\":1685421521582,\"y\":17.7},{\"x\":1685421821663,\"y\":17.7},{\"x\":1685422121736,\"y\":17.6},{\"x\":1685422421818,\"y\":17.7},{\"x\":1685422721865,\"y\":17.7},{\"x\":1685423021869,\"y\":17.6},{\"x\":1685423321956,\"y\":17.4},{\"x\":1685423622015,\"y\":17.5},{\"x\":1685423922080,\"y\":17.4},{\"x\":1685424222162,\"y\":17.5},{\"x\":1685424522203,\"y\":17.5},{\"x\":1685424822288,\"y\":17.5},{\"x\":1685425122362,\"y\":17.4},{\"x\":1685425422438,\"y\":17.4},{\"x\":1685425722513,\"y\":17.4},{\"x\":1685426022547,\"y\":17.4},{\"x\":1685426322620,\"y\":17.5},{\"x\":1685426622723,\"y\":17.5},{\"x\":1685426922744,\"y\":17.4},{\"x\":1685427222854,\"y\":17.4},{\"x\":1685427522913,\"y\":17.4},{\"x\":1685427823002,\"y\":17.4},{\"x\":1685428123104,\"y\":17.5},{\"x\":1685428423173,\"y\":17.5},{\"x\":1685428723329,\"y\":17.4},{\"x\":1685429038338,\"y\":17.4},{\"x\":1685429338390,\"y\":17.4},{\"x\":1685429638534,\"y\":17.4},{\"x\":1685429938550,\"y\":17.4},{\"x\":1685430238655,\"y\":17.4},{\"x\":1685430538750,\"y\":17.4},{\"x\":1685430838822,\"y\":17.3},{\"x\":1685431139008,\"y\":17.5},{\"x\":1685431439050,\"y\":17.5},{\"x\":1685431739109,\"y\":17.4},{\"x\":1685432039156,\"y\":17.5},{\"x\":1685432339204,\"y\":17.7},{\"x\":1685432639299,\"y\":17.3},{\"x\":1685432939394,\"y\":17.2},{\"x\":1685433239499,\"y\":17.2},{\"x\":1685433539575,\"y\":17.2},{\"x\":1685433839700,\"y\":17.2},{\"x\":1685434139771,\"y\":16.9},{\"x\":1685434439873,\"y\":16.7},{\"x\":1685434739965,\"y\":16.5},{\"x\":1685435040038,\"y\":16.5},{\"x\":1685435340054,\"y\":16.5},{\"x\":1685435640133,\"y\":16.5},{\"x\":1685435940205,\"y\":16.5},{\"x\":1685436240311,\"y\":16.8},{\"x\":1685436540423,\"y\":16.9},{\"x\":1685436840548,\"y\":17.1},{\"x\":1685437140665,\"y\":17},{\"x\":1685437440701,\"y\":16.8},{\"x\":1685437740799,\"y\":16.5},{\"x\":1685438040887,\"y\":16.5},{\"x\":1685438340964,\"y\":16.5},{\"x\":1685438641030,\"y\":17.2},{\"x\":1685438941063,\"y\":17.2},{\"x\":1685439241150,\"y\":17.2},{\"x\":1685439541265,\"y\":17},{\"x\":1685439841366,\"y\":17.2},{\"x\":1685440141451,\"y\":16.5},{\"x\":1685440441506,\"y\":16.5},{\"x\":1685440741607,\"y\":16.5},{\"x\":1685441041684,\"y\":16.5},{\"x\":1685441341786,\"y\":16.5},{\"x\":1685441641830,\"y\":16.5},{\"x\":1685441941863,\"y\":16.5},{\"x\":1685442241939,\"y\":17},{\"x\":1685442542010,\"y\":17.3},{\"x\":1685442842108,\"y\":17.2},{\"x\":1685443142199,\"y\":16.6},{\"x\":1685443442319,\"y\":17.2},{\"x\":1685443742376,\"y\":16.5},{\"x\":1685444042480,\"y\":16.8},{\"x\":1685444342574,\"y\":16.8},{\"x\":1685444642649,\"y\":16.5},{\"x\":1685444942748,\"y\":17.2},{\"x\":1685445242792,\"y\":17.1},{\"x\":1685445542799,\"y\":17.2},{\"x\":1685445842907,\"y\":17.3},{\"x\":1685446143003,\"y\":17.4},{\"x\":1685446443122,\"y\":17.4},{\"x\":1685446743194,\"y\":17.3},{\"x\":1685447043298,\"y\":17.2},{\"x\":1685447343341,\"y\":17.4},{\"x\":1685447643442,\"y\":17.2},{\"x\":1685447943557,\"y\":17.2},{\"x\":1685448243632,\"y\":17.3},{\"x\":1685448543708,\"y\":17.2},{\"x\":1685448843747,\"y\":17.1},{\"x\":1685449143768,\"y\":16.6},{\"x\":1685449443878,\"y\":16.5},{\"x\":1685449743965,\"y\":16.5},{\"x\":1685450044053,\"y\":16.5},{\"x\":1685450344178,\"y\":17.2},{\"x\":1685450644263,\"y\":17.3},{\"x\":1685450944279,\"y\":17.4},{\"x\":1685451244382,\"y\":17.2},{\"x\":1685451544448,\"y\":17.3},{\"x\":1685451844600,\"y\":17.3},{\"x\":1685452144626,\"y\":17.4},{\"x\":1685452444701,\"y\":17.4},{\"x\":1685452744749,\"y\":17.2},{\"x\":1685453044818,\"y\":17.4},{\"x\":1685453344909,\"y\":17.7},{\"x\":1685453645071,\"y\":17.6},{\"x\":1685453945113,\"y\":17.5},{\"x\":1685454245205,\"y\":17.6},{\"x\":1685454545276,\"y\":17.6},{\"x\":1685454845390,\"y\":17.7},{\"x\":1685455145455,\"y\":17.8},{\"x\":1685455445552,\"y\":17.7},{\"x\":1685455745631,\"y\":17.7},{\"x\":1685456045706,\"y\":17.7},{\"x\":1685456345813,\"y\":17.6},{\"x\":1685456645826,\"y\":17.7},{\"x\":1685456945928,\"y\":17.7},{\"x\":1685457246034,\"y\":17.4},{\"x\":1685457546104,\"y\":17.4},{\"x\":1685457846188,\"y\":17.6},{\"x\":1685458146263,\"y\":17.9},{\"x\":1685458446341,\"y\":17.9},{\"x\":1685458746470,\"y\":17.9},{\"x\":1685459046554,\"y\":17.8},{\"x\":1685459346643,\"y\":17.7},{\"x\":1685459646708,\"y\":17.9},{\"x\":1685459946734,\"y\":18},{\"x\":1685460246833,\"y\":17.9},{\"x\":1685460546913,\"y\":17.9},{\"x\":1685460847002,\"y\":17.9},{\"x\":1685461147079,\"y\":17.9},{\"x\":1685461447115,\"y\":17.9},{\"x\":1685461747181,\"y\":17.9},{\"x\":1685462047303,\"y\":17.9},{\"x\":1685462347360,\"y\":17.9},{\"x\":1685462647457,\"y\":17.9},{\"x\":1685462947533,\"y\":17.9},{\"x\":1685463247622,\"y\":17.9},{\"x\":1685463437946,\"y\":17.9},{\"x\":1685463752894,\"y\":17.9},{\"x\":1685464067891,\"y\":17.9},{\"x\":1685464367932,\"y\":17.9},{\"x\":1685464668016,\"y\":18.2},{\"x\":1685464968034,\"y\":18.2},{\"x\":1685465268109,\"y\":17.9},{\"x\":1685465568127,\"y\":17.9},{\"x\":1685465868179,\"y\":18},{\"x\":1685466168200,\"y\":18},{\"x\":1685466468292,\"y\":17.9},{\"x\":1685466783247,\"y\":17.9},{\"x\":1685467083281,\"y\":18},{\"x\":1685467383324,\"y\":18},{\"x\":1685467683356,\"y\":17.9},{\"x\":1685467983393,\"y\":17.9},{\"x\":1685468283508,\"y\":18},{\"x\":1685468598507,\"y\":17.9},{\"x\":1685468898532,\"y\":17.9},{\"x\":1685469198568,\"y\":17.9},{\"x\":1685469498602,\"y\":17.9},{\"x\":1685469798635,\"y\":17.9},{\"x\":1685470098653,\"y\":17.8},{\"x\":1685470398701,\"y\":17.8},{\"x\":1685470698741,\"y\":17.8},{\"x\":1685470998770,\"y\":17.7},{\"x\":1685471298800,\"y\":17.8},{\"x\":1685471598821,\"y\":17.9},{\"x\":1685471898877,\"y\":17.7},{\"x\":1685472198886,\"y\":17.7},{\"x\":1685472498957,\"y\":17.7},{\"x\":1685472798992,\"y\":17.7},{\"x\":1685473099049,\"y\":17.9},{\"x\":1685473414059,\"y\":17.9},{\"x\":1685473714086,\"y\":17.7},{\"x\":1685474014164,\"y\":17.6},{\"x\":1685474314238,\"y\":17.4},{\"x\":1685474629191,\"y\":17.5},{\"x\":1685474929212,\"y\":17.6},{\"x\":1685475229261,\"y\":17.7},{\"x\":1685475529275,\"y\":17.4},{\"x\":1685475829340,\"y\":17.4},{\"x\":1685476129417,\"y\":17.4},{\"x\":1685476429433,\"y\":17.4},{\"x\":1685476729449,\"y\":17.3},{\"x\":1685477029470,\"y\":17.2},{\"x\":1685477329512,\"y\":17.2},{\"x\":1685477629544,\"y\":17.3},{\"x\":1685477929562,\"y\":17.4},{\"x\":1685478229615,\"y\":17.3},{\"x\":1685478529641,\"y\":17},{\"x\":1685478829667,\"y\":16.5},{\"x\":1685479129749,\"y\":16.5},{\"x\":1685479429791,\"y\":16.5},{\"x\":1685479729807,\"y\":16.5},{\"x\":1685480029843,\"y\":16.5},{\"x\":1685480329850,\"y\":16.5},{\"x\":1685480629899,\"y\":16.7},{\"x\":1685480929954,\"y\":16.5},{\"x\":1685481244998,\"y\":16.5},{\"x\":1685481545012,\"y\":16.5},{\"x\":1685481845043,\"y\":16.5},{\"x\":1685482145093,\"y\":16.5},{\"x\":1685482445120,\"y\":16.5},{\"x\":1685482745153,\"y\":16.5},{\"x\":1685483045222,\"y\":16.5},{\"x\":1685483345235,\"y\":16.5},{\"x\":1685483645258,\"y\":16.5},{\"x\":1685483945299,\"y\":16.5},{\"x\":1685484245340,\"y\":16.5},{\"x\":1685484545382,\"y\":16.5},{\"x\":1685484845385,\"y\":16.5},{\"x\":1685485145425,\"y\":16.5},{\"x\":1685485445459,\"y\":16.5},{\"x\":1685485745479,\"y\":16.5},{\"x\":1685486045546,\"y\":16.5},{\"x\":1685486345575,\"y\":16.5},{\"x\":1685486645633,\"y\":16.5},{\"x\":1685486945725,\"y\":16.5},{\"x\":1685487260693,\"y\":16.5},{\"x\":1685487560707,\"y\":16.5},{\"x\":1685487860814,\"y\":16.5},{\"x\":1685488175808,\"y\":16.5},{\"x\":1685488475825,\"y\":16.5},{\"x\":1685488775853,\"y\":16.5},{\"x\":1685489075914,\"y\":16.5},{\"x\":1685489375930,\"y\":16.5},{\"x\":1685489675982,\"y\":16.5},{\"x\":1685489976039,\"y\":16.5},{\"x\":1685490276048,\"y\":16.5},{\"x\":1685490576126,\"y\":16.5},{\"x\":1685490876150,\"y\":16.5},{\"x\":1685491176154,\"y\":16.5},{\"x\":1685491476262,\"y\":16.5},{\"x\":1685491791218,\"y\":16.5},{\"x\":1685492091237,\"y\":16.5},{\"x\":1685492391276,\"y\":16.5},{\"x\":1685492691341,\"y\":16.5},{\"x\":1685492991352,\"y\":16.5},{\"x\":1685493291416,\"y\":16.5},{\"x\":1685493591464,\"y\":16.5},{\"x\":1685493891476,\"y\":16.2},{\"x\":1685494191553,\"y\":16.5},{\"x\":1685494491555,\"y\":16.5},{\"x\":1685494806655,\"y\":16.5},{\"x\":1685495121636,\"y\":16.5},{\"x\":1685495436647,\"y\":16.5},{\"x\":1685495736703,\"y\":16.5},{\"x\":1685496036726,\"y\":16.4},{\"x\":1685496336779,\"y\":16.5},{\"x\":1685496636854,\"y\":16.4},{\"x\":1685496951873,\"y\":16.4},{\"x\":1685497251911,\"y\":16.5},{\"x\":1685497566911,\"y\":16.5},{\"x\":1685497866915,\"y\":16.5},{\"x\":1685498166972,\"y\":16.2},{\"x\":1685498466989,\"y\":16.2},{\"x\":1685498767043,\"y\":16.4},{\"x\":1685499067095,\"y\":16.2},{\"x\":1685499367129,\"y\":16.5},{\"x\":1685499667180,\"y\":16.4},{\"x\":1685499967184,\"y\":16.5},{\"x\":1685500282215,\"y\":16.5},{\"x\":1685500582236,\"y\":16.5},{\"x\":1685500882309,\"y\":16.5},{\"x\":1685501197336,\"y\":16.4},{\"x\":1685501497371,\"y\":16.2},{\"x\":1685501797405,\"y\":16.3},{\"x\":1685502097446,\"y\":16.1},{\"x\":1685502397471,\"y\":16},{\"x\":1685502697518,\"y\":15.7},{\"x\":1685502997526,\"y\":16},{\"x\":1685503297549,\"y\":16},{\"x\":1685503597589,\"y\":16.2},{\"x\":1685503897624,\"y\":16.2},{\"x\":1685504197687,\"y\":16},{\"x\":1685504497728,\"y\":15.9},{\"x\":1685504797753,\"y\":16},{\"x\":1685505097809,\"y\":16},{\"x\":1685505397837,\"y\":16.2},{\"x\":1685505697885,\"y\":16.4},{\"x\":1685505997899,\"y\":16.3},{\"x\":1685506297916,\"y\":16.1},{\"x\":1685506597958,\"y\":16},{\"x\":1685506897987,\"y\":16},{\"x\":1685507198004,\"y\":15.8},{\"x\":1685507498078,\"y\":15.9},{\"x\":1685507798110,\"y\":16},{\"x\":1685508098131,\"y\":15.8},{\"x\":1685508398152,\"y\":16},{\"x\":1685508698194,\"y\":15.9},{\"x\":1685508998213,\"y\":15.8},{\"x\":1685509298220,\"y\":15.8},{\"x\":1685509598290,\"y\":15.9},{\"x\":1685509898323,\"y\":15.7},{\"x\":1685510198331,\"y\":15.7},{\"x\":1685510498370,\"y\":15.7},{\"x\":1685510798393,\"y\":15.5},{\"x\":1685511098454,\"y\":15.8},{\"x\":1685511398491,\"y\":15.7},{\"x\":1685511698520,\"y\":15.9},{\"x\":1685512013528,\"y\":15.9},{\"x\":1685512313605,\"y\":15.8},{\"x\":1685512628564,\"y\":15.7},{\"x\":1685512928630,\"y\":15.5},{\"x\":1685513228650,\"y\":15.5},{\"x\":1685513528663,\"y\":15.5},{\"x\":1685513828703,\"y\":15.6},{\"x\":1685514143711,\"y\":15.7},{\"x\":1685514443729,\"y\":15.7},{\"x\":1685514743749,\"y\":15.8},{\"x\":1685515043937,\"y\":15.7},{\"x\":1685515358896,\"y\":15.5},{\"x\":1685515658956,\"y\":15.6},{\"x\":1685515959007,\"y\":15.7},{\"x\":1685516259087,\"y\":15.6},{\"x\":1685516559112,\"y\":15.9},{\"x\":1685516859194,\"y\":16},{\"x\":1685517174175,\"y\":15.9},{\"x\":1685517474229,\"y\":15.8},{\"x\":1685517774323,\"y\":15.7},{\"x\":1685518074366,\"y\":15.7},{\"x\":1685518374438,\"y\":15.7},{\"x\":1685518674561,\"y\":15.9},{\"x\":1685518974586,\"y\":15.7},{\"x\":1685519274602,\"y\":16},{\"x\":1685519574700,\"y\":15.7},{\"x\":1685519889719,\"y\":15.8},{\"x\":1685520189753,\"y\":15.9},{\"x\":1685520489831,\"y\":15.8},{\"x\":1685520789861,\"y\":15.9},{\"x\":1685521089913,\"y\":16},{\"x\":1685521390016,\"y\":15.7},{\"x\":1685521690069,\"y\":15.5},{\"x\":1685521990135,\"y\":15.5},{\"x\":1685522290216,\"y\":15.5},{\"x\":1685522590301,\"y\":15.5},{\"x\":1685522905260,\"y\":15.7},{\"x\":1685523205311,\"y\":15.7},{\"x\":1685523505402,\"y\":15.7},{\"x\":1685523805456,\"y\":15.6},{\"x\":1685524105543,\"y\":15.9},{\"x\":1685524405592,\"y\":15.7},{\"x\":1685524705754,\"y\":15.5},{\"x\":1685525005757,\"y\":15.5},{\"x\":1685525305830,\"y\":15.7},{\"x\":1685525605894,\"y\":15.5},{\"x\":1685525905934,\"y\":15.7},{\"x\":1685526205944,\"y\":15.7},{\"x\":1685526505986,\"y\":15.5},{\"x\":1685526806078,\"y\":15.6},{\"x\":1685527106180,\"y\":15.8},{\"x\":1685527406287,\"y\":15.5},{\"x\":1685527706314,\"y\":15.6},{\"x\":1685528006417,\"y\":15.7},{\"x\":1685528306469,\"y\":15.8},{\"x\":1685528606541,\"y\":15.9},{\"x\":1685528906576,\"y\":15.7},{\"x\":1685529206598,\"y\":15.7},{\"x\":1685529506705,\"y\":15.7},{\"x\":1685529806799,\"y\":15.5},{\"x\":1685530106900,\"y\":15.7},{\"x\":1685530406997,\"y\":15.9},{\"x\":1685530707031,\"y\":15.9},{\"x\":1685531007135,\"y\":15.7},{\"x\":1685531307217,\"y\":15.9},{\"x\":1685531607277,\"y\":15.7},{\"x\":1685531907312,\"y\":15.9},{\"x\":1685532207333,\"y\":16},{\"x\":1685532507449,\"y\":15.9},{\"x\":1685532807538,\"y\":16},{\"x\":1685533107607,\"y\":15.9},{\"x\":1685533407716,\"y\":16.1},{\"x\":1685533707801,\"y\":16.4},{\"x\":1685534007891,\"y\":16.5},{\"x\":1685534307936,\"y\":17.9},{\"x\":1685534607976,\"y\":18.5},{\"x\":1685534908043,\"y\":19},{\"x\":1685535208090,\"y\":19.4},{\"x\":1685535508186,\"y\":19.5},{\"x\":1685535808331,\"y\":19.5},{\"x\":1685536108352,\"y\":19.5},{\"x\":1685536408438,\"y\":19.9},{\"x\":1685536708630,\"y\":20.2},{\"x\":1685537023666,\"y\":20.3},{\"x\":1685537323705,\"y\":20.5},{\"x\":1685537623753,\"y\":20.4},{\"x\":1685537923799,\"y\":20.4},{\"x\":1685538223965,\"y\":20.5},{\"x\":1685538538965,\"y\":20.4},{\"x\":1685538839060,\"y\":20.5},{\"x\":1685539139180,\"y\":20.5},{\"x\":1685539439233,\"y\":20.5},{\"x\":1685539739375,\"y\":20.4},{\"x\":1685540039424,\"y\":20.6},{\"x\":1685540339520,\"y\":20.4},{\"x\":1685540639601,\"y\":20.6},{\"x\":1685540939668,\"y\":20.5},{\"x\":1685541239749,\"y\":20.5},{\"x\":1685541539840,\"y\":20.8},{\"x\":1685541839913,\"y\":20.5},{\"x\":1685542139995,\"y\":20.4},{\"x\":1685542440177,\"y\":20.8},{\"x\":1685542740192,\"y\":20.4},{\"x\":1685543040283,\"y\":20.8},{\"x\":1685543340389,\"y\":20.5},{\"x\":1685543640471,\"y\":20.8},{\"x\":1685543940473,\"y\":20.5},{\"x\":1685544240621,\"y\":20.8},{\"x\":1685544540685,\"y\":20.5},{\"x\":1685544840799,\"y\":20.7},{\"x\":1685545140901,\"y\":20.5},{\"x\":1685545441001,\"y\":20.8},{\"x\":1685545741065,\"y\":20.6},{\"x\":1685546041186,\"y\":20.5},{\"x\":1685546341243,\"y\":20.7},{\"x\":1685546641292,\"y\":20.4},{\"x\":1685546941376,\"y\":20.8},{\"x\":1685547241438,\"y\":20.5},{\"x\":1685547541565,\"y\":20.8},{\"x\":1685547841634,\"y\":20.5},{\"x\":1685548141711,\"y\":20.8},{\"x\":1685548441854,\"y\":20.6},{\"x\":1685548741902,\"y\":20.5},{\"x\":1685549042002,\"y\":20.8},{\"x\":1685549342099,\"y\":20.5},{\"x\":1685549642156,\"y\":20.8},{\"x\":1685549942238,\"y\":20.7},{\"x\":1685550242287,\"y\":20.5},{\"x\":1685550542321,\"y\":20.8},{\"x\":1685550842435,\"y\":20.7},{\"x\":1685551142532,\"y\":20.5},{\"x\":1685551442679,\"y\":20.8},{\"x\":1685551742742,\"y\":20.8},{\"x\":1685552042824,\"y\":20.7},{\"x\":1685552342904,\"y\":20.6},{\"x\":1685552643024,\"y\":20.7},{\"x\":1685552943102,\"y\":20.7},{\"x\":1685553243197,\"y\":20.7},{\"x\":1685553543262,\"y\":20.7},{\"x\":1685553843353,\"y\":20.8},{\"x\":1685554143366,\"y\":20.8},{\"x\":1685554443531,\"y\":20.8},{\"x\":1685554743589,\"y\":20.8},{\"x\":1685555043690,\"y\":20.8},{\"x\":1685555343799,\"y\":20.8},{\"x\":1685555643872,\"y\":20.8},{\"x\":1685555943930,\"y\":20.8},{\"x\":1685556244056,\"y\":20.8},{\"x\":1685556544148,\"y\":20.8},{\"x\":1685556844209,\"y\":20.8},{\"x\":1685557144344,\"y\":20.7},{\"x\":1685557444363,\"y\":20.8},{\"x\":1685557744405,\"y\":20.7},{\"x\":1685558044539,\"y\":20.7},{\"x\":1685558344605,\"y\":20.7},{\"x\":1685558644713,\"y\":20.7},{\"x\":1685558944785,\"y\":20.7},{\"x\":1685559244901,\"y\":20.6},{\"x\":1685559544945,\"y\":20.6},{\"x\":1685559845042,\"y\":20.5},{\"x\":1685560145143,\"y\":20.5},{\"x\":1685560445241,\"y\":20.6},{\"x\":1685560745257,\"y\":20.5},{\"x\":1685561045328,\"y\":20.5},{\"x\":1685561345445,\"y\":20.5},{\"x\":1685561645525,\"y\":20.6},{\"x\":1685561945646,\"y\":20.8},{\"x\":1685562245696,\"y\":20.7},{\"x\":1685562545795,\"y\":20.6},{\"x\":1685562845871,\"y\":20.7},{\"x\":1685563145971,\"y\":20.8},{\"x\":1685563446094,\"y\":20.7},{\"x\":1685563746173,\"y\":20.5},{\"x\":1685564046283,\"y\":20.8},{\"x\":1685564346314,\"y\":20.8},{\"x\":1685564646386,\"y\":20.7},{\"x\":1685564946460,\"y\":20.6},{\"x\":1685565246561,\"y\":20.5},{\"x\":1685565546656,\"y\":20.7},{\"x\":1685565846745,\"y\":20.8},{\"x\":1685566146812,\"y\":20.6},{\"x\":1685566446933,\"y\":20.7},{\"x\":1685566746982,\"y\":20.7},{\"x\":1685567047084,\"y\":20.5},{\"x\":1685567347176,\"y\":20.8},{\"x\":1685567647243,\"y\":20.5},{\"x\":1685567947275,\"y\":20.8},{\"x\":1685568247414,\"y\":20.8},{\"x\":1685568547487,\"y\":20.8},{\"x\":1685568847582,\"y\":20.6},{\"x\":1685569147698,\"y\":20.6},{\"x\":1685569447750,\"y\":20.5},{\"x\":1685569747835,\"y\":20.8},{\"x\":1685570047948,\"y\":20.7},{\"x\":1685570348041,\"y\":20.5},{\"x\":1685570648135,\"y\":20.7},{\"x\":1685570948186,\"y\":20.7},{\"x\":1685571248285,\"y\":20.5},{\"x\":1685571548397,\"y\":20.7},{\"x\":1685571848461,\"y\":20.6},{\"x\":1685572148552,\"y\":20.7},{\"x\":1685572448646,\"y\":20.8},{\"x\":1685572748724,\"y\":20.7},{\"x\":1685573048812,\"y\":20.4},{\"x\":1685573348924,\"y\":20.8},{\"x\":1685573648998,\"y\":20.7},{\"x\":1685573949035,\"y\":20.5},{\"x\":1685574249162,\"y\":20.7},{\"x\":1685574564170,\"y\":20.5},{\"x\":1685574864211,\"y\":20.3},{\"x\":1685575164295,\"y\":20.3},{\"x\":1685575464392,\"y\":20.3},{\"x\":1685575764473,\"y\":20.2},{\"x\":1685576064520,\"y\":20},{\"x\":1685576364561,\"y\":19.9},{\"x\":1685576664649,\"y\":19.6},{\"x\":1685576964764,\"y\":19.5},{\"x\":1685577264783,\"y\":19.5},{\"x\":1685577564825,\"y\":19.5},{\"x\":1685577864898,\"y\":19.5},{\"x\":1685578165022,\"y\":19.5},{\"x\":1685578465041,\"y\":19.5},{\"x\":1685578765123,\"y\":19.5},{\"x\":1685579065190,\"y\":19.5},{\"x\":1685579365242,\"y\":19.5},{\"x\":1685579665270,\"y\":19.5},{\"x\":1685579965342,\"y\":19.5},{\"x\":1685580265419,\"y\":19.5},{\"x\":1685580565474,\"y\":19.5},{\"x\":1685580865528,\"y\":19.5},{\"x\":1685581165617,\"y\":19.5},{\"x\":1685581465707,\"y\":19.5},{\"x\":1685581765780,\"y\":19.5},{\"x\":1685582065871,\"y\":19.5},{\"x\":1685582365914,\"y\":19.5},{\"x\":1685582665994,\"y\":19.5},{\"x\":1685582966029,\"y\":19.5},{\"x\":1685583266054,\"y\":19.5},{\"x\":1685583566137,\"y\":19.5},{\"x\":1685583866167,\"y\":19.5},{\"x\":1685584166249,\"y\":19.5},{\"x\":1685584466332,\"y\":19.5},{\"x\":1685584766387,\"y\":19.5},{\"x\":1685585066444,\"y\":19.5},{\"x\":1685585366558,\"y\":19.5},{\"x\":1685585666591,\"y\":19.5},{\"x\":1685585966705,\"y\":19.5},{\"x\":1685586281700,\"y\":19.5},{\"x\":1685586581763,\"y\":19.5},{\"x\":1685586881831,\"y\":19.5},{\"x\":1685587181935,\"y\":19.5},{\"x\":1685587481999,\"y\":19.4},{\"x\":1685587782043,\"y\":19.4},{\"x\":1685588082116,\"y\":19.4},{\"x\":1685588382188,\"y\":19.4},{\"x\":1685588682255,\"y\":19.4},{\"x\":1685588982296,\"y\":19.4},{\"x\":1685589282341,\"y\":19.3},{\"x\":1685589582405,\"y\":19.4},{\"x\":1685589882472,\"y\":19.4},{\"x\":1685590182540,\"y\":19.3},{\"x\":1685590482618,\"y\":19.3},{\"x\":1685590782722,\"y\":19.3},{\"x\":1685591082771,\"y\":19.3},{\"x\":1685591382819,\"y\":19.2},{\"x\":1685591682931,\"y\":19.2},{\"x\":1685591982967,\"y\":19.2},{\"x\":1685592282986,\"y\":19.2},{\"x\":1685592583082,\"y\":19.2},{\"x\":1685592883165,\"y\":19.2},{\"x\":1685593183195,\"y\":19.2},{\"x\":1685593483277,\"y\":19.1},{\"x\":1685593783380,\"y\":19.2},{\"x\":1685594083450,\"y\":19.2},{\"x\":1685594383491,\"y\":19.2},{\"x\":1685594683592,\"y\":19.1},{\"x\":1685594983618,\"y\":19.1},{\"x\":1685595283711,\"y\":19.1},{\"x\":1685595583730,\"y\":19.1},{\"x\":1685595883817,\"y\":19},{\"x\":1685596183896,\"y\":19},{\"x\":1685596483953,\"y\":19},{\"x\":1685596784061,\"y\":19},{\"x\":1685597084124,\"y\":19},{\"x\":1685597384139,\"y\":19.1},{\"x\":1685597684213,\"y\":19},{\"x\":1685597984240,\"y\":19},{\"x\":1685598284312,\"y\":19},{\"x\":1685598584338,\"y\":19},{\"x\":1685598884381,\"y\":19},{\"x\":1685599184436,\"y\":19},{\"x\":1685599499407,\"y\":19},{\"x\":1685599799490,\"y\":18.9},{\"x\":1685600099528,\"y\":19},{\"x\":1685600399539,\"y\":19},{\"x\":1685600699637,\"y\":19},{\"x\":1685600999644,\"y\":19},{\"x\":1685601299689,\"y\":19},{\"x\":1685601599758,\"y\":19},{\"x\":1685601899826,\"y\":19},{\"x\":1685602199848,\"y\":18.9},{\"x\":1685602499869,\"y\":18.9},{\"x\":1685602799939,\"y\":19},{\"x\":1685603099979,\"y\":18.9},{\"x\":1685603414978,\"y\":18.9},{\"x\":1685603715053,\"y\":19},{\"x\":1685604030039,\"y\":19},{\"x\":1685604330117,\"y\":19},{\"x\":1685604630167,\"y\":19},{\"x\":1685604930199,\"y\":19},{\"x\":1685605230238,\"y\":18.9},{\"x\":1685605530260,\"y\":19},{\"x\":1685605830318,\"y\":19},{\"x\":1685606130319,\"y\":19},{\"x\":1685606430335,\"y\":19},{\"x\":1685606730393,\"y\":19},{\"x\":1685607030458,\"y\":19},{\"x\":1685607330485,\"y\":19.5},{\"x\":1685607630516,\"y\":20.3},{\"x\":1685607930568,\"y\":20.6},{\"x\":1685608230576,\"y\":20.4},{\"x\":1685608530643,\"y\":20.5},{\"x\":1685608830684,\"y\":20.5},{\"x\":1685609130723,\"y\":20.5},{\"x\":1685609430778,\"y\":20.5},{\"x\":1685609745753,\"y\":20.7},{\"x\":1685610045798,\"y\":20.4},{\"x\":1685610345834,\"y\":20.7},{\"x\":1685610645876,\"y\":20.4},{\"x\":1685610945921,\"y\":20.8},{\"x\":1685611245935,\"y\":20.5},{\"x\":1685611545963,\"y\":20.8},{\"x\":1685611845989,\"y\":20.6},{\"x\":1685612146117,\"y\":20.5},{\"x\":1685612446189,\"y\":20.8},{\"x\":1685612746312,\"y\":20.6},{\"x\":1685613046346,\"y\":20.5},{\"x\":1685613346425,\"y\":20.8},{\"x\":1685613646580,\"y\":20.6},{\"x\":1685613961556,\"y\":20.6},{\"x\":1685614261648,\"y\":20.7},{\"x\":1685614576669,\"y\":20.7},{\"x\":1685614876695,\"y\":20.5},{\"x\":1685615176733,\"y\":20.4},{\"x\":1685615476808,\"y\":20.7},{\"x\":1685615776863,\"y\":20.7},{\"x\":1685616076917,\"y\":20.5},{\"x\":1685616376987,\"y\":20.4},{\"x\":1685616677033,\"y\":20.3},{\"x\":1685616977113,\"y\":20.3},{\"x\":1685617277203,\"y\":20.3},{\"x\":1685617577236,\"y\":20.3},{\"x\":1685617877275,\"y\":20.2},{\"x\":1685618177324,\"y\":20.3},{\"x\":1685618477408,\"y\":20.2},{\"x\":1685618777447,\"y\":20.3},{\"x\":1685619077601,\"y\":20.2},{\"x\":1685619377646,\"y\":20.2},{\"x\":1685619677730,\"y\":20.1},{\"x\":1685619977761,\"y\":20.2},{\"x\":1685620017473,\"y\":20.1},{\"x\":1685620080667,\"y\":20.1}],[{\"x\":1685015251039,\"y\":19.18},{\"x\":1685015551015,\"y\":19.18},{\"x\":1685015851010,\"y\":19.18},{\"x\":1685016151028,\"y\":19.09},{\"x\":1685016451042,\"y\":18.75},{\"x\":1685016751019,\"y\":18.66},{\"x\":1685017051028,\"y\":18.75},{\"x\":1685017351047,\"y\":18.89},{\"x\":1685017651031,\"y\":18.8},{\"x\":1685017951098,\"y\":18.83},{\"x\":1685018251018,\"y\":18.3},{\"x\":1685018551020,\"y\":18.3},{\"x\":1685018851032,\"y\":18.36},{\"x\":1685019151038,\"y\":18.36},{\"x\":1685019451035,\"y\":19.14},{\"x\":1685019751031,\"y\":19.04},{\"x\":1685020051038,\"y\":19.64},{\"x\":1685020351040,\"y\":19.42},{\"x\":1685020651063,\"y\":19.88},{\"x\":1685020951079,\"y\":19.73},{\"x\":1685021251036,\"y\":19.73},{\"x\":1685021551137,\"y\":19.54},{\"x\":1685021851047,\"y\":19.21},{\"x\":1685022151057,\"y\":19.13},{\"x\":1685022451042,\"y\":19.21},{\"x\":1685022751080,\"y\":19.1},{\"x\":1685023051043,\"y\":18.46},{\"x\":1685023351058,\"y\":18.46},{\"x\":1685023651118,\"y\":18.47},{\"x\":1685023951188,\"y\":18.48},{\"x\":1685024251055,\"y\":18.48},{\"x\":1685024551051,\"y\":18.44},{\"x\":1685024851052,\"y\":18.44},{\"x\":1685025151135,\"y\":18.73},{\"x\":1685025451076,\"y\":18.73},{\"x\":1685025751061,\"y\":18.73},{\"x\":1685026051071,\"y\":19.02},{\"x\":1685026351158,\"y\":19.05},{\"x\":1685026651115,\"y\":19.03},{\"x\":1685026951071,\"y\":19.03},{\"x\":1685027251066,\"y\":19.03},{\"x\":1685027551148,\"y\":18.62},{\"x\":1685027851058,\"y\":18.62},{\"x\":1685028151059,\"y\":18.62},{\"x\":1685028451112,\"y\":19},{\"x\":1685028751146,\"y\":18.91},{\"x\":1685029051091,\"y\":18.1},{\"x\":1685029351104,\"y\":18.12},{\"x\":1685029651134,\"y\":18.51},{\"x\":1685029951350,\"y\":18.12},{\"x\":1685030251085,\"y\":18.51},{\"x\":1685030551087,\"y\":18.42},{\"x\":1685030851113,\"y\":18.46},{\"x\":1685031151160,\"y\":18.37},{\"x\":1685031451109,\"y\":18.33},{\"x\":1685031751080,\"y\":18.21},{\"x\":1685032051082,\"y\":18.3},{\"x\":1685032351518,\"y\":18.3},{\"x\":1685032651206,\"y\":17.89},{\"x\":1685032951081,\"y\":17.8},{\"x\":1685033251086,\"y\":17.8},{\"x\":1685033551096,\"y\":17.8},{\"x\":1685033851075,\"y\":17.8},{\"x\":1685034151086,\"y\":17.78},{\"x\":1685034451139,\"y\":17.63},{\"x\":1685034751066,\"y\":17.63},{\"x\":1685035051075,\"y\":17.63},{\"x\":1685035351084,\"y\":17.54},{\"x\":1685035651091,\"y\":17.54},{\"x\":1685035951102,\"y\":17.55},{\"x\":1685036251109,\"y\":17.28},{\"x\":1685036551080,\"y\":17.54},{\"x\":1685036851085,\"y\":17.25},{\"x\":1685037151113,\"y\":17.2},{\"x\":1685037451085,\"y\":16.99},{\"x\":1685037751124,\"y\":16.99},{\"x\":1685038051100,\"y\":16.99},{\"x\":1685038351547,\"y\":17.1},{\"x\":1685038651116,\"y\":17.02},{\"x\":1685038951187,\"y\":16.94},{\"x\":1685039251190,\"y\":16.88},{\"x\":1685039551190,\"y\":16.85},{\"x\":1685039851181,\"y\":16.85},{\"x\":1685040151138,\"y\":16.82},{\"x\":1685040451132,\"y\":16.79},{\"x\":1685040751183,\"y\":16.79},{\"x\":1685041051160,\"y\":16.79},{\"x\":1685041351171,\"y\":16.7},{\"x\":1685041651173,\"y\":16.44},{\"x\":1685041951215,\"y\":16.44},{\"x\":1685042251220,\"y\":16.48},{\"x\":1685042551262,\"y\":16.52},{\"x\":1685042851204,\"y\":16.48},{\"x\":1685043151252,\"y\":16.52},{\"x\":1685043451209,\"y\":16.4},{\"x\":1685043751246,\"y\":16.26},{\"x\":1685044051210,\"y\":16.26},{\"x\":1685044351522,\"y\":16.26},{\"x\":1685044651215,\"y\":16.17},{\"x\":1685044951218,\"y\":16.17},{\"x\":1685045251281,\"y\":15.98},{\"x\":1685045551345,\"y\":15.98},{\"x\":1685045851232,\"y\":16.01},{\"x\":1685046151240,\"y\":15.98},{\"x\":1685046451276,\"y\":15.75},{\"x\":1685046751262,\"y\":15.75},{\"x\":1685047051266,\"y\":15.75},{\"x\":1685047351285,\"y\":15.68},{\"x\":1685047651267,\"y\":15.68},{\"x\":1685047951728,\"y\":15.49},{\"x\":1685048251321,\"y\":15.5},{\"x\":1685048551265,\"y\":15.49},{\"x\":1685048851352,\"y\":15.35},{\"x\":1685049151302,\"y\":15.35},{\"x\":1685049451264,\"y\":14.98},{\"x\":1685049751268,\"y\":14.98},{\"x\":1685050051322,\"y\":14.97},{\"x\":1685050351309,\"y\":14.97},{\"x\":1685050651266,\"y\":14.75},{\"x\":1685050951264,\"y\":14.75},{\"x\":1685051251276,\"y\":14.82},{\"x\":1685051551291,\"y\":14.73},{\"x\":1685051851384,\"y\":14.57},{\"x\":1685052151274,\"y\":14.41},{\"x\":1685052451281,\"y\":14.41},{\"x\":1685052751280,\"y\":14.35},{\"x\":1685053051279,\"y\":14.49},{\"x\":1685053351326,\"y\":14.29},{\"x\":1685053651310,\"y\":14.3},{\"x\":1685053951323,\"y\":14.29},{\"x\":1685054251377,\"y\":13.95},{\"x\":1685054551303,\"y\":13.95},{\"x\":1685054851307,\"y\":13.95},{\"x\":1685055151655,\"y\":13.71},{\"x\":1685055451302,\"y\":13.71},{\"x\":1685055751335,\"y\":13.58},{\"x\":1685056051330,\"y\":13.55},{\"x\":1685056351435,\"y\":13.58},{\"x\":1685056651370,\"y\":13.33},{\"x\":1685056951348,\"y\":13.58},{\"x\":1685057251335,\"y\":13.23},{\"x\":1685057551642,\"y\":13.23},{\"x\":1685057851342,\"y\":13.23},{\"x\":1685058151360,\"y\":12.98},{\"x\":1685058451395,\"y\":13},{\"x\":1685058751511,\"y\":13.01},{\"x\":1685059051391,\"y\":13.01},{\"x\":1685059351339,\"y\":12.9},{\"x\":1685059651359,\"y\":12.9},{\"x\":1685059951438,\"y\":12.48},{\"x\":1685060251356,\"y\":12.48},{\"x\":1685060551372,\"y\":12.48},{\"x\":1685060851371,\"y\":12.32},{\"x\":1685061151374,\"y\":12.32},{\"x\":1685061451412,\"y\":11.72},{\"x\":1685061751394,\"y\":11.63},{\"x\":1685062051381,\"y\":11.72},{\"x\":1685062352047,\"y\":11.55},{\"x\":1685062651444,\"y\":11.36},{\"x\":1685062951373,\"y\":11.46},{\"x\":1685063251410,\"y\":11.36},{\"x\":1685063551479,\"y\":11.37},{\"x\":1685063851380,\"y\":11.36},{\"x\":1685064151391,\"y\":11.44},{\"x\":1685064451471,\"y\":11.34},{\"x\":1685064751589,\"y\":11.32},{\"x\":1685065051396,\"y\":11.26},{\"x\":1685065351399,\"y\":11.22},{\"x\":1685065651409,\"y\":11.39},{\"x\":1685065952272,\"y\":11.33},{\"x\":1685066251396,\"y\":11.21},{\"x\":1685066551403,\"y\":11.21},{\"x\":1685066851417,\"y\":11.34},{\"x\":1685067151442,\"y\":11.28},{\"x\":1685067451399,\"y\":11.27},{\"x\":1685067751460,\"y\":11.06},{\"x\":1685068051420,\"y\":11.27},{\"x\":1685068351485,\"y\":11.02},{\"x\":1685068651412,\"y\":11.02},{\"x\":1685068951402,\"y\":10.91},{\"x\":1685069251450,\"y\":11},{\"x\":1685069551482,\"y\":11},{\"x\":1685069851414,\"y\":10.88},{\"x\":1685070151414,\"y\":10.66},{\"x\":1685070451408,\"y\":10.66},{\"x\":1685070752331,\"y\":10.76},{\"x\":1685071051424,\"y\":10.76},{\"x\":1685071351428,\"y\":10.76},{\"x\":1685071651498,\"y\":10.55},{\"x\":1685071951451,\"y\":10.55},{\"x\":1685072251483,\"y\":10.55},{\"x\":1685072551428,\"y\":10.55},{\"x\":1685072851477,\"y\":10.34},{\"x\":1685073151490,\"y\":10.34},{\"x\":1685073451401,\"y\":10.34},{\"x\":1685073751433,\"y\":10.08},{\"x\":1685074051433,\"y\":10.08},{\"x\":1685074351659,\"y\":9.98},{\"x\":1685074651481,\"y\":9.98},{\"x\":1685074951442,\"y\":9.98},{\"x\":1685075251486,\"y\":9.76},{\"x\":1685075553145,\"y\":9.76},{\"x\":1685075851454,\"y\":9.76},{\"x\":1685076151559,\"y\":9.76},{\"x\":1685076451455,\"y\":9.76},{\"x\":1685076751637,\"y\":9.76},{\"x\":1685077051562,\"y\":10.23},{\"x\":1685077351466,\"y\":10.23},{\"x\":1685077651495,\"y\":10.23},{\"x\":1685077951596,\"y\":10.15},{\"x\":1685078251465,\"y\":10.36},{\"x\":1685078551490,\"y\":10.36},{\"x\":1685078851486,\"y\":10.82},{\"x\":1685079152413,\"y\":10.82},{\"x\":1685079451481,\"y\":11.06},{\"x\":1685079751484,\"y\":11.06},{\"x\":1685080051515,\"y\":11.26},{\"x\":1685080353153,\"y\":11.08},{\"x\":1685080651466,\"y\":11.37},{\"x\":1685080951559,\"y\":11.85},{\"x\":1685081251496,\"y\":12.07},{\"x\":1685081552662,\"y\":11.54},{\"x\":1685081851505,\"y\":11.54},{\"x\":1685082151498,\"y\":12.9},{\"x\":1685082451574,\"y\":11.98},{\"x\":1685082752768,\"y\":12.05},{\"x\":1685083051516,\"y\":12.05},{\"x\":1685083351518,\"y\":12.54},{\"x\":1685083651522,\"y\":12.54},{\"x\":1685083951825,\"y\":13.01},{\"x\":1685084251526,\"y\":13.01},{\"x\":1685084551531,\"y\":13.84},{\"x\":1685084851537,\"y\":14.15},{\"x\":1685085154315,\"y\":14.2},{\"x\":1685085451540,\"y\":14.2},{\"x\":1685085751599,\"y\":14.74},{\"x\":1685086051532,\"y\":14.65},{\"x\":1685086352440,\"y\":14.65},{\"x\":1685086651538,\"y\":15.31},{\"x\":1685086951544,\"y\":15.31},{\"x\":1685087251583,\"y\":15.43},{\"x\":1685087552469,\"y\":15.61},{\"x\":1685087851550,\"y\":15.43},{\"x\":1685088151549,\"y\":15.75},{\"x\":1685088451547,\"y\":15.75},{\"x\":1685088752994,\"y\":16.44},{\"x\":1685089051545,\"y\":16.44},{\"x\":1685089351589,\"y\":16.84},{\"x\":1685089651598,\"y\":16.44},{\"x\":1685089952825,\"y\":16.84},{\"x\":1685090251561,\"y\":17.35},{\"x\":1685090551636,\"y\":17.66},{\"x\":1685090851564,\"y\":17.56},{\"x\":1685091152016,\"y\":17.66},{\"x\":1685091451594,\"y\":17.93},{\"x\":1685091751567,\"y\":18.04},{\"x\":1685092051581,\"y\":17.93},{\"x\":1685092352469,\"y\":18.08},{\"x\":1685092651613,\"y\":18.17},{\"x\":1685092951598,\"y\":18.25},{\"x\":1685093251572,\"y\":18.23},{\"x\":1685093551684,\"y\":18.43},{\"x\":1685093851596,\"y\":18.5},{\"x\":1685094151578,\"y\":18.5},{\"x\":1685094451581,\"y\":18.85},{\"x\":1685094753849,\"y\":18.85},{\"x\":1685095051634,\"y\":19.17},{\"x\":1685095351592,\"y\":19.09},{\"x\":1685095651582,\"y\":19.37},{\"x\":1685095952262,\"y\":19.37},{\"x\":1685096251665,\"y\":19.46},{\"x\":1685096551653,\"y\":19.46},{\"x\":1685096851597,\"y\":19.46},{\"x\":1685097151894,\"y\":19.32},{\"x\":1685097451605,\"y\":19.59},{\"x\":1685097751603,\"y\":19.85},{\"x\":1685098051603,\"y\":19.75},{\"x\":1685098351825,\"y\":19.65},{\"x\":1685098651611,\"y\":19.82},{\"x\":1685098951612,\"y\":19.65},{\"x\":1685099251620,\"y\":19.57},{\"x\":1685099554557,\"y\":19.84},{\"x\":1685099851611,\"y\":19.98},{\"x\":1685100151610,\"y\":19.98},{\"x\":1685100451674,\"y\":19.9},{\"x\":1685100751631,\"y\":19.9},{\"x\":1685101051671,\"y\":20.45},{\"x\":1685101351650,\"y\":20.45},{\"x\":1685101651625,\"y\":20.45},{\"x\":1685101952592,\"y\":19.74},{\"x\":1685102251628,\"y\":19.74},{\"x\":1685102551620,\"y\":19.74},{\"x\":1685102851630,\"y\":19.84},{\"x\":1685103152663,\"y\":20.53},{\"x\":1685103451625,\"y\":20.53},{\"x\":1685103751638,\"y\":20.37},{\"x\":1685104051653,\"y\":20.37},{\"x\":1685104351690,\"y\":20.42},{\"x\":1685104651625,\"y\":20.92},{\"x\":1685104951669,\"y\":21.05},{\"x\":1685105251635,\"y\":20.92},{\"x\":1685106270209,\"y\":21.51},{\"x\":1685106570210,\"y\":21.28},{\"x\":1685106870262,\"y\":21.31},{\"x\":1685107170259,\"y\":20.87},{\"x\":1685107470202,\"y\":20.87},{\"x\":1685107770205,\"y\":20.87},{\"x\":1685108070201,\"y\":20.24},{\"x\":1685108370268,\"y\":20.24},{\"x\":1685108670211,\"y\":20.81},{\"x\":1685108970215,\"y\":20.61},{\"x\":1685109270264,\"y\":21.02},{\"x\":1685109570225,\"y\":21.02},{\"x\":1685109870210,\"y\":21.35},{\"x\":1685110170276,\"y\":21.42},{\"x\":1685110470215,\"y\":21.08},{\"x\":1685110770218,\"y\":21.08},{\"x\":1685111070267,\"y\":21.3},{\"x\":1685111370237,\"y\":21.32},{\"x\":1685111670242,\"y\":21.21},{\"x\":1685111970278,\"y\":21.56},{\"x\":1685112270243,\"y\":21.43},{\"x\":1685112570286,\"y\":20.62},{\"x\":1685112870312,\"y\":20.61},{\"x\":1685113170279,\"y\":20.61},{\"x\":1685113470259,\"y\":20.52},{\"x\":1685113770274,\"y\":21.03},{\"x\":1685114070330,\"y\":20.96},{\"x\":1685114370346,\"y\":20.62},{\"x\":1685114670290,\"y\":20.62},{\"x\":1685114970303,\"y\":20.62},{\"x\":1685115270293,\"y\":20.57},{\"x\":1685115570345,\"y\":20.81},{\"x\":1685115870301,\"y\":20.81},{\"x\":1685116170341,\"y\":20.85},{\"x\":1685116470425,\"y\":20.35},{\"x\":1685116770319,\"y\":20.73},{\"x\":1685117070306,\"y\":20.35},{\"x\":1685117370344,\"y\":20.75},{\"x\":1685117670330,\"y\":20.75},{\"x\":1685117970336,\"y\":20.75},{\"x\":1685118270330,\"y\":20.45},{\"x\":1685118570325,\"y\":20.45},{\"x\":1685118870362,\"y\":19.95},{\"x\":1685119170359,\"y\":19.95},{\"x\":1685119470369,\"y\":19.95},{\"x\":1685119770365,\"y\":19.92},{\"x\":1685120070363,\"y\":20.06},{\"x\":1685120370385,\"y\":20.17},{\"x\":1685120670359,\"y\":20.17},{\"x\":1685120970383,\"y\":20.08},{\"x\":1685121270373,\"y\":19.95},{\"x\":1685121570376,\"y\":19.85},{\"x\":1685121870394,\"y\":19.88},{\"x\":1685122170385,\"y\":19.88},{\"x\":1685122470467,\"y\":19},{\"x\":1685122770415,\"y\":19},{\"x\":1685123070412,\"y\":19},{\"x\":1685123370409,\"y\":18.86},{\"x\":1685123670402,\"y\":18.86},{\"x\":1685123970418,\"y\":18.86},{\"x\":1685124270399,\"y\":18.86},{\"x\":1685124570415,\"y\":18.79},{\"x\":1685124870446,\"y\":18.79},{\"x\":1685125170457,\"y\":18.85},{\"x\":1685125470494,\"y\":18.85},{\"x\":1685125770424,\"y\":18.85},{\"x\":1685126070436,\"y\":18.71},{\"x\":1685126370464,\"y\":18.71},{\"x\":1685126670446,\"y\":18.67},{\"x\":1685126970458,\"y\":18.67},{\"x\":1685127270730,\"y\":17.76},{\"x\":1685127570540,\"y\":17.84},{\"x\":1685127870479,\"y\":17.42},{\"x\":1685128170488,\"y\":17.42},{\"x\":1685128470446,\"y\":17.32},{\"x\":1685128770711,\"y\":17.38},{\"x\":1685129070493,\"y\":17.55},{\"x\":1685129370460,\"y\":17.46},{\"x\":1685129670456,\"y\":17.51},{\"x\":1685129970444,\"y\":16.98},{\"x\":1685130270452,\"y\":16.94},{\"x\":1685130570463,\"y\":16.94},{\"x\":1685130870454,\"y\":16.63},{\"x\":1685131170558,\"y\":16.63},{\"x\":1685131470459,\"y\":16.63},{\"x\":1685131770480,\"y\":15.83},{\"x\":1685132070539,\"y\":15.61},{\"x\":1685132370654,\"y\":15.61},{\"x\":1685132670510,\"y\":15.17},{\"x\":1685132970470,\"y\":15.17},{\"x\":1685133270483,\"y\":15.17},{\"x\":1685133570534,\"y\":15.33},{\"x\":1685133870537,\"y\":15.17},{\"x\":1685134170481,\"y\":15.33},{\"x\":1685134470522,\"y\":15.17},{\"x\":1685134770619,\"y\":15.17},{\"x\":1685135070468,\"y\":15.17},{\"x\":1685135370539,\"y\":14.12},{\"x\":1685135670470,\"y\":14.12},{\"x\":1685135970588,\"y\":14.12},{\"x\":1685136270506,\"y\":13.94},{\"x\":1685136570482,\"y\":13.94},{\"x\":1685136870483,\"y\":13.88},{\"x\":1685137170591,\"y\":13.53},{\"x\":1685137470482,\"y\":13.29},{\"x\":1685137770513,\"y\":13.29},{\"x\":1685138070565,\"y\":13.24},{\"x\":1685138370612,\"y\":13.3},{\"x\":1685138670513,\"y\":12.65},{\"x\":1685138970488,\"y\":13.03},{\"x\":1685139270567,\"y\":12.44},{\"x\":1685139570520,\"y\":12.44},{\"x\":1685139870495,\"y\":12.47},{\"x\":1685140170514,\"y\":12.41},{\"x\":1685140470469,\"y\":12.41},{\"x\":1685140770566,\"y\":12.32},{\"x\":1685141070532,\"y\":11.91},{\"x\":1685141370505,\"y\":11.91},{\"x\":1685141670535,\"y\":11.91},{\"x\":1685141970572,\"y\":11.87},{\"x\":1685142270528,\"y\":11.87},{\"x\":1685142570539,\"y\":11.49},{\"x\":1685142870540,\"y\":11.49},{\"x\":1685143170721,\"y\":11.69},{\"x\":1685143470532,\"y\":11.69},{\"x\":1685143770537,\"y\":11.69},{\"x\":1685144070587,\"y\":11.38},{\"x\":1685144370585,\"y\":11.29},{\"x\":1685144670544,\"y\":11.38},{\"x\":1685144970592,\"y\":11.66},{\"x\":1685145270578,\"y\":11.66},{\"x\":1685145570616,\"y\":11.66},{\"x\":1685145870578,\"y\":11.54},{\"x\":1685146170575,\"y\":11.3},{\"x\":1685146470586,\"y\":11.26},{\"x\":1685146770618,\"y\":10.97},{\"x\":1685147070575,\"y\":10.97},{\"x\":1685147370627,\"y\":11.06},{\"x\":1685147670593,\"y\":10.78},{\"x\":1685147970600,\"y\":11.06},{\"x\":1685148270605,\"y\":10.65},{\"x\":1685148570644,\"y\":10.63},{\"x\":1685148870584,\"y\":10.63},{\"x\":1685149170590,\"y\":10.63},{\"x\":1685149470587,\"y\":10.44},{\"x\":1685149770583,\"y\":10.44},{\"x\":1685150070580,\"y\":10.1},{\"x\":1685150370584,\"y\":10.1},{\"x\":1685150670582,\"y\":10.03},{\"x\":1685150970594,\"y\":10.03},{\"x\":1685151270632,\"y\":10.08},{\"x\":1685151570591,\"y\":10.08},{\"x\":1685151870605,\"y\":9.61},{\"x\":1685152170611,\"y\":9.61},{\"x\":1685152470611,\"y\":9.42},{\"x\":1685152770613,\"y\":9.42},{\"x\":1685153070628,\"y\":9.48},{\"x\":1685153370683,\"y\":9.39},{\"x\":1685153670664,\"y\":9.33},{\"x\":1685153970636,\"y\":9.33},{\"x\":1685154270611,\"y\":9.33},{\"x\":1685154570650,\"y\":9.14},{\"x\":1685154870623,\"y\":9.14},{\"x\":1685155170939,\"y\":9.05},{\"x\":1685155470674,\"y\":8.95},{\"x\":1685155770628,\"y\":8.95},{\"x\":1685156070642,\"y\":8.86},{\"x\":1685156370678,\"y\":8.95},{\"x\":1685156670691,\"y\":8.95},{\"x\":1685156970647,\"y\":8.95},{\"x\":1685157270649,\"y\":8.48},{\"x\":1685157570728,\"y\":8.51},{\"x\":1685157870653,\"y\":8.67},{\"x\":1685158170653,\"y\":8.67},{\"x\":1685158470652,\"y\":8.47},{\"x\":1685158770755,\"y\":8.29},{\"x\":1685159070671,\"y\":8.29},{\"x\":1685159370691,\"y\":8.19},{\"x\":1685159670661,\"y\":8.19},{\"x\":1685159971379,\"y\":8.08},{\"x\":1685160270711,\"y\":8.04},{\"x\":1685160570739,\"y\":8.22},{\"x\":1685160870655,\"y\":8.22},{\"x\":1685161170787,\"y\":8.04},{\"x\":1685161470703,\"y\":8.24},{\"x\":1685161770668,\"y\":8.04},{\"x\":1685162070717,\"y\":8.02},{\"x\":1685162371170,\"y\":7.93},{\"x\":1685162670708,\"y\":8.02},{\"x\":1685162970719,\"y\":8.55},{\"x\":1685163270676,\"y\":8.55},{\"x\":1685163571126,\"y\":8.55},{\"x\":1685163870678,\"y\":8.77},{\"x\":1685164170681,\"y\":8.69},{\"x\":1685164470773,\"y\":9.46},{\"x\":1685164771057,\"y\":9.68},{\"x\":1685165070675,\"y\":9.97},{\"x\":1685165370677,\"y\":9.83},{\"x\":1685165670713,\"y\":10.29},{\"x\":1685165970916,\"y\":10.29},{\"x\":1685166270681,\"y\":10.47},{\"x\":1685166570684,\"y\":10.47},{\"x\":1685166870680,\"y\":11},{\"x\":1685167171095,\"y\":10.85},{\"x\":1685167470678,\"y\":11.53},{\"x\":1685167770689,\"y\":12.14},{\"x\":1685168070683,\"y\":12.14},{\"x\":1685168370787,\"y\":11.68},{\"x\":1685168670739,\"y\":11.68},{\"x\":1685168970743,\"y\":11.98},{\"x\":1685169270696,\"y\":11.98},{\"x\":1685169570699,\"y\":11.98},{\"x\":1685169870784,\"y\":13.66},{\"x\":1685170170701,\"y\":13.66},{\"x\":1685170470695,\"y\":13.66},{\"x\":1685170770757,\"y\":13.66},{\"x\":1685171070684,\"y\":13.98},{\"x\":1685171370697,\"y\":13.98},{\"x\":1685171670698,\"y\":14.38},{\"x\":1685171971138,\"y\":14.38},{\"x\":1685172270759,\"y\":13.85},{\"x\":1685172570699,\"y\":13.85},{\"x\":1685172870711,\"y\":13.85},{\"x\":1685173170778,\"y\":15.4},{\"x\":1685173470740,\"y\":15.5},{\"x\":1685173770702,\"y\":15.18},{\"x\":1685174070719,\"y\":15.49},{\"x\":1685174371094,\"y\":15.8},{\"x\":1685174670695,\"y\":15.49},{\"x\":1685174970800,\"y\":16.03},{\"x\":1685175270722,\"y\":15.86},{\"x\":1685175570763,\"y\":15.86},{\"x\":1685175870697,\"y\":16.35},{\"x\":1685176170707,\"y\":16.48},{\"x\":1685176470754,\"y\":16.94},{\"x\":1685176770748,\"y\":16.94},{\"x\":1685177070739,\"y\":17.05},{\"x\":1685177370721,\"y\":17.05},{\"x\":1685177670735,\"y\":17.05},{\"x\":1685177971115,\"y\":17.66},{\"x\":1685178270740,\"y\":17.66},{\"x\":1685178570747,\"y\":17.64},{\"x\":1685178870774,\"y\":18.01},{\"x\":1685179171072,\"y\":17.92},{\"x\":1685179470822,\"y\":18.19},{\"x\":1685179770758,\"y\":18.1},{\"x\":1685180070762,\"y\":18.19},{\"x\":1685180370805,\"y\":18.1},{\"x\":1685180670780,\"y\":18.51},{\"x\":1685180970809,\"y\":18.65},{\"x\":1685181270793,\"y\":18.72},{\"x\":1685181570900,\"y\":19.19},{\"x\":1685181870801,\"y\":19.1},{\"x\":1685182170811,\"y\":19.19},{\"x\":1685182470860,\"y\":19.44},{\"x\":1685182771048,\"y\":19.67},{\"x\":1685183070826,\"y\":19.44},{\"x\":1685183370877,\"y\":19.67},{\"x\":1685183670882,\"y\":19.85},{\"x\":1685183970939,\"y\":19.85},{\"x\":1685184270841,\"y\":19.85},{\"x\":1685184570853,\"y\":19.96},{\"x\":1685184870900,\"y\":20.18},{\"x\":1685185170967,\"y\":20.47},{\"x\":1685185470866,\"y\":20.18},{\"x\":1685185770871,\"y\":20.65},{\"x\":1685186070870,\"y\":20.65},{\"x\":1685186370892,\"y\":20.6},{\"x\":1685186670893,\"y\":20.57},{\"x\":1685186970904,\"y\":20.71},{\"x\":1685187270868,\"y\":20.71},{\"x\":1685187572989,\"y\":20.99},{\"x\":1685187870872,\"y\":20.99},{\"x\":1685188170877,\"y\":20.99},{\"x\":1685188470913,\"y\":21.13},{\"x\":1685188771470,\"y\":21.2},{\"x\":1685189070899,\"y\":21.12},{\"x\":1685189370894,\"y\":21.2},{\"x\":1685189670921,\"y\":21.41},{\"x\":1685189971610,\"y\":21.41},{\"x\":1685190270898,\"y\":21.41},{\"x\":1685190570940,\"y\":21.32},{\"x\":1685190870918,\"y\":21.32},{\"x\":1685191171751,\"y\":21.78},{\"x\":1685191470923,\"y\":21.68},{\"x\":1685191770922,\"y\":21.68},{\"x\":1685192070981,\"y\":22.17},{\"x\":1685192371026,\"y\":22.17},{\"x\":1685192670934,\"y\":22.17},{\"x\":1685192970954,\"y\":22.46},{\"x\":1685193270953,\"y\":22.46},{\"x\":1685193571076,\"y\":21.99},{\"x\":1685193870942,\"y\":21.91},{\"x\":1685194171071,\"y\":21.99},{\"x\":1685194470972,\"y\":21.91},{\"x\":1685194770981,\"y\":21.91},{\"x\":1685195070960,\"y\":23.06},{\"x\":1685195370975,\"y\":22.73},{\"x\":1685195670969,\"y\":23.06},{\"x\":1685195971032,\"y\":22.82},{\"x\":1685196270973,\"y\":22.82},{\"x\":1685196570982,\"y\":22.82},{\"x\":1685196870986,\"y\":21.82},{\"x\":1685197171084,\"y\":21.93},{\"x\":1685197470984,\"y\":22.31},{\"x\":1685197770988,\"y\":22.31},{\"x\":1685198070990,\"y\":22.31},{\"x\":1685198371020,\"y\":22.31},{\"x\":1685198671053,\"y\":22.37},{\"x\":1685198970993,\"y\":22.23},{\"x\":1685199271011,\"y\":22.37},{\"x\":1685199571046,\"y\":22.48},{\"x\":1685199871115,\"y\":21.98},{\"x\":1685200171006,\"y\":21.98},{\"x\":1685200471008,\"y\":21.98},{\"x\":1685200771192,\"y\":22.9},{\"x\":1685201071014,\"y\":22.9},{\"x\":1685201371067,\"y\":22.11},{\"x\":1685201671018,\"y\":22.11},{\"x\":1685201971372,\"y\":22.11},{\"x\":1685202271010,\"y\":21.91},{\"x\":1685202571020,\"y\":21.91},{\"x\":1685202871094,\"y\":22.37},{\"x\":1685203171957,\"y\":22.04},{\"x\":1685203471024,\"y\":21.92},{\"x\":1685203771087,\"y\":22.72},{\"x\":1685204071079,\"y\":22.72},{\"x\":1685204372043,\"y\":22.63},{\"x\":1685204671023,\"y\":22.63},{\"x\":1685204971067,\"y\":22.65},{\"x\":1685205271028,\"y\":22.65},{\"x\":1685205571490,\"y\":22.29},{\"x\":1685205871024,\"y\":21.93},{\"x\":1685206171037,\"y\":21.93},{\"x\":1685206471023,\"y\":21.53},{\"x\":1685206771057,\"y\":21.53},{\"x\":1685207071049,\"y\":20.84},{\"x\":1685207371039,\"y\":20.84},{\"x\":1685207671107,\"y\":20.92},{\"x\":1685207971068,\"y\":20.84},{\"x\":1685208271098,\"y\":22.62},{\"x\":1685208571110,\"y\":22.62},{\"x\":1685208871086,\"y\":22.62},{\"x\":1685209171774,\"y\":20.91},{\"x\":1685209471092,\"y\":20.82},{\"x\":1685209771092,\"y\":19.71},{\"x\":1685210071129,\"y\":19.73},{\"x\":1685210371211,\"y\":19.73},{\"x\":1685210671121,\"y\":19.24},{\"x\":1685210971142,\"y\":21.59},{\"x\":1685211271109,\"y\":21.59},{\"x\":1685211573673,\"y\":21.59},{\"x\":1685211871103,\"y\":21.59},{\"x\":1685212171150,\"y\":21.56},{\"x\":1685212471136,\"y\":21.47},{\"x\":1685212771652,\"y\":20.46},{\"x\":1685213071195,\"y\":20.46},{\"x\":1685213371188,\"y\":19.23},{\"x\":1685213671169,\"y\":19.15},{\"x\":1685213971280,\"y\":19.23},{\"x\":1685214271228,\"y\":19.41},{\"x\":1685214571150,\"y\":19.41},{\"x\":1685214871151,\"y\":19.24},{\"x\":1685215171157,\"y\":19.21},{\"x\":1685215471219,\"y\":19.21},{\"x\":1685215771223,\"y\":18.87},{\"x\":1685216071239,\"y\":18.72},{\"x\":1685216371187,\"y\":18.87},{\"x\":1685216671220,\"y\":16.48},{\"x\":1685216971167,\"y\":16.48},{\"x\":1685217271190,\"y\":16.48},{\"x\":1685217571385,\"y\":16.48},{\"x\":1685217871202,\"y\":16.48},{\"x\":1685218171176,\"y\":16.48},{\"x\":1685218471193,\"y\":17.13},{\"x\":1685218772083,\"y\":17.04},{\"x\":1685219071242,\"y\":16.94},{\"x\":1685219371182,\"y\":16.94},{\"x\":1685219671182,\"y\":16.94},{\"x\":1685219971327,\"y\":17.29},{\"x\":1685220271195,\"y\":17.28},{\"x\":1685220571204,\"y\":17.28},{\"x\":1685220871265,\"y\":16.43},{\"x\":1685221171213,\"y\":16.43},{\"x\":1685221471206,\"y\":16.43},{\"x\":1685221771244,\"y\":16.32},{\"x\":1685222071214,\"y\":16.32},{\"x\":1685222371240,\"y\":16.22},{\"x\":1685222671231,\"y\":15.63},{\"x\":1685222971227,\"y\":15.63},{\"x\":1685223271215,\"y\":15.63},{\"x\":1685223571642,\"y\":15.63},{\"x\":1685223871216,\"y\":14.69},{\"x\":1685224171238,\"y\":14.69},{\"x\":1685224471255,\"y\":15.05},{\"x\":1685224771718,\"y\":15.05},{\"x\":1685225071254,\"y\":15},{\"x\":1685225371225,\"y\":14.91},{\"x\":1685225671230,\"y\":14.91},{\"x\":1685225971278,\"y\":15.03},{\"x\":1685226271229,\"y\":14.76},{\"x\":1685226571268,\"y\":14.62},{\"x\":1685226871228,\"y\":14.62},{\"x\":1685227171310,\"y\":14.58},{\"x\":1685227471236,\"y\":14.47},{\"x\":1685227771233,\"y\":14.47},{\"x\":1685228071264,\"y\":14.56},{\"x\":1685228371438,\"y\":14.56},{\"x\":1685228671307,\"y\":14.37},{\"x\":1685228971287,\"y\":14.26},{\"x\":1685229271247,\"y\":14.17},{\"x\":1685229571283,\"y\":14.26},{\"x\":1685229871252,\"y\":14.08},{\"x\":1685230171240,\"y\":14.08},{\"x\":1685230471249,\"y\":13.81},{\"x\":1685230771547,\"y\":13.9},{\"x\":1685231071282,\"y\":13.88},{\"x\":1685231371247,\"y\":13.88},{\"x\":1685231671245,\"y\":13.57},{\"x\":1685231971299,\"y\":13.8},{\"x\":1685232271267,\"y\":13.8},{\"x\":1685232571258,\"y\":13.67},{\"x\":1685232871256,\"y\":13.67},{\"x\":1685233172128,\"y\":13.34},{\"x\":1685233471266,\"y\":13.26},{\"x\":1685233771304,\"y\":13.55},{\"x\":1685234071280,\"y\":13.5},{\"x\":1685234371267,\"y\":13.55},{\"x\":1685234671308,\"y\":13.47},{\"x\":1685234971294,\"y\":13.47},{\"x\":1685235271259,\"y\":13.47},{\"x\":1685235573201,\"y\":13.47},{\"x\":1685235871295,\"y\":13.51},{\"x\":1685236171262,\"y\":13.65},{\"x\":1685236471308,\"y\":13.36},{\"x\":1685236772640,\"y\":13.36},{\"x\":1685237071263,\"y\":13.55},{\"x\":1685237371273,\"y\":13.46},{\"x\":1685237671265,\"y\":13.46},{\"x\":1685237972838,\"y\":13.08},{\"x\":1685238271265,\"y\":13.08},{\"x\":1685238571268,\"y\":13.08},{\"x\":1685238871275,\"y\":13.28},{\"x\":1685239172611,\"y\":13.41},{\"x\":1685239471288,\"y\":13.33},{\"x\":1685239771274,\"y\":13.31},{\"x\":1685240071325,\"y\":13.15},{\"x\":1685240372227,\"y\":13.07},{\"x\":1685240671318,\"y\":13.07},{\"x\":1685240971329,\"y\":13.07},{\"x\":1685241271288,\"y\":13.07},{\"x\":1685241571466,\"y\":13.07},{\"x\":1685241871279,\"y\":12.98},{\"x\":1685242171369,\"y\":13.28},{\"x\":1685242471346,\"y\":13.28},{\"x\":1685242771307,\"y\":13.28},{\"x\":1685243071278,\"y\":13.28},{\"x\":1685243371329,\"y\":13.28},{\"x\":1685243671340,\"y\":13.34},{\"x\":1685243971443,\"y\":13.34},{\"x\":1685244271334,\"y\":13.34},{\"x\":1685244571292,\"y\":13.34},{\"x\":1685244871333,\"y\":13.47},{\"x\":1685245171537,\"y\":13.69},{\"x\":1685245471361,\"y\":13.69},{\"x\":1685245771298,\"y\":13.69},{\"x\":1685246071304,\"y\":13.6},{\"x\":1685246371537,\"y\":13.65},{\"x\":1685246671321,\"y\":13.56},{\"x\":1685246971335,\"y\":13.65},{\"x\":1685247271382,\"y\":13.48},{\"x\":1685247571470,\"y\":13.48},{\"x\":1685247871384,\"y\":13.28},{\"x\":1685248171411,\"y\":13.28},{\"x\":1685248471353,\"y\":13.24},{\"x\":1685248771403,\"y\":13.24},{\"x\":1685249071402,\"y\":13.23},{\"x\":1685249371376,\"y\":13.23},{\"x\":1685249671393,\"y\":13.23},{\"x\":1685249971493,\"y\":13.17},{\"x\":1685250271426,\"y\":13.08},{\"x\":1685250571467,\"y\":13.11},{\"x\":1685250871415,\"y\":13.14},{\"x\":1685251171439,\"y\":13.14},{\"x\":1685251471413,\"y\":13.14},{\"x\":1685251771409,\"y\":13.14},{\"x\":1685252071420,\"y\":13.19},{\"x\":1685252371615,\"y\":13.25},{\"x\":1685252671425,\"y\":13.17},{\"x\":1685252971434,\"y\":13.18},{\"x\":1685253271482,\"y\":13.38},{\"x\":1685253571596,\"y\":13.3},{\"x\":1685253871453,\"y\":13.38},{\"x\":1685254171462,\"y\":13.45},{\"x\":1685254471470,\"y\":13.51},{\"x\":1685254771721,\"y\":13.58},{\"x\":1685255071473,\"y\":13.58},{\"x\":1685255371478,\"y\":13.58},{\"x\":1685255671578,\"y\":13.78},{\"x\":1685255971776,\"y\":13.54},{\"x\":1685256271512,\"y\":13.78},{\"x\":1685256571498,\"y\":13.76},{\"x\":1685256871502,\"y\":13.87},{\"x\":1685257171620,\"y\":13.87},{\"x\":1685257471509,\"y\":14.11},{\"x\":1685257771511,\"y\":14.36},{\"x\":1685258071508,\"y\":14.36},{\"x\":1685258371764,\"y\":14.39},{\"x\":1685258671517,\"y\":14.56},{\"x\":1685258971516,\"y\":14.56},{\"x\":1685259271515,\"y\":14.92},{\"x\":1685259571742,\"y\":14.92},{\"x\":1685259871562,\"y\":15.01},{\"x\":1685260171531,\"y\":15.01},{\"x\":1685260471534,\"y\":15.01},{\"x\":1685260771696,\"y\":14.85},{\"x\":1685261071539,\"y\":15.04},{\"x\":1685261371589,\"y\":15.04},{\"x\":1685261671538,\"y\":15.4},{\"x\":1685261971867,\"y\":15.59},{\"x\":1685262271550,\"y\":15.59},{\"x\":1685262571548,\"y\":15.59},{\"x\":1685262871567,\"y\":15.46},{\"x\":1685263171794,\"y\":15.73},{\"x\":1685263471562,\"y\":15.69},{\"x\":1685263771602,\"y\":15.85},{\"x\":1685264071565,\"y\":15.71},{\"x\":1685264371846,\"y\":15.71},{\"x\":1685264671596,\"y\":15.72},{\"x\":1685264971615,\"y\":15.73},{\"x\":1685265271570,\"y\":15.73},{\"x\":1685265571596,\"y\":15.65},{\"x\":1685265871585,\"y\":15.47},{\"x\":1685266171591,\"y\":15.47},{\"x\":1685266471584,\"y\":15.48},{\"x\":1685266771720,\"y\":15.72},{\"x\":1685267071581,\"y\":15.72},{\"x\":1685267371587,\"y\":15.9},{\"x\":1685267671638,\"y\":16.17},{\"x\":1685267971896,\"y\":16.17},{\"x\":1685268271589,\"y\":15.98},{\"x\":1685268571603,\"y\":16.37},{\"x\":1685268871604,\"y\":16.37},{\"x\":1685269171897,\"y\":16.65},{\"x\":1685269471607,\"y\":16.58},{\"x\":1685269771613,\"y\":16.58},{\"x\":1685270071662,\"y\":16.41},{\"x\":1685270371882,\"y\":16.41},{\"x\":1685270671617,\"y\":16.41},{\"x\":1685270971621,\"y\":16.28},{\"x\":1685271271686,\"y\":16.19},{\"x\":1685271571713,\"y\":16.22},{\"x\":1685271871621,\"y\":16.15},{\"x\":1685272171630,\"y\":16.22},{\"x\":1685272471630,\"y\":15.92},{\"x\":1685272771817,\"y\":15.96},{\"x\":1685273071635,\"y\":15.96},{\"x\":1685273371650,\"y\":16.12},{\"x\":1685273671673,\"y\":15.95},{\"x\":1685273971701,\"y\":16.31},{\"x\":1685274271656,\"y\":16.31},{\"x\":1685274571692,\"y\":16.32},{\"x\":1685274871686,\"y\":16.32},{\"x\":1685275171921,\"y\":16.73},{\"x\":1685275471665,\"y\":16.65},{\"x\":1685275771698,\"y\":16.73},{\"x\":1685276071659,\"y\":16.22},{\"x\":1685276371756,\"y\":16.22},{\"x\":1685276671662,\"y\":16.48},{\"x\":1685276971675,\"y\":15.89},{\"x\":1685277271661,\"y\":15.89},{\"x\":1685277571970,\"y\":15.63},{\"x\":1685277871714,\"y\":15.68},{\"x\":1685278171675,\"y\":15.63},{\"x\":1685278471667,\"y\":15.56},{\"x\":1685278771920,\"y\":15.65},{\"x\":1685279071689,\"y\":15.87},{\"x\":1685279371671,\"y\":15.87},{\"x\":1685279671675,\"y\":15.87},{\"x\":1685279972052,\"y\":15.71},{\"x\":1685280271668,\"y\":15.71},{\"x\":1685280571670,\"y\":16.04},{\"x\":1685280871676,\"y\":16.04},{\"x\":1685281671888,\"y\":15.97},{\"x\":1685284096032,\"y\":15.5},{\"x\":1685284395921,\"y\":15.5},{\"x\":1685284695933,\"y\":15.5},{\"x\":1685284995930,\"y\":15.47},{\"x\":1685285295935,\"y\":15.47},{\"x\":1685285595944,\"y\":15.8},{\"x\":1685285895939,\"y\":15.8},{\"x\":1685286195949,\"y\":15.51},{\"x\":1685286495951,\"y\":15.51},{\"x\":1685286796009,\"y\":15.36},{\"x\":1685287095965,\"y\":15.27},{\"x\":1685287395953,\"y\":15.27},{\"x\":1685287695970,\"y\":15.36},{\"x\":1685287995955,\"y\":15.36},{\"x\":1685288295961,\"y\":15.34},{\"x\":1685288595996,\"y\":14.97},{\"x\":1685288895968,\"y\":14.97},{\"x\":1685289196133,\"y\":14.79},{\"x\":1685289495999,\"y\":14.79},{\"x\":1685289795972,\"y\":14.69},{\"x\":1685290095973,\"y\":14.69},{\"x\":1685290395973,\"y\":14.69},{\"x\":1685290695981,\"y\":14.79},{\"x\":1685290996003,\"y\":14.79},{\"x\":1685291295989,\"y\":14.79},{\"x\":1685291595985,\"y\":14.77},{\"x\":1685291896008,\"y\":14.5},{\"x\":1685292196008,\"y\":14.5},{\"x\":1685292496041,\"y\":14.55},{\"x\":1685292796005,\"y\":14.47},{\"x\":1685293096013,\"y\":14.55},{\"x\":1685293396082,\"y\":14.49},{\"x\":1685293696011,\"y\":14.45},{\"x\":1685293996026,\"y\":14.65},{\"x\":1685294296016,\"y\":14.65},{\"x\":1685294596031,\"y\":14.43},{\"x\":1685294896026,\"y\":14.43},{\"x\":1685295196031,\"y\":14.42},{\"x\":1685295496032,\"y\":14.4},{\"x\":1685295796073,\"y\":14.53},{\"x\":1685296096031,\"y\":14.53},{\"x\":1685296396105,\"y\":14.32},{\"x\":1685296696030,\"y\":14.24},{\"x\":1685296996047,\"y\":14.24},{\"x\":1685297296080,\"y\":14.48},{\"x\":1685297596038,\"y\":14.4},{\"x\":1685297896041,\"y\":14.48},{\"x\":1685298196059,\"y\":14.08},{\"x\":1685298496041,\"y\":14.08},{\"x\":1685298796049,\"y\":14.33},{\"x\":1685299096058,\"y\":14},{\"x\":1685299396061,\"y\":13.59},{\"x\":1685299696048,\"y\":13.56},{\"x\":1685299996049,\"y\":13.56},{\"x\":1685300296111,\"y\":13.8},{\"x\":1685300596061,\"y\":13.71},{\"x\":1685300896068,\"y\":13.71},{\"x\":1685301196056,\"y\":13.49},{\"x\":1685301496064,\"y\":13.49},{\"x\":1685301796076,\"y\":13.49},{\"x\":1685302096103,\"y\":13.4},{\"x\":1685302396063,\"y\":13.46},{\"x\":1685302696077,\"y\":13.34},{\"x\":1685302996145,\"y\":13.04},{\"x\":1685303296067,\"y\":12.95},{\"x\":1685303596073,\"y\":13.04},{\"x\":1685303896072,\"y\":12.69},{\"x\":1685304196079,\"y\":12.67},{\"x\":1685304496103,\"y\":12.49},{\"x\":1685304796082,\"y\":12.49},{\"x\":1685305096071,\"y\":12.49},{\"x\":1685305396094,\"y\":12.49},{\"x\":1685305696124,\"y\":12.2},{\"x\":1685305996075,\"y\":12.14},{\"x\":1685306296121,\"y\":11.8},{\"x\":1685306596072,\"y\":11.8},{\"x\":1685306896081,\"y\":11.8},{\"x\":1685307196124,\"y\":11.34},{\"x\":1685307496091,\"y\":11.34},{\"x\":1685307797175,\"y\":11.28},{\"x\":1685308096131,\"y\":11.3},{\"x\":1685308396095,\"y\":11.28},{\"x\":1685308696095,\"y\":11.3},{\"x\":1685308996098,\"y\":11.22},{\"x\":1685309296119,\"y\":11},{\"x\":1685309596104,\"y\":11},{\"x\":1685309896135,\"y\":10.5},{\"x\":1685310196123,\"y\":10.5},{\"x\":1685310496134,\"y\":10.5},{\"x\":1685310796104,\"y\":10.45},{\"x\":1685311096115,\"y\":10.45},{\"x\":1685311396137,\"y\":10.24},{\"x\":1685311696121,\"y\":10.24},{\"x\":1685311996202,\"y\":10.21},{\"x\":1685312296125,\"y\":10.21},{\"x\":1685312596146,\"y\":10.21},{\"x\":1685312896148,\"y\":9.93},{\"x\":1685313196132,\"y\":10.07},{\"x\":1685313496203,\"y\":9.89},{\"x\":1685313796150,\"y\":9.83},{\"x\":1685314096147,\"y\":9.75},{\"x\":1685314396195,\"y\":9.81},{\"x\":1685314696202,\"y\":9.81},{\"x\":1685314996169,\"y\":9.68},{\"x\":1685315296183,\"y\":9.65},{\"x\":1685315596155,\"y\":9.37},{\"x\":1685315896207,\"y\":9.43},{\"x\":1685316196167,\"y\":9.43},{\"x\":1685316496181,\"y\":9.32},{\"x\":1685316796216,\"y\":9.22},{\"x\":1685317096164,\"y\":9.22},{\"x\":1685317396213,\"y\":8.84},{\"x\":1685317696170,\"y\":8.76},{\"x\":1685317996185,\"y\":8.81},{\"x\":1685318296172,\"y\":8.72},{\"x\":1685318596191,\"y\":8.81},{\"x\":1685318896205,\"y\":8.99},{\"x\":1685319196165,\"y\":8.99},{\"x\":1685319496187,\"y\":8.9},{\"x\":1685319796169,\"y\":8.9},{\"x\":1685320096204,\"y\":8.99},{\"x\":1685320396170,\"y\":8.7},{\"x\":1685320696174,\"y\":8.65},{\"x\":1685320996175,\"y\":8.65},{\"x\":1685321296205,\"y\":8.41},{\"x\":1685321596177,\"y\":8.41},{\"x\":1685321896203,\"y\":8.41},{\"x\":1685322196246,\"y\":8.09},{\"x\":1685322496225,\"y\":7.88},{\"x\":1685322796181,\"y\":7.88},{\"x\":1685323096176,\"y\":7.73},{\"x\":1685323396175,\"y\":7.73},{\"x\":1685323696184,\"y\":7.67},{\"x\":1685323996186,\"y\":7.67},{\"x\":1685324296221,\"y\":7.83},{\"x\":1685324596190,\"y\":7.74},{\"x\":1685324896222,\"y\":7.83},{\"x\":1685325196203,\"y\":7.74},{\"x\":1685325496188,\"y\":7.32},{\"x\":1685325796536,\"y\":7.32},{\"x\":1685326096185,\"y\":7.32},{\"x\":1685326396231,\"y\":7.41},{\"x\":1685326696191,\"y\":7.41},{\"x\":1685326996194,\"y\":7.41},{\"x\":1685327296195,\"y\":7.29},{\"x\":1685327596190,\"y\":7.3},{\"x\":1685327896227,\"y\":7.33},{\"x\":1685328196279,\"y\":7.33},{\"x\":1685328496276,\"y\":7.33},{\"x\":1685328796240,\"y\":7.2},{\"x\":1685329096191,\"y\":7.2},{\"x\":1685329396192,\"y\":7.11},{\"x\":1685329696248,\"y\":7.2},{\"x\":1685329996247,\"y\":7.2},{\"x\":1685330296206,\"y\":7.2},{\"x\":1685330596222,\"y\":7.11},{\"x\":1685330896241,\"y\":7.37},{\"x\":1685331196223,\"y\":7.37},{\"x\":1685331496227,\"y\":7.37},{\"x\":1685331796287,\"y\":7.54},{\"x\":1685332096241,\"y\":7.54},{\"x\":1685332396262,\"y\":7.25},{\"x\":1685332696288,\"y\":7.34},{\"x\":1685332996282,\"y\":7.25},{\"x\":1685333296250,\"y\":7.34},{\"x\":1685333596314,\"y\":7.3},{\"x\":1685333896248,\"y\":7.3},{\"x\":1685334196250,\"y\":7.22},{\"x\":1685334496287,\"y\":7.3},{\"x\":1685334796262,\"y\":7.3},{\"x\":1685335096310,\"y\":7.34},{\"x\":1685335396322,\"y\":7.34},{\"x\":1685335696328,\"y\":7.35},{\"x\":1685335996277,\"y\":7.35},{\"x\":1685336296341,\"y\":7.46},{\"x\":1685336596305,\"y\":7.46},{\"x\":1685336896288,\"y\":7.46},{\"x\":1685337196337,\"y\":7.77},{\"x\":1685337496307,\"y\":7.68},{\"x\":1685337796344,\"y\":7.77},{\"x\":1685338096310,\"y\":7.7},{\"x\":1685338396309,\"y\":7.7},{\"x\":1685338696313,\"y\":7.87},{\"x\":1685338996338,\"y\":7.91},{\"x\":1685339296305,\"y\":8.24},{\"x\":1685339596306,\"y\":8.24},{\"x\":1685339896363,\"y\":8.87},{\"x\":1685340196325,\"y\":8.81},{\"x\":1685340496328,\"y\":8.87},{\"x\":1685340796324,\"y\":8.93},{\"x\":1685341096323,\"y\":8.93},{\"x\":1685341396403,\"y\":9.54},{\"x\":1685341696331,\"y\":9.46},{\"x\":1685341996329,\"y\":9.48},{\"x\":1685342296334,\"y\":9.53},{\"x\":1685342596385,\"y\":9.94},{\"x\":1685342896330,\"y\":9.94},{\"x\":1685343196338,\"y\":10.15},{\"x\":1685343496342,\"y\":10.74},{\"x\":1685343796354,\"y\":10.74},{\"x\":1685344096339,\"y\":11.17},{\"x\":1685344396346,\"y\":11.17},{\"x\":1685344697407,\"y\":11.29},{\"x\":1685344996353,\"y\":11.26},{\"x\":1685345296424,\"y\":11.74},{\"x\":1685345596406,\"y\":11.65},{\"x\":1685345896351,\"y\":11.65},{\"x\":1685346196416,\"y\":12.07},{\"x\":1685346496410,\"y\":12.16},{\"x\":1685346796378,\"y\":12.07},{\"x\":1685347096406,\"y\":12.03},{\"x\":1685347396359,\"y\":12.03},{\"x\":1685347696378,\"y\":12.03},{\"x\":1685347996394,\"y\":12.1},{\"x\":1685348296366,\"y\":12.1},{\"x\":1685348596360,\"y\":12.02},{\"x\":1685348896369,\"y\":12.44},{\"x\":1685349196480,\"y\":12.49},{\"x\":1685349496365,\"y\":12.49},{\"x\":1685349796376,\"y\":12.49},{\"x\":1685350096394,\"y\":12.97},{\"x\":1685350396369,\"y\":12.97},{\"x\":1685350696451,\"y\":13.05},{\"x\":1685350996421,\"y\":13.19},{\"x\":1685351296370,\"y\":13.05},{\"x\":1685351596436,\"y\":13.25},{\"x\":1685351896375,\"y\":13.25},{\"x\":1685352196381,\"y\":13.25},{\"x\":1685352496379,\"y\":13.28},{\"x\":1685352796390,\"y\":13.28},{\"x\":1685353096503,\"y\":13.97},{\"x\":1685353396412,\"y\":13.97},{\"x\":1685353696392,\"y\":13.97},{\"x\":1685353996429,\"y\":14.15},{\"x\":1685354296437,\"y\":14.15},{\"x\":1685354596450,\"y\":14.15},{\"x\":1685354896393,\"y\":14.3},{\"x\":1685355196401,\"y\":14.3},{\"x\":1685355496440,\"y\":14.36},{\"x\":1685355796402,\"y\":14.36},{\"x\":1685356096449,\"y\":14.32},{\"x\":1685356396391,\"y\":14.24},{\"x\":1685356696401,\"y\":14.32},{\"x\":1685356996401,\"y\":14.61},{\"x\":1685357296408,\"y\":14.61},{\"x\":1685357596403,\"y\":15.01},{\"x\":1685357896429,\"y\":15.59},{\"x\":1685358196393,\"y\":15.59},{\"x\":1685358496468,\"y\":15.59},{\"x\":1685358796424,\"y\":16},{\"x\":1685359096400,\"y\":16},{\"x\":1685359396448,\"y\":16.11},{\"x\":1685359696410,\"y\":16.08},{\"x\":1685359996404,\"y\":16.08},{\"x\":1685360296411,\"y\":16.35},{\"x\":1685360596442,\"y\":16.35},{\"x\":1685360896460,\"y\":16.44},{\"x\":1685361196442,\"y\":16.44},{\"x\":1685361496409,\"y\":16.44},{\"x\":1685361796427,\"y\":16.52},{\"x\":1685362096416,\"y\":16.62},{\"x\":1685362396413,\"y\":16.67},{\"x\":1685362696411,\"y\":16.68},{\"x\":1685362996461,\"y\":16.79},{\"x\":1685363296411,\"y\":16.85},{\"x\":1685363596420,\"y\":16.79},{\"x\":1685363896467,\"y\":16.76},{\"x\":1685364196435,\"y\":16.76},{\"x\":1685364496434,\"y\":16.76},{\"x\":1685364796430,\"y\":16.68},{\"x\":1685365096423,\"y\":17.1},{\"x\":1685365396432,\"y\":17.1},{\"x\":1685365696497,\"y\":17.46},{\"x\":1685365996459,\"y\":17.44},{\"x\":1685366296423,\"y\":17.46},{\"x\":1685366596445,\"y\":17.44},{\"x\":1685366896437,\"y\":17.38},{\"x\":1685367196433,\"y\":17.38},{\"x\":1685367496432,\"y\":17.38},{\"x\":1685367796435,\"y\":17.27},{\"x\":1685368096426,\"y\":17.27},{\"x\":1685368396429,\"y\":17.26},{\"x\":1685368696436,\"y\":17.26},{\"x\":1685368996438,\"y\":17.44},{\"x\":1685369296428,\"y\":17.32},{\"x\":1685369596501,\"y\":17.5},{\"x\":1685369896441,\"y\":17.42},{\"x\":1685370196509,\"y\":17.5},{\"x\":1685370496485,\"y\":17.7},{\"x\":1685370796502,\"y\":17.7},{\"x\":1685371096455,\"y\":17.7},{\"x\":1685371396510,\"y\":17.39},{\"x\":1685371696485,\"y\":17.39},{\"x\":1685371996502,\"y\":17.64},{\"x\":1685372296476,\"y\":17.31},{\"x\":1685372596474,\"y\":17.78},{\"x\":1685372896482,\"y\":17.78},{\"x\":1685373196484,\"y\":17.78},{\"x\":1685373496572,\"y\":17.86},{\"x\":1685373796514,\"y\":17.78},{\"x\":1685374096510,\"y\":17.81},{\"x\":1685374396544,\"y\":17.86},{\"x\":1685374696519,\"y\":17.78},{\"x\":1685374996566,\"y\":17.78},{\"x\":1685375296542,\"y\":17.89},{\"x\":1685375596519,\"y\":17.89},{\"x\":1685375896531,\"y\":17.89},{\"x\":1685376196570,\"y\":17.89},{\"x\":1685376496581,\"y\":18.04},{\"x\":1685376796589,\"y\":18.04},{\"x\":1685377096547,\"y\":17.74},{\"x\":1685377396613,\"y\":17.74},{\"x\":1685377696596,\"y\":17.6},{\"x\":1685377996542,\"y\":17.51},{\"x\":1685378296565,\"y\":17.51},{\"x\":1685378596624,\"y\":17.51},{\"x\":1685378896627,\"y\":17.42},{\"x\":1685379196643,\"y\":17.2},{\"x\":1685379496586,\"y\":17.42},{\"x\":1685379796637,\"y\":17.09},{\"x\":1685380096596,\"y\":17.09},{\"x\":1685380396635,\"y\":17.11},{\"x\":1685380696607,\"y\":17.04},{\"x\":1685380996713,\"y\":17.08},{\"x\":1685381296598,\"y\":16.99},{\"x\":1685381596615,\"y\":17.08},{\"x\":1685381896608,\"y\":17.03},{\"x\":1685382196647,\"y\":17.03},{\"x\":1685382496621,\"y\":16.09},{\"x\":1685382796622,\"y\":16.25},{\"x\":1685383096636,\"y\":16.25},{\"x\":1685383396656,\"y\":16.25},{\"x\":1685383696677,\"y\":16.1},{\"x\":1685383996640,\"y\":16.09},{\"x\":1685384296634,\"y\":16.09},{\"x\":1685384596702,\"y\":16.09},{\"x\":1685384896755,\"y\":15.72},{\"x\":1685385196657,\"y\":15.7},{\"x\":1685385496648,\"y\":15.72},{\"x\":1685385796721,\"y\":14.94},{\"x\":1685386096661,\"y\":14.89},{\"x\":1685386396764,\"y\":14.48},{\"x\":1685386696644,\"y\":14.48},{\"x\":1685386996711,\"y\":14.39},{\"x\":1685387296665,\"y\":14.47},{\"x\":1685387596687,\"y\":14.47},{\"x\":1685387896719,\"y\":14.45},{\"x\":1685388196746,\"y\":14.37},{\"x\":1685388496677,\"y\":14.36},{\"x\":1685388796728,\"y\":14.16},{\"x\":1685389096728,\"y\":14.95},{\"x\":1685389396772,\"y\":15.89},{\"x\":1685389696700,\"y\":15.8},{\"x\":1685389996694,\"y\":15.8},{\"x\":1685390296741,\"y\":13.52},{\"x\":1685390596769,\"y\":13.52},{\"x\":1685390896706,\"y\":13.52},{\"x\":1685391196708,\"y\":12.38},{\"x\":1685391496778,\"y\":12.52},{\"x\":1685391796848,\"y\":12.52},{\"x\":1685392096805,\"y\":12.37},{\"x\":1685392396723,\"y\":12.01},{\"x\":1685392696723,\"y\":12.01},{\"x\":1685392996799,\"y\":11.33},{\"x\":1685393296731,\"y\":11.58},{\"x\":1685393596725,\"y\":11.33},{\"x\":1685393896759,\"y\":11.64},{\"x\":1685394196778,\"y\":11.64},{\"x\":1685394496727,\"y\":11.63},{\"x\":1685394796838,\"y\":11},{\"x\":1685395096733,\"y\":10.92},{\"x\":1685395396756,\"y\":10.84},{\"x\":1685395696744,\"y\":10.81},{\"x\":1685395996738,\"y\":10.81},{\"x\":1685396296745,\"y\":10.81},{\"x\":1685396596863,\"y\":9.92},{\"x\":1685396896748,\"y\":9.92},{\"x\":1685397196744,\"y\":9.92},{\"x\":1685397496752,\"y\":9.84},{\"x\":1685397797031,\"y\":9.29},{\"x\":1685398096745,\"y\":9.59},{\"x\":1685398396778,\"y\":9.29},{\"x\":1685398696753,\"y\":9.67},{\"x\":1685398996882,\"y\":9.8},{\"x\":1685399296749,\"y\":9.67},{\"x\":1685399596872,\"y\":9.64},{\"x\":1685399896856,\"y\":9.29},{\"x\":1685400196853,\"y\":9.29},{\"x\":1685400496758,\"y\":9.29},{\"x\":1685400796806,\"y\":8.88},{\"x\":1685401096749,\"y\":8.88},{\"x\":1685401396853,\"y\":8.88},{\"x\":1685401696823,\"y\":8.87},{\"x\":1685401996749,\"y\":8.93},{\"x\":1685402296834,\"y\":8.83},{\"x\":1685402596862,\"y\":8.83},{\"x\":1685402896766,\"y\":8.83},{\"x\":1685403196764,\"y\":8.69},{\"x\":1685403496778,\"y\":8.79},{\"x\":1685403796902,\"y\":8.76},{\"x\":1685404096775,\"y\":8.76},{\"x\":1685404396829,\"y\":8.45},{\"x\":1685404696790,\"y\":8.45},{\"x\":1685404996816,\"y\":8.36},{\"x\":1685405296883,\"y\":8.71},{\"x\":1685405596870,\"y\":8.7},{\"x\":1685405896793,\"y\":8.63},{\"x\":1685406196983,\"y\":8.7},{\"x\":1685406496794,\"y\":8.47},{\"x\":1685406796799,\"y\":8.47},{\"x\":1685407096806,\"y\":8.41},{\"x\":1685407396889,\"y\":8.27},{\"x\":1685407696814,\"y\":8.19},{\"x\":1685407996818,\"y\":8.27},{\"x\":1685408296823,\"y\":8.19},{\"x\":1685408596913,\"y\":8.19},{\"x\":1685408896867,\"y\":7.82},{\"x\":1685409196827,\"y\":7.82},{\"x\":1685409496885,\"y\":7.78},{\"x\":1685409796923,\"y\":7.69},{\"x\":1685410096833,\"y\":7.69},{\"x\":1685410396863,\"y\":7.68},{\"x\":1685410696853,\"y\":7.69},{\"x\":1685410996931,\"y\":7.59},{\"x\":1685411296850,\"y\":7.59},{\"x\":1685411596860,\"y\":7.59},{\"x\":1685411896863,\"y\":7.34},{\"x\":1685412197065,\"y\":7.41},{\"x\":1685412496865,\"y\":7.34},{\"x\":1685412796871,\"y\":7.34},{\"x\":1685413096914,\"y\":7.41},{\"x\":1685413396986,\"y\":7.41},{\"x\":1685413696881,\"y\":7.41},{\"x\":1685413996886,\"y\":7.35},{\"x\":1685414296923,\"y\":7.82},{\"x\":1685414597013,\"y\":7.95},{\"x\":1685414896976,\"y\":7.95},{\"x\":1685415196895,\"y\":7.95},{\"x\":1685415496889,\"y\":7.95},{\"x\":1685415796967,\"y\":8.17},{\"x\":1685416096899,\"y\":8.17},{\"x\":1685416396952,\"y\":8.17},{\"x\":1685416696900,\"y\":8.09},{\"x\":1685416997056,\"y\":8.23},{\"x\":1685417296914,\"y\":8.21},{\"x\":1685417596918,\"y\":8.23},{\"x\":1685417896941,\"y\":8.11},{\"x\":1685418197072,\"y\":8.11},{\"x\":1685418496972,\"y\":7.78},{\"x\":1685418796976,\"y\":7.78},{\"x\":1685419096917,\"y\":7.78},{\"x\":1685419397122,\"y\":7.76},{\"x\":1685419696925,\"y\":7.7},{\"x\":1685419996981,\"y\":7.76},{\"x\":1685420296927,\"y\":7.98},{\"x\":1685420597159,\"y\":7.68},{\"x\":1685420896981,\"y\":8.25},{\"x\":1685421196938,\"y\":8.25},{\"x\":1685421496968,\"y\":9.25},{\"x\":1685421797058,\"y\":8.95},{\"x\":1685422096935,\"y\":8.95},{\"x\":1685422397004,\"y\":9.27},{\"x\":1685422696956,\"y\":9.27},{\"x\":1685422997188,\"y\":9.29},{\"x\":1685423296955,\"y\":9.29},{\"x\":1685423597001,\"y\":9.11},{\"x\":1685423896956,\"y\":9.02},{\"x\":1685424197012,\"y\":9.02},{\"x\":1685424497000,\"y\":9.11},{\"x\":1685424796971,\"y\":9.02},{\"x\":1685425096964,\"y\":10.04},{\"x\":1685425397109,\"y\":10.04},{\"x\":1685425696964,\"y\":10.02},{\"x\":1685425996974,\"y\":9.99},{\"x\":1685426296965,\"y\":10.2},{\"x\":1685426597100,\"y\":10.28},{\"x\":1685426896989,\"y\":10.33},{\"x\":1685427196988,\"y\":10.33},{\"x\":1685427497031,\"y\":10.45},{\"x\":1685427797156,\"y\":10.38},{\"x\":1685428097083,\"y\":10.94},{\"x\":1685428396991,\"y\":10.94},{\"x\":1685428696996,\"y\":10.82},{\"x\":1685428997105,\"y\":11.01},{\"x\":1685429296992,\"y\":11.01},{\"x\":1685429597030,\"y\":11.01},{\"x\":1685429896994,\"y\":11.04},{\"x\":1685430197076,\"y\":11.04},{\"x\":1685430497010,\"y\":11.39},{\"x\":1685430796999,\"y\":11.6},{\"x\":1685431097007,\"y\":11.55},{\"x\":1685431397249,\"y\":11.81},{\"x\":1685431697002,\"y\":11.81},{\"x\":1685431997013,\"y\":11.81},{\"x\":1685432297009,\"y\":11.82},{\"x\":1685432597030,\"y\":11.82},{\"x\":1685432897132,\"y\":12.15},{\"x\":1685433197016,\"y\":12.07},{\"x\":1685433497009,\"y\":12.15},{\"x\":1685433797139,\"y\":12.16},{\"x\":1685434097016,\"y\":12.19},{\"x\":1685434397064,\"y\":12.22},{\"x\":1685434697027,\"y\":12.14},{\"x\":1685434997238,\"y\":12.14},{\"x\":1685435297077,\"y\":12.32},{\"x\":1685435597014,\"y\":12.33},{\"x\":1685435897071,\"y\":12.41},{\"x\":1685436197195,\"y\":12.35},{\"x\":1685436497013,\"y\":12.35},{\"x\":1685436797015,\"y\":12.32},{\"x\":1685437097050,\"y\":12.32},{\"x\":1685437397237,\"y\":12.33},{\"x\":1685437697030,\"y\":12.33},{\"x\":1685437997069,\"y\":12.27},{\"x\":1685438297034,\"y\":12.19},{\"x\":1685438597203,\"y\":12.17},{\"x\":1685438897033,\"y\":12.34},{\"x\":1685439197064,\"y\":12.31},{\"x\":1685439497033,\"y\":12.32},{\"x\":1685439797107,\"y\":12.31},{\"x\":1685440097051,\"y\":13.11},{\"x\":1685440397038,\"y\":13.21},{\"x\":1685440697073,\"y\":13.37},{\"x\":1685440997179,\"y\":13.19},{\"x\":1685441297048,\"y\":13.28},{\"x\":1685441597041,\"y\":13.19},{\"x\":1685441897039,\"y\":13.29},{\"x\":1685442197094,\"y\":13.29},{\"x\":1685442497030,\"y\":13.33},{\"x\":1685442797045,\"y\":13.33},{\"x\":1685443097030,\"y\":13.2},{\"x\":1685443397349,\"y\":13.31},{\"x\":1685443697027,\"y\":13.31},{\"x\":1685443997092,\"y\":13.23},{\"x\":1685444297033,\"y\":13.77},{\"x\":1685444597073,\"y\":13.68},{\"x\":1685444897078,\"y\":16.19},{\"x\":1685445197034,\"y\":16.19},{\"x\":1685445497068,\"y\":16.19},{\"x\":1685445797209,\"y\":16.19},{\"x\":1685446097058,\"y\":16.19},{\"x\":1685446397051,\"y\":17.22},{\"x\":1685446697059,\"y\":17.22},{\"x\":1685446997166,\"y\":18.41},{\"x\":1685447297115,\"y\":18.41},{\"x\":1685447597112,\"y\":18.41},{\"x\":1685447897089,\"y\":18.41},{\"x\":1685448197159,\"y\":12.74},{\"x\":1685448497098,\"y\":12.72},{\"x\":1685448797127,\"y\":12.89},{\"x\":1685449097105,\"y\":12.97},{\"x\":1685449397373,\"y\":13.15},{\"x\":1685449697109,\"y\":13.03},{\"x\":1685449997136,\"y\":13.06},{\"x\":1685450297128,\"y\":13.06},{\"x\":1685450597387,\"y\":13.06},{\"x\":1685450897183,\"y\":13.21},{\"x\":1685451197144,\"y\":13.21},{\"x\":1685451497142,\"y\":13.33},{\"x\":1685451797185,\"y\":13.33},{\"x\":1685452097148,\"y\":13.57},{\"x\":1685452397249,\"y\":13.64},{\"x\":1685452697161,\"y\":13.64},{\"x\":1685452997474,\"y\":13.64},{\"x\":1685453297206,\"y\":13.97},{\"x\":1685453597173,\"y\":13.69},{\"x\":1685453897183,\"y\":13.94},{\"x\":1685454197215,\"y\":13.94},{\"x\":1685454497192,\"y\":14.16},{\"x\":1685454797260,\"y\":14.48},{\"x\":1685455097202,\"y\":14.67},{\"x\":1685455397476,\"y\":14.67},{\"x\":1685455697209,\"y\":14.86},{\"x\":1685455997240,\"y\":15.03},{\"x\":1685456297262,\"y\":15.18},{\"x\":1685456597249,\"y\":15.18},{\"x\":1685456897228,\"y\":15.09},{\"x\":1685457197269,\"y\":15.55},{\"x\":1685457497236,\"y\":15.55},{\"x\":1685457797465,\"y\":15.55},{\"x\":1685458097286,\"y\":16.03},{\"x\":1685458397242,\"y\":16.03},{\"x\":1685458697267,\"y\":16.03},{\"x\":1685458997286,\"y\":15.84},{\"x\":1685459297248,\"y\":15.84},{\"x\":1685459597268,\"y\":16.22},{\"x\":1685459897263,\"y\":16.22},{\"x\":1685460197357,\"y\":16.94},{\"x\":1685460497301,\"y\":16.94},{\"x\":1685460797266,\"y\":16.94},{\"x\":1685461097305,\"y\":16.95},{\"x\":1685461397544,\"y\":17.05},{\"x\":1685461697280,\"y\":16.95},{\"x\":1685461997291,\"y\":16.97},{\"x\":1685462297287,\"y\":16.97},{\"x\":1685462597465,\"y\":17.35},{\"x\":1685462897303,\"y\":17.35},{\"x\":1685463197366,\"y\":17.2},{\"x\":1685463737229,\"y\":17.2},{\"x\":1685464037277,\"y\":17.14},{\"x\":1685464337245,\"y\":17.14},{\"x\":1685464637256,\"y\":17.03},{\"x\":1685464937262,\"y\":17.14},{\"x\":1685465237316,\"y\":17.08},{\"x\":1685465537294,\"y\":17.08},{\"x\":1685465837334,\"y\":17.31},{\"x\":1685466137337,\"y\":17.31},{\"x\":1685466437310,\"y\":17.03},{\"x\":1685466737354,\"y\":17.04},{\"x\":1685467037407,\"y\":16.96},{\"x\":1685467337326,\"y\":17.16},{\"x\":1685467637328,\"y\":17.13},{\"x\":1685467937333,\"y\":16.81},{\"x\":1685468237339,\"y\":16.48},{\"x\":1685468537360,\"y\":16.48},{\"x\":1685468837368,\"y\":16.47},{\"x\":1685469137413,\"y\":15.9},{\"x\":1685469437370,\"y\":16.04},{\"x\":1685469737372,\"y\":15.9},{\"x\":1685470037427,\"y\":15.82},{\"x\":1685470337392,\"y\":15.82},{\"x\":1685470637406,\"y\":15.82},{\"x\":1685470937407,\"y\":15.69},{\"x\":1685471237416,\"y\":15.83},{\"x\":1685471537421,\"y\":15.83},{\"x\":1685471837423,\"y\":15.95},{\"x\":1685472137555,\"y\":15.16},{\"x\":1685472437434,\"y\":15.08},{\"x\":1685472737439,\"y\":14.87},{\"x\":1685473037727,\"y\":14.79},{\"x\":1685473337523,\"y\":15.1},{\"x\":1685473637452,\"y\":14.79},{\"x\":1685473937500,\"y\":14.51},{\"x\":1685474237504,\"y\":14.29},{\"x\":1685474537449,\"y\":14.29},{\"x\":1685474837471,\"y\":14.21},{\"x\":1685475137479,\"y\":14.06},{\"x\":1685475437511,\"y\":13.27},{\"x\":1685475737497,\"y\":13.27},{\"x\":1685476037496,\"y\":13.19},{\"x\":1685476337572,\"y\":13.61},{\"x\":1685476637511,\"y\":13.49},{\"x\":1685476937513,\"y\":13.47},{\"x\":1685477237528,\"y\":13.42},{\"x\":1685477537526,\"y\":13.44},{\"x\":1685477837530,\"y\":13.44},{\"x\":1685478137582,\"y\":13.03},{\"x\":1685478437532,\"y\":13.14},{\"x\":1685478737565,\"y\":12.83},{\"x\":1685479037545,\"y\":12.91},{\"x\":1685479337555,\"y\":12.59},{\"x\":1685479637566,\"y\":12.56},{\"x\":1685479937595,\"y\":12.5},{\"x\":1685480237565,\"y\":12.61},{\"x\":1685480537635,\"y\":12.43},{\"x\":1685480837564,\"y\":12.43},{\"x\":1685481137578,\"y\":12.27},{\"x\":1685481437654,\"y\":12.32},{\"x\":1685481737583,\"y\":12.24},{\"x\":1685482037634,\"y\":12.13},{\"x\":1685482337596,\"y\":12.13},{\"x\":1685482637588,\"y\":12.04},{\"x\":1685482937605,\"y\":11.88},{\"x\":1685483237606,\"y\":11.88},{\"x\":1685483537617,\"y\":11.87},{\"x\":1685483837694,\"y\":11.87},{\"x\":1685484137652,\"y\":11.83},{\"x\":1685484437620,\"y\":11.55},{\"x\":1685484737629,\"y\":11.83},{\"x\":1685485037682,\"y\":11.43},{\"x\":1685485337673,\"y\":11.43},{\"x\":1685485637637,\"y\":11.43},{\"x\":1685485937698,\"y\":11.4},{\"x\":1685486237666,\"y\":11.4},{\"x\":1685486537695,\"y\":11.4},{\"x\":1685486837701,\"y\":11.34},{\"x\":1685487137670,\"y\":11.34},{\"x\":1685487437690,\"y\":11.34},{\"x\":1685487737711,\"y\":11.08},{\"x\":1685488037671,\"y\":11.08},{\"x\":1685488337728,\"y\":11.08},{\"x\":1685488637752,\"y\":11.06},{\"x\":1685488937679,\"y\":11.06},{\"x\":1685489237735,\"y\":11.04},{\"x\":1685489537691,\"y\":10.96},{\"x\":1685489837848,\"y\":11.05},{\"x\":1685490137701,\"y\":11.05},{\"x\":1685490437705,\"y\":10.95},{\"x\":1685490737730,\"y\":10.86},{\"x\":1685491037756,\"y\":10.8},{\"x\":1685491337708,\"y\":10.8},{\"x\":1685491637705,\"y\":10.8},{\"x\":1685491937711,\"y\":10.71},{\"x\":1685492237764,\"y\":10.8},{\"x\":1685492537769,\"y\":10.8},{\"x\":1685492837718,\"y\":10.8},{\"x\":1685493137753,\"y\":10.8},{\"x\":1685493437930,\"y\":10.87},{\"x\":1685493737724,\"y\":10.87},{\"x\":1685494037777,\"y\":10.61},{\"x\":1685494337747,\"y\":10.52},{\"x\":1685494637852,\"y\":10.61},{\"x\":1685494937737,\"y\":10.61},{\"x\":1685495237777,\"y\":10.63},{\"x\":1685495537775,\"y\":10.62},{\"x\":1685495837812,\"y\":10.51},{\"x\":1685496137755,\"y\":10.43},{\"x\":1685496437796,\"y\":10.49},{\"x\":1685496737809,\"y\":10.49},{\"x\":1685497037761,\"y\":10.49},{\"x\":1685497337761,\"y\":10.23},{\"x\":1685497637765,\"y\":10.23},{\"x\":1685497937814,\"y\":10.31},{\"x\":1685498237807,\"y\":10.31},{\"x\":1685498537788,\"y\":10.31},{\"x\":1685498837775,\"y\":10.23},{\"x\":1685499137789,\"y\":10.23},{\"x\":1685499437805,\"y\":10.23},{\"x\":1685499737817,\"y\":10.31},{\"x\":1685500037780,\"y\":10.31},{\"x\":1685500337844,\"y\":10.14},{\"x\":1685500637891,\"y\":10.14},{\"x\":1685500937793,\"y\":10.14},{\"x\":1685501237793,\"y\":10.14},{\"x\":1685501537807,\"y\":10.05},{\"x\":1685501837925,\"y\":10.05},{\"x\":1685502137801,\"y\":10.05},{\"x\":1685502437811,\"y\":9.97},{\"x\":1685502737895,\"y\":10.14},{\"x\":1685503037900,\"y\":10.16},{\"x\":1685503337808,\"y\":10.16},{\"x\":1685503637807,\"y\":10.08},{\"x\":1685503937812,\"y\":9.94},{\"x\":1685504237850,\"y\":9.94},{\"x\":1685504537822,\"y\":9.99},{\"x\":1685504837872,\"y\":9.96},{\"x\":1685505137866,\"y\":9.98},{\"x\":1685505438032,\"y\":9.98},{\"x\":1685505737834,\"y\":9.98},{\"x\":1685506037879,\"y\":9.81},{\"x\":1685506337861,\"y\":9.81},{\"x\":1685506637914,\"y\":9.81},{\"x\":1685506937872,\"y\":9.81},{\"x\":1685507237889,\"y\":9.81},{\"x\":1685507537838,\"y\":9.81},{\"x\":1685507837888,\"y\":9.81},{\"x\":1685508137887,\"y\":9.82},{\"x\":1685508437832,\"y\":9.73},{\"x\":1685508737867,\"y\":9.82},{\"x\":1685509037974,\"y\":9.62},{\"x\":1685509337857,\"y\":9.82},{\"x\":1685509637848,\"y\":9.62},{\"x\":1685509937889,\"y\":9.62},{\"x\":1685510237947,\"y\":9.62},{\"x\":1685510537889,\"y\":9.65},{\"x\":1685510837852,\"y\":9.65},{\"x\":1685511137855,\"y\":9.65},{\"x\":1685511437965,\"y\":9.57},{\"x\":1685511737859,\"y\":9.57},{\"x\":1685512037848,\"y\":9.54},{\"x\":1685512337852,\"y\":9.55},{\"x\":1685512638025,\"y\":9.58},{\"x\":1685512937857,\"y\":9.57},{\"x\":1685513237850,\"y\":9.58},{\"x\":1685513537848,\"y\":9.57},{\"x\":1685513837893,\"y\":9.57},{\"x\":1685514137857,\"y\":9.75},{\"x\":1685514437855,\"y\":9.75},{\"x\":1685514737977,\"y\":9.84},{\"x\":1685515037889,\"y\":9.84},{\"x\":1685515337874,\"y\":9.84},{\"x\":1685515637865,\"y\":9.84},{\"x\":1685515937961,\"y\":9.84},{\"x\":1685516238059,\"y\":9.84},{\"x\":1685516537872,\"y\":9.75},{\"x\":1685516837969,\"y\":10.01},{\"x\":1685517137902,\"y\":10.01},{\"x\":1685517437997,\"y\":10.01},{\"x\":1685517737887,\"y\":10.03},{\"x\":1685518037974,\"y\":10.03},{\"x\":1685518337895,\"y\":9.92},{\"x\":1685518637932,\"y\":9.92},{\"x\":1685518937904,\"y\":9.96},{\"x\":1685519238002,\"y\":10.1},{\"x\":1685519537959,\"y\":10.1},{\"x\":1685519837988,\"y\":10.1},{\"x\":1685520137912,\"y\":10.1},{\"x\":1685520437906,\"y\":10.37},{\"x\":1685520737913,\"y\":10.33},{\"x\":1685521038006,\"y\":10.33},{\"x\":1685521337908,\"y\":10.33},{\"x\":1685521637915,\"y\":10.33},{\"x\":1685521937963,\"y\":10.25},{\"x\":1685522238080,\"y\":10.54},{\"x\":1685522537924,\"y\":10.54},{\"x\":1685522837919,\"y\":10.54},{\"x\":1685523137944,\"y\":10.5},{\"x\":1685523438045,\"y\":10.6},{\"x\":1685523737930,\"y\":10.58},{\"x\":1685524037929,\"y\":10.52},{\"x\":1685524337974,\"y\":10.82},{\"x\":1685524638007,\"y\":10.82},{\"x\":1685524937934,\"y\":10.82},{\"x\":1685525237935,\"y\":10.95},{\"x\":1685525537943,\"y\":10.99},{\"x\":1685525838078,\"y\":11.08},{\"x\":1685526137951,\"y\":11.08},{\"x\":1685526437950,\"y\":10.99},{\"x\":1685526737944,\"y\":11.07},{\"x\":1685527038721,\"y\":11.07},{\"x\":1685527338012,\"y\":11.11},{\"x\":1685527637961,\"y\":11.11},{\"x\":1685527937952,\"y\":11.07},{\"x\":1685528238080,\"y\":11.16},{\"x\":1685528537978,\"y\":11.03},{\"x\":1685528837960,\"y\":11.12},{\"x\":1685529137967,\"y\":11.08},{\"x\":1685529438131,\"y\":11.61},{\"x\":1685529738016,\"y\":11.61},{\"x\":1685530037967,\"y\":11.61},{\"x\":1685530337963,\"y\":11.61},{\"x\":1685530638067,\"y\":11.61},{\"x\":1685530937995,\"y\":11.61},{\"x\":1685531237967,\"y\":11.61},{\"x\":1685531537979,\"y\":12.07},{\"x\":1685531838029,\"y\":11.73},{\"x\":1685532137972,\"y\":11.72},{\"x\":1685532437974,\"y\":11.73},{\"x\":1685532738070,\"y\":11.43},{\"x\":1685533038022,\"y\":11.92},{\"x\":1685533338038,\"y\":16.07},{\"x\":1685533637982,\"y\":16.07},{\"x\":1685533938045,\"y\":16.07},{\"x\":1685534238057,\"y\":16.07},{\"x\":1685534537980,\"y\":16.07},{\"x\":1685534838036,\"y\":12.17},{\"x\":1685535137993,\"y\":12.17},{\"x\":1685535438136,\"y\":12.17},{\"x\":1685535737984,\"y\":12.17},{\"x\":1685536037986,\"y\":12.17},{\"x\":1685536337985,\"y\":12.17},{\"x\":1685536638024,\"y\":11.9},{\"x\":1685536938025,\"y\":15.91},{\"x\":1685537237999,\"y\":15.91},{\"x\":1685537538002,\"y\":15.91},{\"x\":1685537838106,\"y\":15.91},{\"x\":1685538138007,\"y\":15.82},{\"x\":1685538438058,\"y\":13.28},{\"x\":1685538738048,\"y\":13.28},{\"x\":1685539038104,\"y\":13.28},{\"x\":1685539338007,\"y\":13.2},{\"x\":1685539638019,\"y\":13.2},{\"x\":1685539938003,\"y\":13.33},{\"x\":1685540238052,\"y\":13.2},{\"x\":1685540538039,\"y\":16.75},{\"x\":1685540838017,\"y\":16.67},{\"x\":1685541138064,\"y\":16.75},{\"x\":1685541438054,\"y\":16.67},{\"x\":1685541738229,\"y\":17.3},{\"x\":1685542038012,\"y\":17.22},{\"x\":1685542338042,\"y\":17.3},{\"x\":1685542638126,\"y\":18.38},{\"x\":1685542938012,\"y\":18.38},{\"x\":1685543238056,\"y\":18.97},{\"x\":1685543538110,\"y\":17.86},{\"x\":1685543838052,\"y\":17.86},{\"x\":1685544138125,\"y\":17.78},{\"x\":1685544438027,\"y\":17.78},{\"x\":1685544738110,\"y\":14.96},{\"x\":1685545038075,\"y\":14.94},{\"x\":1685545338027,\"y\":14.94},{\"x\":1685545638075,\"y\":15.63},{\"x\":1685545938022,\"y\":15.63},{\"x\":1685546238174,\"y\":16},{\"x\":1685546538079,\"y\":15.83},{\"x\":1685546838020,\"y\":15.81},{\"x\":1685547138129,\"y\":14.9},{\"x\":1685547438095,\"y\":14.83},{\"x\":1685547738125,\"y\":14.9},{\"x\":1685548038046,\"y\":14.83},{\"x\":1685548338056,\"y\":14.9},{\"x\":1685548638087,\"y\":14.85},{\"x\":1685548938038,\"y\":14.85},{\"x\":1685549238040,\"y\":14.85},{\"x\":1685549538033,\"y\":14.97},{\"x\":1685549838168,\"y\":14.97},{\"x\":1685550138087,\"y\":15.03},{\"x\":1685550438030,\"y\":14.97},{\"x\":1685550738120,\"y\":14.3},{\"x\":1685551038139,\"y\":14.3},{\"x\":1685551338037,\"y\":13.6},{\"x\":1685551638033,\"y\":13.49},{\"x\":1685551938041,\"y\":13.47},{\"x\":1685552238174,\"y\":13.61},{\"x\":1685552538043,\"y\":13.61},{\"x\":1685552838043,\"y\":13.52},{\"x\":1685553138067,\"y\":13.52},{\"x\":1685553438546,\"y\":13.6},{\"x\":1685553738055,\"y\":13.52},{\"x\":1685554038140,\"y\":13.59},{\"x\":1685554338045,\"y\":13.51},{\"x\":1685554638134,\"y\":13.16},{\"x\":1685554938070,\"y\":13.16},{\"x\":1685555238057,\"y\":13.18},{\"x\":1685555538064,\"y\":13.07},{\"x\":1685555838093,\"y\":13.07},{\"x\":1685556138085,\"y\":13.14},{\"x\":1685556438188,\"y\":13.11},{\"x\":1685556738092,\"y\":13.12},{\"x\":1685557038192,\"y\":13.01},{\"x\":1685557338095,\"y\":13.01},{\"x\":1685557638182,\"y\":12.89},{\"x\":1685557938150,\"y\":12.8},{\"x\":1685558238172,\"y\":12.61},{\"x\":1685558538110,\"y\":12.8},{\"x\":1685558838201,\"y\":12.58},{\"x\":1685559138123,\"y\":12.58},{\"x\":1685559438206,\"y\":12.59},{\"x\":1685559738189,\"y\":12.57},{\"x\":1685560038129,\"y\":12.57},{\"x\":1685560338155,\"y\":12.57},{\"x\":1685560638404,\"y\":12.29},{\"x\":1685560938173,\"y\":12.29},{\"x\":1685561238169,\"y\":12.29},{\"x\":1685561538183,\"y\":12.17},{\"x\":1685561838237,\"y\":12.17},{\"x\":1685562138233,\"y\":12.21},{\"x\":1685562438249,\"y\":12.18},{\"x\":1685562738211,\"y\":12.18},{\"x\":1685563038228,\"y\":12.09},{\"x\":1685563338214,\"y\":12.07},{\"x\":1685563638217,\"y\":12.07},{\"x\":1685563938231,\"y\":11.8},{\"x\":1685564238362,\"y\":11.86},{\"x\":1685564538381,\"y\":11.83},{\"x\":1685564838277,\"y\":11.83},{\"x\":1685565138242,\"y\":11.79},{\"x\":1685565438280,\"y\":11.7},{\"x\":1685565738251,\"y\":11.53},{\"x\":1685566038259,\"y\":11.53},{\"x\":1685566338265,\"y\":11.53},{\"x\":1685566638303,\"y\":11.43},{\"x\":1685566938268,\"y\":11.43},{\"x\":1685567238323,\"y\":11.33},{\"x\":1685567538343,\"y\":11.39},{\"x\":1685567838304,\"y\":11.31},{\"x\":1685568138281,\"y\":11.39},{\"x\":1685568438336,\"y\":11.31},{\"x\":1685568738284,\"y\":11.31},{\"x\":1685569038329,\"y\":11.31},{\"x\":1685569338310,\"y\":11.25},{\"x\":1685569638302,\"y\":11.25},{\"x\":1685569938300,\"y\":11.25},{\"x\":1685570238298,\"y\":10.97},{\"x\":1685570538297,\"y\":10.97},{\"x\":1685570838294,\"y\":10.97},{\"x\":1685571138323,\"y\":10.78},{\"x\":1685571438384,\"y\":10.78},{\"x\":1685571738313,\"y\":10.78},{\"x\":1685572038419,\"y\":10.8},{\"x\":1685572338315,\"y\":10.8},{\"x\":1685572638324,\"y\":10.8},{\"x\":1685572938327,\"y\":10.44},{\"x\":1685573238329,\"y\":10.44},{\"x\":1685573538333,\"y\":10.44},{\"x\":1685573838369,\"y\":10.44},{\"x\":1685574138332,\"y\":10.44},{\"x\":1685574438328,\"y\":10.43},{\"x\":1685574738335,\"y\":10.43},{\"x\":1685575038372,\"y\":10.42},{\"x\":1685575338334,\"y\":10.43},{\"x\":1685575638376,\"y\":10.44},{\"x\":1685575938389,\"y\":10.48},{\"x\":1685576238419,\"y\":10.39},{\"x\":1685576538344,\"y\":10.11},{\"x\":1685576838359,\"y\":10.2},{\"x\":1685577138380,\"y\":10.29},{\"x\":1685577438375,\"y\":10.29},{\"x\":1685577738413,\"y\":10.3},{\"x\":1685578038361,\"y\":10.29},{\"x\":1685578338371,\"y\":10.3},{\"x\":1685578638503,\"y\":10.18},{\"x\":1685578938376,\"y\":10.18},{\"x\":1685579238383,\"y\":10.09},{\"x\":1685579538380,\"y\":10.17},{\"x\":1685579838438,\"y\":10.13},{\"x\":1685580138376,\"y\":9.88},{\"x\":1685580438398,\"y\":9.88},{\"x\":1685580738405,\"y\":9.94},{\"x\":1685581038616,\"y\":9.94},{\"x\":1685581338395,\"y\":9.94},{\"x\":1685581638435,\"y\":9.94},{\"x\":1685581938409,\"y\":9.88},{\"x\":1685582238480,\"y\":9.86},{\"x\":1685582538402,\"y\":9.86},{\"x\":1685582838405,\"y\":9.86},{\"x\":1685583138415,\"y\":9.86},{\"x\":1685583438613,\"y\":9.86},{\"x\":1685583738415,\"y\":9.88},{\"x\":1685584038410,\"y\":9.88},{\"x\":1685584338448,\"y\":9.79},{\"x\":1685584638454,\"y\":9.88},{\"x\":1685584938414,\"y\":9.88},{\"x\":1685585238420,\"y\":9.98},{\"x\":1685585538423,\"y\":9.89},{\"x\":1685585838440,\"y\":9.89},{\"x\":1685586138428,\"y\":9.84},{\"x\":1685586438423,\"y\":9.84},{\"x\":1685586738424,\"y\":9.79},{\"x\":1685587038648,\"y\":9.93},{\"x\":1685587338431,\"y\":9.79},{\"x\":1685587638450,\"y\":9.79},{\"x\":1685587938475,\"y\":9.88},{\"x\":1685588238559,\"y\":9.88},{\"x\":1685588538441,\"y\":9.88},{\"x\":1685588838584,\"y\":9.67},{\"x\":1685589138428,\"y\":9.58},{\"x\":1685589438536,\"y\":9.67},{\"x\":1685589738473,\"y\":9.67},{\"x\":1685590038433,\"y\":9.67},{\"x\":1685590338478,\"y\":9.67},{\"x\":1685590638761,\"y\":9.42},{\"x\":1685590938425,\"y\":9.42},{\"x\":1685591238442,\"y\":9.42},{\"x\":1685591538439,\"y\":9.31},{\"x\":1685591838504,\"y\":9.31},{\"x\":1685592138436,\"y\":9.34},{\"x\":1685592438435,\"y\":9.34},{\"x\":1685592738487,\"y\":9.38},{\"x\":1685593038723,\"y\":9.38},{\"x\":1685593338444,\"y\":9.38},{\"x\":1685593638424,\"y\":9.34},{\"x\":1685593938457,\"y\":9.34},{\"x\":1685594238655,\"y\":9.86},{\"x\":1685594538452,\"y\":9.86},{\"x\":1685594838449,\"y\":9.86},{\"x\":1685595138489,\"y\":9.84},{\"x\":1685595438547,\"y\":9.84},{\"x\":1685595738460,\"y\":9.78},{\"x\":1685596038492,\"y\":9.85},{\"x\":1685596338571,\"y\":9.85},{\"x\":1685596638545,\"y\":9.85},{\"x\":1685596938457,\"y\":9.76},{\"x\":1685597238477,\"y\":9.85},{\"x\":1685597538472,\"y\":9.8},{\"x\":1685597838494,\"y\":9.83},{\"x\":1685598138501,\"y\":9.86},{\"x\":1685598438509,\"y\":10.14},{\"x\":1685598738535,\"y\":10.14},{\"x\":1685599038819,\"y\":10.14},{\"x\":1685599338484,\"y\":10.07},{\"x\":1685599638529,\"y\":10.07},{\"x\":1685599938480,\"y\":10.14},{\"x\":1685600238698,\"y\":10.23},{\"x\":1685600538482,\"y\":10.1},{\"x\":1685600838527,\"y\":10.43},{\"x\":1685601138487,\"y\":10.43},{\"x\":1685601438740,\"y\":10.43},{\"x\":1685601738547,\"y\":10.45},{\"x\":1685602038498,\"y\":10.36},{\"x\":1685602338488,\"y\":10.43},{\"x\":1685602638682,\"y\":10.46},{\"x\":1685602938581,\"y\":10.46},{\"x\":1685603238508,\"y\":10.47},{\"x\":1685603538503,\"y\":10.47},{\"x\":1685603838572,\"y\":10.47},{\"x\":1685604138511,\"y\":10.47},{\"x\":1685604438579,\"y\":10.6},{\"x\":1685604738515,\"y\":10.51},{\"x\":1685605038560,\"y\":10.55},{\"x\":1685605338556,\"y\":11.07},{\"x\":1685605638563,\"y\":11.07},{\"x\":1685605938519,\"y\":11.07},{\"x\":1685606238594,\"y\":11.05},{\"x\":1685606538513,\"y\":11.12},{\"x\":1685606838518,\"y\":11.15},{\"x\":1685607138518,\"y\":11.15},{\"x\":1685607438879,\"y\":11.19},{\"x\":1685607738543,\"y\":11.19},{\"x\":1685608038530,\"y\":11.42},{\"x\":1685608338527,\"y\":11.33},{\"x\":1685608638628,\"y\":11.33},{\"x\":1685608938572,\"y\":11.54},{\"x\":1685609238526,\"y\":11.54},{\"x\":1685609538568,\"y\":11.54},{\"x\":1685609838674,\"y\":11.73},{\"x\":1685610138530,\"y\":11.54},{\"x\":1685610438525,\"y\":11.73},{\"x\":1685610738604,\"y\":11.81},{\"x\":1685611038666,\"y\":11.81},{\"x\":1685611338564,\"y\":11.72},{\"x\":1685611638584,\"y\":12.09},{\"x\":1685611938537,\"y\":12.09},{\"x\":1685612238596,\"y\":12.09},{\"x\":1685612538547,\"y\":12.11},{\"x\":1685612838540,\"y\":12.11},{\"x\":1685613138539,\"y\":12.07},{\"x\":1685613438767,\"y\":12.53},{\"x\":1685613738611,\"y\":12.64},{\"x\":1685614038551,\"y\":12.51},{\"x\":1685614338551,\"y\":12.64},{\"x\":1685614638741,\"y\":12.64},{\"x\":1685614938559,\"y\":12.64},{\"x\":1685615238587,\"y\":13.05},{\"x\":1685615538593,\"y\":13.15},{\"x\":1685615838836,\"y\":13.07},{\"x\":1685616138551,\"y\":13.07},{\"x\":1685616438549,\"y\":13.17},{\"x\":1685616738558,\"y\":13.26},{\"x\":1685617038760,\"y\":13.26},{\"x\":1685617338558,\"y\":13.36},{\"x\":1685617638534,\"y\":13.36},{\"x\":1685617938635,\"y\":13.73},{\"x\":1685618238566,\"y\":13.71},{\"x\":1685618538558,\"y\":13.71},{\"x\":1685618838593,\"y\":13.75},{\"x\":1685619138565,\"y\":13.75},{\"x\":1685619438673,\"y\":13.93},{\"x\":1685619738585,\"y\":13.93}]],\"labels\":[\"\"]}]","payloadType":"json","x":150,"y":180,"wires":[["3eb08d4843164efc"]]},{"id":"41e847ff22249c0e","type":"ui_group","name":"Temperature","tab":"1d985094b1a81b0c","order":5,"disp":true,"width":"24","collapse":false,"className":""},{"id":"1d985094b1a81b0c","type":"ui_tab","name":"Wide View","icon":"dashboard","disabled":false,"hidden":false}] ``` :: ### 3. Using sliders and persisting the current value Sliders are a really useful user-interface element. Where you need to control the speed of a piece of machinery, having the ability to use a slider rather than manually typing in a value is a much better fit for shop-floor HMIs. When using sliders in your dashboards, it's important to consider how you will persist the state of the slider. If you don't persist the state, you will find that a redeploy of your dashboard will set the slider back to the default value. That would also change the speed of your machine. :video{ariaLabel="An example of a slider in a HMI" autoPlay="true" height="164" loop="true" muted="true" playsInline="true" preload="none" width="540"} To retain the current value of the slider we can use Node-RED's context. Each time the slider value is updated, we store the value in context. Each time we deploy the flow, we can now load the value back from context. If you'd like to view this slider and the flow which makes it work on your own Node-RED, you can import the flow below. ::render-flow{:height='300'} ```json [{"id":"05340e7a133098a6","type":"ui_slider","z":"0e6be5088cecccc1","name":"","label":"{{msg.payload}}","tooltip":"","group":"41e847ff22249c0e","order":1,"width":0,"height":0,"passthru":true,"outs":"all","topic":"topic","topicType":"msg","min":0,"max":"20","step":1,"className":"","x":230,"y":300,"wires":[["945db61c94fc0704","ad7819da2cfbcd4e"]]},{"id":"849d76d146ff8c12","type":"inject","z":"0e6be5088cecccc1","name":"Inject on deploy","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":160,"y":240,"wires":[["aeedb3c60965c1af"]]},{"id":"945db61c94fc0704","type":"change","z":"0e6be5088cecccc1","name":"Set global-slider-value = msg.payload","rules":[{"t":"set","p":"slider-value","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":300,"wires":[[]]},{"id":"aeedb3c60965c1af","type":"change","z":"0e6be5088cecccc1","name":"Set msg.payload = global.slider-value","rules":[{"t":"set","p":"payload","pt":"msg","to":"slider-value","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":240,"wires":[["05340e7a133098a6"]]},{"id":"ad7819da2cfbcd4e","type":"ui_text","z":"0e6be5088cecccc1","group":"41e847ff22249c0e","order":2,"width":0,"height":0,"name":"","label":"","format":"The current value is {{msg.payload}} meters per minute","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":350,"y":340,"wires":[]},{"id":"41e847ff22249c0e","type":"ui_group","name":"Meters per Minute","tab":"466e33abb95e4dd4","order":5,"disp":true,"width":"10","collapse":false,"className":""},{"id":"466e33abb95e4dd4","type":"ui_tab","name":"Sliders","icon":"dashboard","order":3,"disabled":false,"hidden":false}] ``` :: We hope you found these tips useful, if you'd like to suggest some of your own tips which you think we should share in our future blog posts please [get in touch](mailto\:contact@flowfuse.com). You can also read some of our previous Node-RED tips using the links below. [Node-RED Tips - Subflows, Link Nodes, and the Range Node](https://flowfuse.com/blog/2023/04/3-quick-node-red-tips-6/):br[Node-RED Tips - Importing, Exporting, and Grouping Flows](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/):br[Node-RED Tips - Smooth, Catch, and Maths](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-4/):br[Node-RED Tips - Exec, Filter, and Debug](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-3/):br[Node-RED Tips - Deploying, Debugging, and Delaying](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-2/):br[Node-RED Tips - Wiring Shortcuts](https://flowfuse.com/blog/2023/02/3-quick-node-red-tips-1/) # Community News June 2023 Welcome to the FlowFuse newsletter for June 2023, a monthly roundup of what’s been happening with both FlowFuse and the wider Node-RED community. ## New Release This week we released FlowFuse 1.8, featuring high availability for Node-RED and DevOps software delivery pipelines. Both these features were in high demand from our community and will make it easier to reliably deliver Node-RED for business critical applications. Read about the details of FlowFuse 1.8 in our [release announcement](https://flowfuse.com/blog/2023/06/flowforge-1-8-released/). ## Upcoming events ### Building Node-RED Applications for Scalability and High Availability Our June webinar will focus on the new FlowFuse 1.8 feature of running high availability Node-RED applications. Marian Demme, FlowFuse Product Manager, will lead this session and share practical insights and best practices to show how FlowFuse can unlock the true potential of Node-RED in large-scale deployments. [Sign-up today](https://flowfuse.com/webinars/2023/building-scalable-ha-node-red/) to join us on June 22. ### Build an Edge-to-Cloud Solution with the MING Stack On June 27, FlowFuse is doing a webinar with our friends at InfluxDB. A great opportunity to see how easy it is to use Node-RED and InfluxDB to send data from the edge to the cloud.  [Sign-up today](https://www.influxdata.com/resources/build-an-edge-to-cloud-solution-with-the-ming-stack/?utm_source=partner&utm_medium=referral&utm_campaign=2023-06-27_Webinar_FlowFuse-NodeRED&utm_term=speaker){rel=""nofollow""}. ## From our Blog - [Bringing High Availability to Node-RED](https://flowfuse.com/blog/2023/05/bringing-high-availability-to-node-red/) - FlowFuse CTO discusses the strategy for delivering high availability in the FlowFuse platform. - Two articles featuring how to connect Modbus data with Node-RED: - [Using Node-RED to Visualize Industrial Production Data via Modbus](https://flowfuse.com/docs/node-red/protocol/modbus/) - \[Best Practices Integrating a Modbus Device With Node-RED]\(/blog/2023/05/integrating modbus with node-red/) - [Node-RED Tips - Dashboard Edition](https://flowfuse.com/blog/2023/06/3-quick-node-red-tips-7/) - A new set of Node-RED quick tips that are focused on using Node-RED Dashboard. - [Persisting chart data in Node-RED Dashboards](https://flowfuse.com/blog/2023/05/persisting-chart-data-in-node-red/) - How to store data from the Node-RED Dasbhaord chart node. - [Node-RED Community Survey Results](https://flowfuse.com/blog/2023/05/node-red-community-survey-results/) - A quick summary of the Node-RED Community Survey results. - [FlowFuse 1.7 Now Available with Remote Node-RED Editor Access](https://flowfuse.com/blog/2023/05/flowforge-1-7-released/) ## From the Community Gerrit Riessen has published a list of [Pros and Cons for using Node-RED](https://gorenje.medium.com/fourteen-for-fourteen-against-why-i-love-hate-and-connect-with-node-23797f9466ec){rel=""nofollow""}. It is a pretty comprehensive list so check it out. FlowFuse is working to address some of the cons in Gerrit's list, specifically software delivery pipelines and the ability to deploy out to many end points. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [DevOps Engineer](https://boards.greenhouse.io/flowfuse/jobs/4796271004){rel=""nofollow""} - [Sales Representative](https://boards.greenhouse.io/flowfuse/jobs/4843566004){rel=""nofollow""} # The Next Step in Data Visualization - Announcing the Successor to the Node-RED Dashboard For the past several years, the Node-RED Dashboard has been an indispensable tool for many Node-RED users. It has offered a seamless way to create live dashboards, enabling the quick and intuitive creation of user interfaces for Node-RED flows. However, as the saying goes, "all good things must come to an end." We at FlowFuse have identified a significant need for a modern, interactive data visualization and dashboard solution. Having evaluated a wide range of options, we've decided to embark on an exciting journey: the creation of what we hope becomes the official successor to the Node-RED Dashboard. ## The Problem at Hand The original Node-RED Dashboard is based on Angular v1, which is no longer maintained. Although small patches have been and will continue to be applied on a "best can do" basis, there will be no major feature upgrades. The lack of ongoing maintenance and updates has the potential to lead to underlying security breakages, a risk we are not comfortable taking. We have recognized the need to innovate and adapt, which is why we are creating a completely new project to replace the existing Node-RED Dashboard. ## The Solution The successor to the Node-RED Dashboard will be a completely new project, published as a separate package. This new project will take the reins from the old dashboard and guide us into the future with the support and blessing of the existing dashboard maintainers. The Node-RED community can rest assured that the project will stay under the Apache 2.0 licence and keep intact the core principles of open-source and community-driven development. ## Community Involvement and Open Source Contribution We believe in the power of the community, and we want your feedback. If there are features you'd like to see or improvements you think can be made, we invite you to open a [Github issue](https://github.com/FlowFuse/node-red-dashboard/issues/new/choose){rel=""nofollow""}. Your insights and suggestions will be invaluable in shaping the future of this project. Contributions from the community are not just welcome, but highly encouraged. We believe that the strength of a project is proportional to the strength of its community. In our commitment to transparency and collaboration, we will be documenting all major decision-making processes and sharing the details here in our blog as they become available. By working closely with additional Node-RED key figures like Dave Conway-Jones, and by making our development process as open and collaborative as possible, we aim to ensure that the successor to the Node-RED Dashboard lives up to the high standards set by the original, while also introducing innovative features and enhancements. ## Join the Team Are you a developer looking for a new challenge? We are searching for a freelancer to help us with the development of the first version of the new dashboard. This is a 2-3 month project that provides an exciting opportunity to contribute to the future of data visualization and Node-RED. If you're interested, [we'd love to hear from you.](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} ## Charting the Course As we embark on this new journey, we are excited about the potential of the successor to the Node-RED Dashboard. The road ahead will be filled with challenges, but we are confident that with the help of the community, and a dedicated team, we will create a tool that surpasses its predecessor in every way. Join us on this exciting journey as we innovate, create, and redefine the future of data visualization with the successor to the Node-RED Dashboard. # FlowFuse now offers High Availability Node-RED FlowFuse 1.8 introduces two key features that allow organizations to reliably deploy Node-RED applications into production. In 1.8, it is now possible to run Node-RED applications with high availability so the application is more scalable and more fault tolerant. FlowFuse 1.8 also introduces software deliver pipelines, so development teams can now set up dev/test/production environments for their Node-RED applications. ## More reliable and scalable Node-RED applications FlowFuse now makes it possible to deploy business critical applications built in Node-RED that are reliable and scalable. The new 1.8 features allows a Node-RED instance to be deployed in high availability mode, meaning two instances of the same Node-RED flows are available behind a load balancer. This allows for increased traffic to be automatically distributed across the two Node-RED instances. This means your Node-RED applications can handle more traffic and experience less downtime. For more details, please see our [documentation](https://flowfuse.com/docs/user/high-availability). Additionally, we're pleased to offer a 30-day premium trial license for self-managed installs on Kubernetes. To avail of this offer, book a demo at [flowforge.com/book-demo](https://flowfuse.com/book-demo). High Availability is our first [preview feature](https://flowfuse.com/handbook/engineering/product/versioning/#preview-features), and your feedback is crucial. We encourage you to try out HA in your Node-RED instances and share your experiences with us. Your feedback will help us refine this feature and make it even better. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: mbDkjKhVwIw --- :: ## DevOps Pipelines FlowFuse 1.8 introduces the concept of Pipelines to better organize your Node-RED development. Development team can now set up different staging environments for different steps in the development cycle, ex. test, development and production. Node-RED instances can be pushed along a pipeline as they move along the development process. This allows for a better organized and predictable development process for your team. The new Pipelines feature builds upon the Staged Development support that we introduced in[FlowFuse Version 1.4](https://flowfuse.com/blog/2023/02/flowforge-1-4-0-released/). We highly recommend that development teams avoid developing their flows directly in production instances. This approach fosters a more reliable and robust development process, reducing the risks associated with production environment modifications. Instead, start your development in a dedicated development or test instance and then deploy your Node-RED instance to production once they have been thoroughly tested and reviewed. ::lite-youtube --- params: rel=0 style: "width: 704px; height: 100%;" title: YouTube video player videoid: Pbql22f3vqY --- :: ## User interface for Device Agent In our previous release, we introduced [Editor Access for Devices](https://flowfuse.com/blog/2023/05/flowforge-1-7-released/). Now, the FlowFuse Device Agent now comes with its very own User Interface (UI) for configuration. Imagine this: Your industrial equipment arrives with the Device Agent preinstalled. In the past, you might have faced challenges in configuring and connecting your device with FlowFuse, particularly if you had no direct shell access. But not any more. With the newly introduced UI, you can easily set up and connect your device with FlowFuse without needing to access the command line interface directly. This simplifies the process significantly and saves you time. For more details, see our [documentation](https://flowfuse.com/docs/device-agent/introduction/). ## Node-RED 3.1 Beta 3 Available FlowFuse Cloud is a great place to try out the new Node-RED features, with FlowFuse Cloud now including the [Node-RED 3.1.0-beta.3](https://discourse.nodered.org/t/node-red-3-1-0-beta-3-released/78716){rel=""nofollow""}. If you want to try this version you can [duplicate your instance](https://flowfuse.com/docs/user/instance-settings/) and [upgrade your stack](https://flowfuse.com/docs/user/changestack/). ## Ongoing Topics ### SOC2 Certification We're making great strides on our journey towards SOC2 certification, striving to meet the highest industry standards for security and privacy. While we're not quite ready to disclose specific milestones, be assured that everything is progressing smoothly. As we continue working diligently towards our target, we promise to keep you informed every step of the way. Our unwavering commitment to deliver secure and private services to our customers and partners remains our foremost priority. Stay tuned for more updates! ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our [Product Roadmap Page](https://flowfuse.com/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Bug Fixes When editing a device in developer mode via the tunnel/proxy connection, a list of team projects are not presented in the "Target" field of the project-link nodes. [#2228](https://github.com/FlowFuse/flowfuse/issues/2228){rel=""nofollow""} If a user invites an external user to their team with an sso-enable email domain, when that user registers and logs in, they are not added to the team they were invited to and must be re-invited. [#2232](https://github.com/FlowFuse/flowfuse/issues/2232){rel=""nofollow""} No warning given if tying to start device editor when NR is not running [#2233](https://github.com/FlowFuse/flowfuse/issues/2233){rel=""nofollow""} Stuck on the form Create a new Application & Instance after using an already known instance name [#2221](https://github.com/FlowFuse/flowfuse/issues/2221){rel=""nofollow""} If the device agent finds itself in Developer mode, it stops pulling snapshots from the platform [#97](https://github.com/FlowFuse/device-agent/issues/97){rel=""nofollow""} Accessing the Admin Settings General page resets the Platform Statstics token [#2140](https://github.com/FlowFuse/flowfuse/issues/2140){rel=""nofollow""} Selection of Team and Instance in nr-tools-plugin not possible [#15](https://github.com/FlowFuse/nr-tools-plugin/issues/15){rel=""nofollow""} HOME env var not set within Node-RED process [#117](https://github.com/FlowFuse/flowforge-nr-launcher/issues/117){rel=""nofollow""} ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.8. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go the the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # Use any npm module in Node-RED (2026) Node-RED has [an incredibly rich resource of integrations available](https://flows.nodered.org/search?type=node), but sometimes you need that little bit of extra functionality, or access to a Node.js module that doesn't have it's own custom nodes in Node-RED. **We can easily import any npm module within the built-in Node-RED function nodes.** Historically in Node-RED, you would have needed to manually `npm install` modules from the command line, but now that it's so easy to run Node-RED in the Cloud, where you don't have easy access to those tools, what are the other options available? ## Function Node - Setup ![Location of the "add" button in order to import an npm module intoa function node](https://flowfuse.com/blog/2023/06/images/npmimport-add.jpg "Location of the 'add' button in order to import an npm module intoa function node") :cta-image{alt="Walk through your FlowFuse setup with our team - book a demo" cta="demo" src="https://flowfuse.com/images/cta/book-a-demo.png"} All you need is the name of the module you want to import, then: 1. Drop in a new "function" node & double-click it 2. Switch to the "Setup" tab 3. Underneath the "modules" tab, click "+ add" in the bottom-left of the window. 4. Enter the name of the module you want to use in the newly created row, and (optionally) modify the `variable` that this module will be imported in as. 5. Switch back to the "On Message" tab and write your function. Your new module will be available via the `variable` you defined in the "Setup" tab. ## Example: Moment.js :video{controls="true" height="315" width="560"} Recently we wanted to use [moment](https://www.npmjs.com/package/moment){rel=""nofollow""} for some custom date calculations. Whilst there was set of [Moment Node-RED nodes](https://flows.nodered.org/node/node-red-contrib-moment){rel=""nofollow""} already available, it didn't have all of the functionality we needed. So, all we needed to do was import the module into a function node, and define our comparison there instead, here's a working example: ## Example: Easy CRC :video{controls="true" height="315" width="560"} Something we see [a lot on the Node-RED Forums](https://discourse.nodered.org/search?q=crc%20order%3Alatest){rel=""nofollow""} are questions on how to conduct CRC calculations. There is a popular node module `easy-crc` that can be imported and used in the function nodes, e.g: ## Example: PostHog :video{controls="true" height="315" width="560"} Node-RED is great for [data integration](https://flowfuse.com/use-cases/data-integration/). We use [PostHog](https://posthog.com/) for our internal Product Analysis. We record live events as they occur on FlowFuse Cloud to better understand features that are (and are not) used. We wanted to investigate whether or not we could add backdated data, which in theory was possible via their [posthog-node](https://posthog.com/docs/libraries/node) module. We wanted to populate it with data driven from our own database and API. Within two minutes, we could wire up a node to retrieve data from our API, and then ingest it into `posthog-node` via the import of a function node. ## Simplify Function Node Creation with FlowFuse [FlowFuse](https://flowfuse.com) provides a powerful platform to enhance, scale, and secure your Node-RED applications efficiently. One of our latest features, the **FlowFuse Assistant**, is designed to streamline the process of creating Function nodes. With the FlowFuse Assistant, you can leverage AI to generate Function nodes effortlessly. Just input your prompt, and the Assistant will handle the creation for you, saving time and reducing manual coding. To explore how to make the most of the FlowFuse Assistant and its capabilities, check out the [Assistants Documentation](https://flowfuse.com/docs/user/expert/). # Introducing the FlowFuse Community Forum We are thrilled to announce the launch of the [Community Forum for FlowFuse](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""}. A forum dedicated to empowering developers and enthusiasts to create innovative applications using FlowFuse, Node-RED and related technologies. Our community faces a new set of challenges, for example how to configure FlowFuse templates as administrator, or integration of the FlowFuse platform into another existing environment. These questions do not fit on the Node-RED discourse which is why FlowFuse now sports our own Community Forum. Furthermore, it’s the intent to keep the Node-RED forums vendor agnostic by the OpenJS foundation. Given FlowFuse is a vendor, it’s a fine balance to find. We hope and intend to be additive to the Node-RED community at large. At FlowFuse, our vision is to create a thriving community of Node-RED and MQTT enthusiasts who are passionate about building real-world solutions. We believe in the power of collaboration, knowledge sharing, and problem-solving. With this in mind, FlowFuse aims to foster a positive and inclusive environment where members can engage in discussions, exchange ideas, and support one another. Initially the forum is used to allow for discussions under each blog post on the FlowFuse blog, as well as a venue to provide community support. We also welcome discussions on the roadmap and backlog of FlowFuse. We have hopes that this community thrives and provides guidance, inspiration and opportunity to learn. # Node-RED as a No-Code EtherNet/IP to S7 Protocol Converter Frequently in industrial automation, there's a need for two devices that use different protocols to communicate with each other, requiring protocol conversion. :br In this tutorial, we present a mock scenario where Node-RED is used to enable an Allen Bradley PLC, which uses ethernet/IP, to communicate with a Siemens PLC, which uses S7, using a no-code solution. This example is geared toward beginners and assumes that the end-user knows how to use PLCs, but may be using FlowFuse or Node-RED for the first time. ## Premise ![Mock production facility](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-1.png "FlowFuse Mock production facility") The figure above shows the layout of a mock production facility. Inside this facility, operations suggested adding stack lights as an extra visual aid for operators to get a quick status of its 4 conveyor lines, avoiding the need to constantly monitor the HMI/SCADA displays. :br Engineering has suggested adding a siemens S7 1200 PLC with an IO link connection to 4 stacklights, with each line PLC sending basic status information to the stacklight PLC to control the stack light outputs. :br Line 1-3 PLCs are Siemens-based, and can communicate with the stacklight PLC natively over S7. But line 4 is an Allen Bradley PLC that uses ethernet/IP, and can't communicate with the stacklight PLC without some form of protocol conversion. :br Traditionally, we'd use protocol gateway hardware, like Anybus or Red Lion, to convert ethernet/IP to S7. :br But for this application, we will instead use FlowFuse, a pure software-based approach, to convert ethernet/IP to S7. Let's walk through the process. ## Pre-Requisites and Set Up ### FlowFuse In addition to our two PLCs, we’ll be using FlowFuse software to serve our Node-RED instance. You can either self-host, on-premise or in the cloud. Or use the managed service \[FlowFuse Cloud]\({{ site.appURL }}). In this example, we will be using a self-hosted FlowFuse instance running on [Docker](https://flowfuse.com/docs/install/docker/). ### Data Treatment on Ethernet/IP PLC In our Allen Bradley line 4 PLC, we will send some arbitrary tags of various datatypes to the stacklight PLC for illustrative purposes, described in table 1 below - | **Tag** | **Data Type** | **Description** | | ----------------- | ------------- | ---------------------------- | | Conveyor\_RTS | BOOL | Conveyor Ready to Start | | Robot\_RTS | BOOL | Robot is Ready to Start | | Robot\_Position | REAL | Robot Arm position (degrees) | | Conveyor\_Running | BOOL | Conveyor is running | | Line4\_State | DINT | Line 4 Machine State | | Line4\_Fault | BOOL | Line 4 is faulted | Table 1 - Line 4 Tags to be sent to Stacklight PLC We can send any atomic data type we want, but it must be globally (controller) scoped. !["Screenshot showing the AB Controller Tags"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-2.png "Screenshot showing the AB Controller Tags") Each tag must also have external read/write access enabled. !["Screenshot showing the AB Tag Properties"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-3.png "Screenshot showing the AB Tag Properties") ### Data Treatment on S7 PLC In the Siemens PLC, we have a DB for the data from the Line 4 PLC to be written to. - In the DBs attributes, “optimized block access” must be disabled. - The tags must be writeable and accessible - !["Screenshot showing the Siemens Tag DB Properties"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-4.png "Screenshot showing the Siemens Tag DB Properties"):br “No protection” must be set in the CPU properties - !["Screenshot showing the Siemens CPU Properties"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-5.png "Screenshot showing the Siemens CPU Properties") ## Create The Flow With both PLCs up and running and properly set up to send/receive remote data, we can now create a flow to act as our protocol converter. ### Install Custom Nodes First, we need to add two custom nodes that will give Node-RED the ability to read/write ethernet/IP and S7 data. Click the hamburger icon → manage pallette ![Screenshot showing the 'Manage palette option' in the menu](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-6.png) On the `install` tab, search for `s7` and install the `node-red-contrib-s7` node. !["Installing S7 node"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-7.png "Installing S7 node") Next, search for `ethernet` and install the `node-red-contrib-cip-ethernet-ip` node. !["InstallING EthernetIP Node"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-8.png "InstallING EthernetIP Node") Go to the `nodes` tab and confirm both custom nodes have been properly installed. !["Screenshot of 'Nodes' tab showing Installed nodes List"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-9.png "Screenshot of 'Nodes' tab showing Installed nodes List") ### Set Up Ethernet/IP Data Let’s start by dragging a `eth-ip in` node onto the pallette. Then add a new endpoint, which will point to our Line4 PLC. !["Screenshot showing dragged 'eth-ip in' node and it's config tab"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-10.png "Screenshot showing dragged 'eth-ip in' node and it's config tab") In the endpoint `connection` properties, the connection information must match the PLC, so set the IP address and CPU slot number appropriately. Also, the default cycle time is 500ms. Depending on your application, polling the CPU at 500ms may be appropriate. But being that this is a simple stacklight, 500ms is unnecessarily fast. So we will change it to 1000ms, which is a more appropriate polling rate for this type of application. !["Screenshot showing the eth-ip Endpoint config"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-11.png "Screenshot showing the eth-ip Endpoint config") On the `Tags` tab, populate the tag information to match our Allen Bradley PLC. Then select `Update` to complete configuration of the `eth-ip endpoint`. !["Screenshot showing eth-ip Endpoint Tags"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-12.png "Screenshot showing eth-ip Endpoint Tags") Now that we have our endpoint, let’s finish configuring the `eth-ip in` node. 1. select the endpoint we just created 2. select the first tag in the drop-down 3. give the node a descriptive name ![Screeshot showing the eth-ip in Node config](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-13.png "Screeshot showing the eth-ip in Node config") Now let’s set up a quick test to confirm our PLC connection is valid by adding a `debug` node to the `eth-ip in` node. Then hit `deploy`. - note - you can see we also have a `comment` above the nodes that describes what is happening. This is optional but good practice to help organize and understand your flow. The output of the debug console did not report any errors so communication appears to be okay. ![Screenshot showing the output of eth-ip in Debug panel](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-14.png "Screenshot showing the output of eth-ip in Debug panel") But just to confirm, let’s toggle the value and see if comes through. ![Screenshot showing the eth-ip node output in Debug panel after Toggle](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-15.png "Screenshot showing the eth-ip node output in Debug panel after Toggle") So by toggling the value and see the result, here we confirmed 2 things: - We can detect changes in value - the `eth-ip in` node only sends a message when the value changes, also known as Report by Exception. Because the `eth-ip in` node implicitly uses report by exception, and the protocol doesn't rely on contiguous data consistency (unlike modbus, for instance), we can receive our data one tag at a time to keep our flow simple. Now we can remove the debug node and add the additional `eth-ip in` nodes to receive the remaining tags from our Line 4 PLC. Here’s how the the flow should look at this point. ![Screenshot of Line 4 PLC Nodes](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-16.png "Screenshot of Line 4 PLC Nodes") ### Set Up S7 Data Now we’ll set up the S7 endpoint, using an `s7 out` node. ![Screenshot of s7 out Node on Palette](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-17.png "Screenshot of s7 out Node on Palette") Populate the connection properties to match your hardware. The cycle time is updated to 1000ms to match the cycle time of our `eth-ip in` nodes. You can adjust this value to match your intended application. !["Screenshot showing the S7 endpoint Connection"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-18.png "Screenshot showing the S7 endpoint Connection") On the `Variables` tab, some special formatting is required to point to the absolute reference of the tag DB location in the S7 PLC. For information on how to format S7 absolute tag references in a way the `s7 endpoint` node is expecting, refer to the [node documentation](https://flows.nodered.org/node/node-red-contrib-s7){rel=""nofollow""} for further information. For reference, here is an example of how we set the tags in our stacklight PLC example and how it looks in our `s7 endpoint`. !["Screenshot of s7 endpoint Variables"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-19.png "Screenshot of s7 endpoint Variables") Once the tags are populated we can select our configured endpoint from the dropdown list, point to our first variable, `Conveyor_RTS`, and give the node a name. !["Screenshot of S7 out Config"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-20.png "Screenshot of S7 out Config") Repeat this process for the remaining tags. !["Screenshot of Stacklight PLC Nodes"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-21.png "Screenshot of Stacklight PLC Nodes") ## Test the Conversion The only thing remaining is to simply wire the nodes together, and confirm the values pass through. !["Screenshot of the complete flow with live Data"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-22.png "Screenshot of the complete flow with live Data") Manipulate the incoming values and confirm the data passes through as expected. Because of the report by exception nature of the `eth-ip in` node, tag changes should be near instantaneous on the receiving PLC. We can stop here, but we can improve this flow by adding a `filter` node on our REAL data-type, `Robot_Position`. ### Add Filter to REAL data Depending on how noisy the REAL data is, which is common with unfiltered 4-20mA field transmitters, and how much granularity you need to capture, it is good practice to add a filter on REAL data to reduce FieldBus traffic coming out of our soft protocol converter. !["Screenshot showing the Filter node Configuration"](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-23.png "Screenshot showing the Filter node Configuration") In the example above, we arbitrarily applied a 3% [deadband](https://flowfuse.com/docs/node-red/core-nodes/function/filter/) to the `Robot_Position` value, which means that the value must change by greater than or equal to 3% compared to the last input value, or else the data will be discarded before being sent to the stacklight PLC. You can adjust the deadband to find the right balance for your particular application. We can see the effect the deadband filter had by adding debug nodes before and after the filter. ![Filter Node Debug](https://flowfuse.com/blog/2023/06/images/ethip-to-S7/e-to-p-24.png "Filter Node Debug") As shown above, when `Robot_Position` changed from 15.6 to 15.6999..., the value was captured on the input of the filter, but was discarded on the output. When the `Robot_Position` went from 15.6999 to 18, the filter allowed it to pass as it exceeded the deadband limit we had set. Use filters to optimize your fieldbus converter network performance, especially if dealing with noisy signals or large quantities of REAL datatypes. ## Conclusion In this tutorial, we demonstrated how to use Node-RED as a free Ethernet/IP to S7 protocol converter using a simple no-code approach. We showed how to configure PLC tags to be sent remotely using Ethernet/IP, how to configure PLC tags to be received remotely using S7, and how to create the flow to use Node-RED to seamlessly convert incoming PLC data between the two protocols using `node-red-contrib-cip-ethernet-ip` and `node-red-contrib-s7` custom nodes. We also took things one step further and added a `filter` node to optimize FieldBus network traffic by putting a deadband on REAL data being sent to the receiving PLC. The end result is a simple to set up, free and performant industrial protocol converter that requires minimal PLC configuration, which allows this application to be applied in non-mission critical production systems with minimal, if any downtime. Additionally, the protocol traffic can be visually observed in real-time for easy trouble-shooting and fault analysis by simply accessing the Node-RED UI. In later tutorials, we can show ways this simple flow can be extended to add additional capabilities not normally available in traditional off-the-shelf protocol gateways. If you found this tutorial helpful, or have any questions or comments, please leave us a comment and let us know your thoughts. JSON source code for the flow used in this tutorial is provided below - ::render-flow{:height='500'} ```json [{"id":"ad7b17411c8e83aa","type":"tab","label":"Line 4 to Stacklight PLC","disabled":false,"info":"","env":[]},{"id":"c97a4c9bd1981757","type":"comment","z":"ad7b17411c8e83aa","name":"AB EIP/CIP - Line 4 PLC","info":"","x":190,"y":140,"wires":[]},{"id":"2cc5227ef6a90814","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Conveyor_RTS","program":"","name":"Read Conveyor_RTS","x":200,"y":200,"wires":[["fe18ef80f9e18c13"]]},{"id":"9308dcbda17274c7","type":"comment","z":"ad7b17411c8e83aa","name":"Siemens S7 - Stacklight PLC","info":"","x":620,"y":140,"wires":[]},{"id":"fe18ef80f9e18c13","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Conveyor_RTS","name":"Write Conveyor_RTS","x":620,"y":200,"wires":[]},{"id":"94fe6b73efa1c56b","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Robot_RTS","program":"","name":"Read Robot_RTS","x":180,"y":280,"wires":[["7774d6ce188c288c"]]},{"id":"7e9564cd59e3d0a2","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Robot_Position","program":"","name":"Read Robot_Position","x":200,"y":360,"wires":[["832807bfdc4b76f0"]]},{"id":"c0f712b9e355f1f8","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Conveyor_Running","program":"","name":"Read Conveyor_Running","x":210,"y":440,"wires":[["fbf1b3e38897a9c7"]]},{"id":"db77621e418f1222","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Line4_State","program":"","name":"Read Line4_State","x":190,"y":520,"wires":[["cdeffd9e52cc4384"]]},{"id":"848af9b76f969dd2","type":"eth-ip in","z":"ad7b17411c8e83aa","endpoint":"4ab2910b66e16220","mode":"single","variable":"Line4_Fault","program":"","name":"Read Line4_Fault","x":190,"y":600,"wires":[["0c595b0ac2550593"]]},{"id":"7774d6ce188c288c","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Robot_RTS","name":"Write Robot_RTS","x":610,"y":280,"wires":[]},{"id":"f1572463c50bb4cb","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Robot_Position","name":"Write Robot_Position","x":620,"y":360,"wires":[]},{"id":"fbf1b3e38897a9c7","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Conveyor_Running","name":"Write Conveyor_Running","x":630,"y":440,"wires":[]},{"id":"cdeffd9e52cc4384","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Line4_State","name":"Write Line4_State","x":610,"y":520,"wires":[]},{"id":"0c595b0ac2550593","type":"s7 out","z":"ad7b17411c8e83aa","endpoint":"a1bec25858c6f3ef","variable":"Line4_Fault","name":"Write Line4_Fault","x":610,"y":600,"wires":[]},{"id":"832807bfdc4b76f0","type":"rbe","z":"ad7b17411c8e83aa","name":"","func":"deadbandEq","gap":"3%","start":"","inout":"in","septopics":true,"property":"payload","topi":"topic","x":420,"y":360,"wires":[["f1572463c50bb4cb"]]},{"id":"4ab2910b66e16220","type":"eth-ip endpoint","address":"192.168.0.5","slot":"0","cycletime":"1000","name":"Line4","vartable":{"":{"Conveyor_RTS":{"type":"BOOL"},"Robot_RTS":{"type":"BOOL"},"Robot_Position":{"type":"REAL"},"Conveyor_Running":{"type":"BOOL"},"Line4_State":{"type":"DINT"},"Line4_Fault":{"type":"BOOL"}}}},{"id":"a1bec25858c6f3ef","type":"s7 endpoint","transport":"iso-on-tcp","address":"192.168.0.10","port":"102","rack":"0","slot":"1","localtsaphi":"01","localtsaplo":"00","remotetsaphi":"01","remotetsaplo":"00","connmode":"rack-slot","adapter":"","busaddr":"2","cycletime":"1000","timeout":"3000","name":"Stacklight PLC","vartable":[{"addr":"DB1,X0.0","name":"Conveyor_RTS"},{"addr":"DB1,X0.1","name":"Robot_RTS"},{"addr":"DB1,R2","name":"Robot_Position"},{"addr":"DB1,X6.0","name":"Conveyor_Running"},{"addr":"DB1,DI8","name":"Line4_State"},{"addr":"DB1,X12.0","name":"Line4_Fault"}]}] ``` :: # Community News July 2023 Welcome to the FlowFuse newsletter for July 2023, a monthly roundup of what’s been happening with FlowFuse and the wider Node-RED community. ## New Release Last week we released FlowFuse 1.9, featuring new API documentation available in the Swagger UI and the ability to customize Node-RED palettes. Read about the details of FlowFuse 1.9 in our [release announcement](https://flowfuse.com/blog/2023/07/flowforge-1-9-release/). ## Upcoming events ### How to deploy Node-RED to hundreds of PLCs and IoT edge devices Our next webinar will be focused on the device management capabilities in the FlowFuse platform. Lots of companies are deploying Node-RED to PLCs and IIoT edge computers. FlowFuse allows these companies to scale and manage Node-RED deployment out to hundreds of these types of devices. Discover how during our next webinar. [Sign-up today](https://flowfuse.com/webinars/2023/flowforge-device-management/) to join us on July 27. ## From our Blog and Documentation - [The Next Step in Data Visualization - Announcing the Successor to the Node-RED Dashboard](https://flowfuse.com/blog/2023/06/dashboard-announcement/) - FlowFuse announces plans to develop the next version of the Node-RED Dashboard project. - [Node-RED as a No-Code Ethernet/IP to S7 Protocol Converter](https://flowfuse.com/blog/2023/06/node-red-as-a-no-code-ethernet_ip-to-s7-protocol-converter/) - A guide to using Node-RED for converting ethernet IP data to Siemens S7. Also a [video version](https://youtu.be/dteXgcBXUnk){rel=""nofollow""} of the same content. - [MQTT and its Role in IoT and Industrial IoT](https://flowfuse.com/docs/node-red/protocol/mqtt/) - A practical explainer on the role of MQTT in IoT use cases and how to connect with an MQTT broker in Node-RED. - Two new Node-RED Nodes Explained articles - [Nodes explained: Split](https://flowfuse.com/docs/node-red/core-nodes/sequence/split/) - [Nodes explained: Filter](https://flowfuse.com/docs/node-red/core-nodes/function/filter/) ## From the Community - [Node-RED & Industry 4.0: The Future is Now](https://youtu.be/1GKkXJOQMhU){rel=""nofollow""} - An informative panel discussion led by Walker Reynolds on the role of Node-RED in the future of Industry 4.0. - [Node-RED Terminology](http://blog.openmindmap.org/blog/node-red-terminology){rel=""nofollow""} - An explanation of the different Node-RED terminology. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} # First Pre-Alpha Release of the new Node-RED Dashboard Just weeks ago, we at FlowFuse [announced our plan](https://flowfuse.com/blog/2023/06/dashboard-announcement/) to develop a successor to the Node-RED Dashboard. Today, we're excited to reveal the pre-alpha release of this highly anticipated project, bringing us one step closer to a new era of data visualization in Node-RED. ## Sneak Peek into the New Node-RED Dashboard The Node-RED Dashboard successor is now available for install as an npm package under the name [@flowforge/node-red-dashboard](https://www.npmjs.com/package/@flowforge/node-red-dashboard){rel=""nofollow""} in your Node-RED palette manager. This pre-alpha version includes the first set of Vue.js-based elements familiar to Node-RED Dashboard users: !["new Node-RED Dashboard Elements"](https://flowfuse.com/blog/2023/07/images/nr-dashboard-screenshot.png "new Node-RED Dashboard Elements") This is but a hint of what's to come. The objective of these pre-alpha releases is to provide early access to the current status. The strength of the project comes from the community. Your insights, suggestions and contributions play a significant role in shaping the future of this dashboard. Keep them coming through our [Github page](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. ## Current Status and Next Steps As of now, we've implemented the following Dashboard Widgets: - UI Text ([ui-text Widget](https://github.com/FlowFuse/node-red-dashboard/issues/38){rel=""nofollow""}) - Text Input ([ui-text-input Widget](https://github.com/FlowFuse/node-red-dashboard/issues/39){rel=""nofollow""}) - Range Slider ([ui-slider Widget](https://github.com/FlowFuse/node-red-dashboard/issues/47){rel=""nofollow""}) - Dropdown/Select ([ui-dropdown Widget](https://github.com/FlowFuse/node-red-dashboard/issues/45){rel=""nofollow""}) We've also introduced a new widget: - Markdown ([ui-markdown Widget](https://github.com/FlowFuse/node-red-dashboard/issues/62){rel=""nofollow""}) Not yet part of the first Pre-Alpha Release: - Toggle Switch ([ui-switch Widget](https://github.com/FlowFuse/node-red-dashboard/issues/42){rel=""nofollow""}) - Color Picker ([ui-color-picker Widget](https://github.com/FlowFuse/node-red-dashboard/issues/46){rel=""nofollow""}) - Number Input ([ui-numeric Widget](https://github.com/FlowFuse/node-red-dashboard/issues/41){rel=""nofollow""}) - Form ([ui-form Widget](https://github.com/FlowFuse/node-red-dashboard/issues/49){rel=""nofollow""}) - Date selector ([ui-date-picker](https://github.com/FlowFuse/node-red-dashboard/issues/32){rel=""nofollow""}) Our immediate focus is to continue adding the missing elements from the original Node-RED Dashboard, releasing each as soon as they're fully developed. This will significantly increase the frequency of our releases in the upcoming weeks. In addition to these releases, we plan to publish regular blog posts titled "What's New in Node-RED Dashboard". These posts will keep you informed of all the latest features, updates, and improvements. # FlowFuse 1.9.3 and Device Agent 1.9.5 released FlowFuse and the Device Agent both received updates yesterday that bring improvements to the Device Agent editor experience, making it more resilient to network issues. ## Improving the Device Agent editor experience The ability to remotely edit flows running the Device Agent has been warmly welcomed by many users on the platform. Along with that comes great feedback we can use to continue improving the user experience. Some early feedback identified issues with the resilience of the tunnel we connect between the Device Agent and the platform. If the tunnel was interrupted for any reason, the user would have to manually set it up again. With the FlowFuse 1.9.3 release, now running FlowFuse Cloud, along with the latest version of the Device Agent, we have made the tunnel much more resilient. It can now restablish itself without any intervention from the user - making for a much more seamless experience. - [#2488](https://github.com/FlowFuse/flowfuse/pull/2488){rel=""nofollow""} - [#2507](https://github.com/FlowFuse/flowfuse/pull/2507){rel=""nofollow""} ## Other New Features and Bug Fixes - Fixes incorrect 'start-failed' notifications when restarting an instance [#2505](https://github.com/FlowFuse/flowfuse/pull/2505){rel=""nofollow""} The system log now includes more information about the callingThe FlowFuse device agent is now supported on Windows [#78](https://github.com/FlowFuse/device-agent/issues/78){rel=""nofollow""} - Ensures the system logging captures the proper source IP address of requests [#2505](https://github.com/FlowFuse/flowfuse/pull/2503){rel=""nofollow""} - A few documentation updates, including a clarfication on how to run the Device Agent under docker [#2498](https://github.com/FlowFuse/flowfuse/pull/2498){rel=""nofollow""} ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our [Product Roadmap Page](https://flowfuse.com/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.9.3. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go the the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # FlowFuse now offers API Documentation with Swagger UI FlowFuse 1.9 adds new features to make it easier to administer FlowFuse platform deployments, including new API documentation and the ability to create customized Node-RED palettes. ## API Documentation FlowFuse API allows developers to programmatically interact with the FlowFuse platform. This makes it possible to integrate FlowFuse into different infrastructure technologies, create scripts to automate specific FlowFuse tasks and embed FlowFuse into other applications. In the 1.9 release we are now publishing our [API documentation](https://flowfuse.com/docs/api/) using the [OpenAPI specification](https://swagger.io/specification/){rel=""nofollow""} and making it viewable with the Swagger UI. Both these industrial standards will make using the FlowFuse API easier to use and understand. ## Customize Node-RED Palettes [#2002](https://github.com/FlowFuse/flowfuse/issues/2002){rel=""nofollow""} FlowFuse platform adminstrators are now able to create customized Node-RED palettes that will be used when a Node-RED instance is created. An adminstator can create pre-defined templates to specify the nodes that should be included in the palette. This makes it easier for FlowFuse teams to standardized on Node-RED usage across an organization. Note: this feature is not available for FlowFuse cloud users since they do not have administrator access. ## New RBAC Role for Dashboard users [#2292](https://github.com/FlowFuse/flowfuse/issues/1924){rel=""nofollow""} A new FlowFuse user role has been created to view Node-RED dashboards. This allows for users to view Nod-RED dashboards without access to the Node-RED editor or requiring separate login credentials. ## Other New Features - FlowFuse device agent is now supported on Windows [#78](https://github.com/FlowFuse/device-agent/issues/78){rel=""nofollow""} - Allow local configuration of https/httpStatic on a device [#110](https://github.com/FlowFuse/device-agent/issues/110){rel=""nofollow""} - Implementing custom certificate settings for device configuration [#2257](https://github.com/FlowFuse/flowfuse/issues/2257){rel=""nofollow""} - Allow devices to access the "Snapshot ID" and the "Snapshot Name" running on them [#94](https://github.com/FlowFuse/device-agent/issues/94){rel=""nofollow""} - High Availability logging enhanced: Individual Node-RED Instance replica querying and filtering [#2260](https://github.com/FlowFuse/flowfuse/issues/2260){rel=""nofollow""} - High Availability is now generally available [#2414](https://github.com/FlowFuse/flowfuse/issues/2412){rel=""nofollow""} ## Bug Fixes - Can not promote NR instance in DevOps Pipeline [#2363](https://github.com/FlowFuse/flowfuse/issues/2363){rel=""nofollow""} - Billing team menu item missing on first page load [#2398](https://github.com/FlowFuse/flowfuse/issues/2398){rel=""nofollow""} - Duplicate labels in Instance Import dialog [#2200](https://github.com/FlowFuse/flowfuse/issues/2200){rel=""nofollow""} - Broken littie animations [#2354](https://github.com/FlowFuse/flowfuse/issues/2354){rel=""nofollow""} - Instance Logs page doesn't handle errors well [#1083](https://github.com/FlowFuse/flowfuse/issues/1083){rel=""nofollow""} - Device continues to run edited flows once taken out of dev mode [#2323](https://github.com/FlowFuse/flowfuse/issues/2323){rel=""nofollow""} - Device Editor cannot pickup FF theme [#89](https://github.com/FlowFuse/device-agent/issues/89){rel=""nofollow""} ## Community Contributions Thanks to our community members for their contributions to this release. - sumitshinde-84 - make Instance and application names in delete popup easily selectable [#2291](https://github.com/FlowFuse/flowfuse/pull/2291){rel=""nofollow""} - biancode - Fixed typo in doc [#2327](https://github.com/FlowFuse/flowfuse/pull/2327){rel=""nofollow""} ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our [Product Roadmap Page](https://flowfuse.com/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.9. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go the the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # How to Build an OPC UA Client Dashboard in Node-RED - Part 3 (2026) This article is the third and final part of our OPC UA content series. In the [first article](https://flowfuse.com/blog/2023/07/how-to-deploy-a-basic-opc-ua-server-in-node-red/), we cover some OPC UA fundamentals and walk through an example OPC UA Server flow. In the [second article](https://flowfuse.com/docs/node-red/protocol/opc-ua/), we built a SSL-secured OPC UA server using data from an Allen Bradley PLC as a source. In this article, we show how to build an OPC Client in Node-RED that communicates with a 3rd party OPC UA Server and utilizes an interactive dashboard. This article will requires the [Prosys OPC UA Simulation Server](https://prosysopc.com/products/opc-ua-simulation-server/){rel=""nofollow""}, an application designed for testing OPC UA client applications and learning the technology. It’s a free cross-platform application that supports Windows, Linux, and MacOS. This article will use the Windows version. Note: full source code for the OPC Client Dashboard is included at the end of the article. ## Custom Nodes Used & Assumptions Several custom nodes are required in order to properly deploy this flow. For more detailed information on how to install a custom node, follow the instructions from an [earlier article](https://flowfuse.com/blog/2023/06/node-red-as-a-no-code-ethernet_ip-to-s7-protocol-converter/) where the process on installing custom nodes is explained in detail. - [@flowfuse/node-red-dashboard](https://flows.nodered.org/node/@flowfuse/node-red-dashboard){rel=""nofollow""} - [node-red-contrib-opcua](https://flows.nodered.org/node/node-red-contrib-opcua){rel=""nofollow""} - [@flowfuse/node-red-dashboard-2-ui-led](https://flows.nodered.org/node/@flowfuse/node-red-dashboard-2-ui-led){rel=""nofollow""} As this is not a production application, no security will be utilized, and it is assumed that the OPC UA Server is running on the same network as the Node-RED OPC Client. Is it also assumed that the end user of this article has familiarization with dashboards. There are many dashboard basic guides available on our FlowFuse website, For more infomation go to [Node-RED Dashboard 2.0 guides](https://flowfuse.com/blog/dashboard/). ## Install and Deploy the Prosys OPC UA Simulation Server The Prosys OPC UA Simulation Server is [free to download](https://prosysopc.com/products/opc-ua-simulation-server/evaluate/){rel=""nofollow""}, but requires a sign-up process. Download and install the server, then run the application. Once the application is started, the first thing you should do is go to `options -> switch to expert mode`. This will give us access to the address space tab, which we will need to develop our client application in Node-RED. ![expert-mode.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/expert-mode.png){dataZoomable=""} When the application is run, an endpoint url will be displayed on the `status` tab, along with an indication that the server is currently running. ![opc-endpoint-url.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/opc-endpoint-url.png){dataZoomable=""} Copy the connection endpoint, but be warned that you will likely need to replace the computer name (in my case `DESKTOP-0K0483A`, with the actual IP address of the machine running the server. The IP address of the machine on my local network is `192.168.0.141`, which changes my UA TCP endpoint address to `opc.tcp://192.168.0.141:53530/OPCUA/SimulationServer`. Now the simulation server is set up and we are ready to start developing the OPC Client application. ## Objectives of the Node-RED OPC Client Dashboard Application The goal is not to develop a production-level application, rather, it’s to show a variety of features that one can utilize to demonstrate common OPC UA Client application capabilities in Node-RED, while also demonstrating a variety of methods to visualize the results in a dashboard. There are 4 main objectives of the Node-RED OPC Client Dashboard application. They are: 1. Browse hierarchical server address space structure & display on a dashboard 2. Read OPC UA values from various namespaces, showing a variety of datatypes and different ways they can be visualized 3. Write OPC UA values back to the OPC UA server directly from the OPC UA Client dashboard 4. Read alarms & events from the OPC UA Server and display them on the dashboard Rather than building the flow step-by-step, the flow source code will be presented for each objective, and a the flow will be explained so that it is understood what is happening in each section of code. ## Browse Hierarchical Server Address Space Structure With OPC UA Browser Node :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} The first flow will browse the hierarchical OPC UA Server address space structure and display it on the dashboard. ![image-20230727-085611.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/image-20230727-085611.png){dataZoomable=""} You can import this flow into Node-RED using the code below: ::render-flow ```json [{"id":"ca62be3e01388319","type":"group","z":"5b972161c4e0464e","name":"Browse Hierarchical Address Space Structure & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["6b17b2da2b942bb4","61797eccf2785257","4d92d940177b6ee3","68a113d5893b7c01","d0c969b6a59fac3a","639da01fc957e547","29437ca7222d9a64","49983d5da0958bf2","49040d0cf1144f0a","e7c55f412ef86543","de21b7ad98a05833","2d56e9a431c21a3b","ac95bd0e2b304eec","6fdabcc2950ccf4e","1c49fa5142d2cf17","335878527020598c","7b208f2e8cba6205","52dd2e5dcddad58f","a5acdccfd2033aec","157322c9c360446d","78a012e5db377fd9"],"x":94,"y":139,"w":1172,"h":422},{"id":"6b17b2da2b942bb4","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":280,"wires":[["4d92d940177b6ee3","d0c969b6a59fac3a","639da01fc957e547"]]},{"id":"61797eccf2785257","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get Base Folder Structure","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":280,"y":280,"wires":[["6b17b2da2b942bb4"]]},{"id":"4d92d940177b6ee3","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Simulation Folder","rules":[{"t":"set","p":"Objects.Simulation.nodeId","pt":"flow","to":"payload[2].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[2].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":220,"wires":[["335878527020598c"]]},{"id":"68a113d5893b7c01","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Display on Dashboard","info":"","x":1140,"y":180,"wires":[]},{"id":"d0c969b6a59fac3a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyObjects Folder","rules":[{"t":"set","p":"Objects.MyObjects.nodeId","pt":"flow","to":"payload[4].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":340,"wires":[["52dd2e5dcddad58f"]]},{"id":"639da01fc957e547","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticData Folder","rules":[{"t":"set","p":"Objects.StaticData.nodeId","pt":"flow","to":"payload[3].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[3].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":280,"wires":[["7b208f2e8cba6205"]]},{"id":"29437ca7222d9a64","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":440,"wires":[["49040d0cf1144f0a","e7c55f412ef86543"]]},{"id":"49983d5da0958bf2","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get StaticData Folder Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":270,"y":440,"wires":[["29437ca7222d9a64"]]},{"id":"49040d0cf1144f0a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"AnalogItemArrays Folder","rules":[{"t":"set","p":"Objects.StaticData.AnalogItemArrays.nodeId","pt":"flow","to":"payload[1].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[1].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":460,"wires":[["157322c9c360446d"]]},{"id":"e7c55f412ef86543","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticArrayVariables Folder","rules":[{"t":"set","p":"Objects.StaticData.StaticArrayVariables.nodeId","pt":"flow","to":"payload[6].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[6].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":400,"wires":[["a5acdccfd2033aec"]]},{"id":"de21b7ad98a05833","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyDevice Object","rules":[{"t":"set","p":"Objects.MyObjects.MyDevice.nodeId","pt":"flow","to":"payload[0].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":520,"wires":[["78a012e5db377fd9"]]},{"id":"2d56e9a431c21a3b","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get MyObjects Object Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.MyObjects.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.5","topic":"","payload":"","payloadType":"date","x":270,"y":520,"wires":[["ac95bd0e2b304eec"]]},{"id":"ac95bd0e2b304eec","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":520,"wires":[["de21b7ad98a05833"]]},{"id":"6fdabcc2950ccf4e","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Store & Parse nodeId & browseName","info":"","x":850,"y":180,"wires":[]},{"id":"1c49fa5142d2cf17","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Global Address Space Folder Browse","info":"","x":410,"y":220,"wires":[]},{"id":"335878527020598c","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"Simulation","order":1,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":220,"wires":[[]]},{"id":"7b208f2e8cba6205","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticData","order":2,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":280,"wires":[[]]},{"id":"52dd2e5dcddad58f","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyObjects","order":5,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":340,"wires":[[]]},{"id":"a5acdccfd2033aec","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticArrayVariables","order":3,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1140,"y":400,"wires":[[]]},{"id":"157322c9c360446d","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"AnalogItemArrays","order":4,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1130,"y":460,"wires":[[]]},{"id":"78a012e5db377fd9","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyDevice","order":6,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1100,"y":520,"wires":[[]]},{"id":"53f4394dbf12c6b7","type":"OpcUa-Endpoint","endpoint":"opc.tcp://192.168.56.1:53530/OPCUA/SimulationServer","secpol":"None","secmode":"None","none":true,"login":false,"usercert":false,"usercertificate":"","userprivatekey":""},{"id":"ef9998baf5f61e8a","type":"ui-group","name":" Address Space Folder Structure","page":"44d3feb2a1143d7b","width":"2","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"44d3feb2a1143d7b","type":"ui-page","name":"OPC UA","ui":"5355e0c476f9da3b","path":"/opcua","icon":"home","layout":"grid","theme":"61eee6fc60281b9b","order":1,"className":"","visible":"true","disabled":"false"},{"id":"5355e0c476f9da3b","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"61eee6fc60281b9b","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}] ``` :: To understand what is going on in this flow, we must refer back to the OPC UA Simulation Server `Address Space` tab. When we browse the OPC Server base folder structure in Node-RED, we will be browsing everything included under the `Objects` tree. ![address-space.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/address-space.png){dataZoomable=""} In our flow, we get the base folder structure by using an OPC-UA Browser node, as shown, with an endpoint that points to our OPC UA Server endpoint url we grabbed earlier in this article. It is also worth noting we leave the `Topic` blank. By doing this, we will browse the entire folder structure by default. ![image-20230727-090252.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/image-20230727-090252.png){dataZoomable=""} The configuration of the endpoint properties includes no security credentials, as shown below. ![endpoint-configure.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/endpoint-configure.png){dataZoomable=""} Using the output of a debug node, we get from the OPC UA Browser yield a payload with an array of 5 objects. ![address-debug.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/address-debug.png){dataZoomable=""} Each object returned represents the 5 objects that are in our OPC UA Server Objects tree. ![browse-payload-1.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/browse-payload-1.png){dataZoomable=""} However, of those 5 objects, only 3 of them are folders that contain actual OPC values. `MyObjects`, `Simulation`, and `StaticData`. We can ignore `Aliases` and `Server`. ![address-space-folders-only.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/address-space-folders-only.png){dataZoomable=""} So looking deeper into the payload of our global browse from the `OPC UA Browser node`, we can drill down into the details and see how they correlate with the folders in the server. ![browse-node.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/browse-node.png){dataZoomable=""} As shown above, element 2 in the array returned from the global browse corresponds to the `Simulation` folder. And we are interested in two important values in this data-structure - the `NodeId`, which is topic an OPC Client uses to point specific OPC values, and the `browseName`, which is the name we see visually when we try to identify an OPC topic. We can now use this logic to parse out this useful information using a change node. ![simulation-folder.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/simulation-folder.png){dataZoomable=""} This change node is grabbing the `nodeId` and `browseName` . The `nodeId` is stored in a context variable for later use, while the `browseName` is used as the payload to be displayed on our dashboard. The rest of the flow follows this same pattern, to end up with a folder structure that we can display on our dashboard that matches the structure on our OPC Server - note - to make the flow more manageable, not all browsable folders were included in the dashboard, as this flow is just meant to serve as an example, rather than be a 1:1 copy of everything in the server. If you deploy the flow and pull up the dashboard, it results in the following output - ![address-space-dashboard.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/address-space-dashboard.png){dataZoomable=""} Showing side-by-side with the server, you can see that we successfully browsed a portion of the address space and displayed the values on the dashboard. Admittedly, a lot of work for not much pay-off, but it’s a worthwhile exercise in understanding how to browse topics using the `OPC UA Browser` node. The browser node is best used for reading OPC UA values, which will be covered next. ## Read OPC UA Values Using OPC UA Browser Node The next set of flows read OPC UA values from the server and displays them on the dashboard. ![read-opc-values.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/read-opc-values.png){dataZoomable=""} You can import these flows into Node-RED using the code below: ::render-flow ```json [{"id":"ca62be3e01388319","type":"group","z":"5b972161c4e0464e","name":"Browse Hierarchical Address Space Structure & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["6b17b2da2b942bb4","61797eccf2785257","4d92d940177b6ee3","68a113d5893b7c01","d0c969b6a59fac3a","639da01fc957e547","29437ca7222d9a64","49983d5da0958bf2","49040d0cf1144f0a","e7c55f412ef86543","de21b7ad98a05833","2d56e9a431c21a3b","ac95bd0e2b304eec","6fdabcc2950ccf4e","1c49fa5142d2cf17","335878527020598c","7b208f2e8cba6205","52dd2e5dcddad58f","a5acdccfd2033aec","157322c9c360446d","78a012e5db377fd9"],"x":94,"y":139,"w":1172,"h":422},{"id":"6b17b2da2b942bb4","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":280,"wires":[["4d92d940177b6ee3","d0c969b6a59fac3a","639da01fc957e547"]]},{"id":"61797eccf2785257","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get Base Folder Structure","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":280,"y":280,"wires":[["6b17b2da2b942bb4"]]},{"id":"4d92d940177b6ee3","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Simulation Folder","rules":[{"t":"set","p":"Objects.Simulation.nodeId","pt":"flow","to":"payload[2].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[2].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":220,"wires":[["335878527020598c"]]},{"id":"68a113d5893b7c01","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Display on Dashboard","info":"","x":1140,"y":180,"wires":[]},{"id":"d0c969b6a59fac3a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyObjects Folder","rules":[{"t":"set","p":"Objects.MyObjects.nodeId","pt":"flow","to":"payload[4].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":340,"wires":[["52dd2e5dcddad58f"]]},{"id":"639da01fc957e547","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticData Folder","rules":[{"t":"set","p":"Objects.StaticData.nodeId","pt":"flow","to":"payload[3].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[3].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":280,"wires":[["7b208f2e8cba6205"]]},{"id":"29437ca7222d9a64","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":440,"wires":[["49040d0cf1144f0a","e7c55f412ef86543"]]},{"id":"49983d5da0958bf2","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get StaticData Folder Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":270,"y":440,"wires":[["29437ca7222d9a64"]]},{"id":"49040d0cf1144f0a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"AnalogItemArrays Folder","rules":[{"t":"set","p":"Objects.StaticData.AnalogItemArrays.nodeId","pt":"flow","to":"payload[1].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[1].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":460,"wires":[["157322c9c360446d"]]},{"id":"e7c55f412ef86543","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticArrayVariables Folder","rules":[{"t":"set","p":"Objects.StaticData.StaticArrayVariables.nodeId","pt":"flow","to":"payload[6].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[6].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":400,"wires":[["a5acdccfd2033aec"]]},{"id":"de21b7ad98a05833","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyDevice Object","rules":[{"t":"set","p":"Objects.MyObjects.MyDevice.nodeId","pt":"flow","to":"payload[0].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":520,"wires":[["78a012e5db377fd9"]]},{"id":"2d56e9a431c21a3b","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get MyObjects Object Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.MyObjects.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.5","topic":"","payload":"","payloadType":"date","x":270,"y":520,"wires":[["ac95bd0e2b304eec"]]},{"id":"ac95bd0e2b304eec","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":520,"wires":[["de21b7ad98a05833"]]},{"id":"6fdabcc2950ccf4e","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Store & Parse nodeId & browseName","info":"","x":850,"y":180,"wires":[]},{"id":"1c49fa5142d2cf17","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Global Address Space Folder Browse","info":"","x":410,"y":220,"wires":[]},{"id":"335878527020598c","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"Simulation","order":1,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":220,"wires":[[]]},{"id":"7b208f2e8cba6205","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticData","order":2,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":280,"wires":[[]]},{"id":"52dd2e5dcddad58f","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyObjects","order":5,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":340,"wires":[[]]},{"id":"a5acdccfd2033aec","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticArrayVariables","order":3,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1140,"y":400,"wires":[[]]},{"id":"157322c9c360446d","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"AnalogItemArrays","order":4,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1130,"y":460,"wires":[[]]},{"id":"78a012e5db377fd9","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyDevice","order":6,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1100,"y":520,"wires":[[]]},{"id":"53f4394dbf12c6b7","type":"OpcUa-Endpoint","endpoint":"opc.tcp://192.168.56.1:53530/OPCUA/SimulationServer","secpol":"None","secmode":"None","none":true,"login":false,"usercert":false,"usercertificate":"","userprivatekey":""},{"id":"ef9998baf5f61e8a","type":"ui-group","name":" Address Space Folder Structure","page":"44d3feb2a1143d7b","width":"2","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"44d3feb2a1143d7b","type":"ui-page","name":"OPC UA","ui":"5355e0c476f9da3b","path":"/opcua","icon":"home","layout":"grid","theme":"61eee6fc60281b9b","order":1,"className":"","visible":"true","disabled":"false"},{"id":"5355e0c476f9da3b","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"61eee6fc60281b9b","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"8557072f05e4bda0","type":"group","z":"5b972161c4e0464e","name":"Read Simulation Values & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["9659d40ac9063764","9f5b597ec8179fb4","a8d919f497fcff04","13f5c98b7fd5f5da","ec5dca5eb9d4971b","1780cb86597d3c67","1a2fcac87247cda4","4d9b758e39555124","da468bc150517fa6","82aa12173dd7bbca","57d8777e34b55b7b","10877909d1daf6fe","c4d4a3b0df372e4c","b0cf511f824f2a86","f2efc6b419414c9a"],"x":94,"y":599,"w":1372,"h":302},{"id":"9659d40ac9063764","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"8557072f05e4bda0","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":570,"y":760,"wires":[["ec5dca5eb9d4971b"]]},{"id":"9f5b597ec8179fb4","type":"inject","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Update Simulation Values @ 1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.Simulation.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":760,"wires":[["9659d40ac9063764"]]},{"id":"a8d919f497fcff04","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Read Simulation Values","info":"","x":460,"y":720,"wires":[]},{"id":"13f5c98b7fd5f5da","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Counter Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[1].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1070,"y":680,"wires":[["10877909d1daf6fe"]]},{"id":"ec5dca5eb9d4971b","type":"switch","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":790,"y":760,"wires":[["13f5c98b7fd5f5da","1780cb86597d3c67","1a2fcac87247cda4","4d9b758e39555124"]]},{"id":"1780cb86597d3c67","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Random Number Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[2].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1100,"y":740,"wires":[["c4d4a3b0df372e4c"]]},{"id":"1a2fcac87247cda4","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Sawtooth Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[3].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1080,"y":800,"wires":[["b0cf511f824f2a86"]]},{"id":"4d9b758e39555124","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Sawtooth Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1080,"y":860,"wires":[["f2efc6b419414c9a"]]},{"id":"da468bc150517fa6","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Discard Empty Datasets","info":"","x":780,"y":720,"wires":[]},{"id":"82aa12173dd7bbca","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Parse Simulation Values","info":"","x":1070,"y":640,"wires":[]},{"id":"57d8777e34b55b7b","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Display on Dashboard","info":"","x":1340,"y":640,"wires":[]},{"id":"10877909d1daf6fe","type":"ui-gauge","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Counter","group":"af263064820fb7d0","order":0,"width":3,"height":3,"gtype":"gauge-half","gstyle":"needle","title":"gauge","units":"units","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#5cd65c"},{"from":"15","color":"#ffc800"},{"from":"30","color":"#ea5353"}],"min":0,"max":"30","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":1320,"y":680,"wires":[]},{"id":"c4d4a3b0df372e4c","type":"ui-text","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","order":0,"width":0,"height":0,"name":"Random Number","label":"Random Number","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1350,"y":740,"wires":[]},{"id":"b0cf511f824f2a86","type":"ui-chart","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","name":"","label":"Sawtooth","order":9007199254740991,"chartType":"line","category":"Sawtooth","categoryType":"str","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisProperty":"","ymin":"","ymax":"","action":"append","pointShape":"line","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"60","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"3","height":"4","className":"","x":1320,"y":800,"wires":[[]]},{"id":"f2efc6b419414c9a","type":"ui-chart","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","name":"","label":"Sinusoid","order":9007199254740991,"chartType":"line","category":"Sawtooth","categoryType":"str","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisProperty":"","ymin":"","ymax":"","action":"append","pointShape":"line","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"60","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"3","height":"4","className":"","x":1320,"y":860,"wires":[[]]},{"id":"af263064820fb7d0","type":"ui-group","name":"Simulation values","page":"44d3feb2a1143d7b","width":"3","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"5afdbddf71507886","type":"group","z":"5b972161c4e0464e","name":"Read StaticData Values & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["e998aa804042128b","6c9b7d4d195a1e9a","cd097b744d0ec625","18d21607c87ab153","7b5143c4960f92a1","0625b0cf6f546a4a","9d899fbb4d1648b3","6e1edc31687dde54","051e1f282076fed2","de2a1c3e380f743b","c74606c48ccf5a40","053bda13f2a2eabe","277dcf430dc86996","d708e6264cec0070"],"x":84,"y":939,"w":1382,"h":202},{"id":"e998aa804042128b","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"5afdbddf71507886","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":630,"y":1020,"wires":[["7b5143c4960f92a1"]]},{"id":"6c9b7d4d195a1e9a","type":"inject","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Update AnalogItemArrays Values @ 1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.AnalogItemArrays.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":320,"y":1020,"wires":[["e998aa804042128b"]]},{"id":"cd097b744d0ec625","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Read StaticData Values","info":"","x":520,"y":980,"wires":[]},{"id":"18d21607c87ab153","type":"change","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Get ByteAnalogItemArray Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"$string(payload[0].item.value)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1070,"y":1020,"wires":[["277dcf430dc86996"]]},{"id":"7b5143c4960f92a1","type":"switch","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":830,"y":1020,"wires":[["18d21607c87ab153"]]},{"id":"0625b0cf6f546a4a","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"5afdbddf71507886","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":630,"y":1100,"wires":[["051e1f282076fed2"]]},{"id":"9d899fbb4d1648b3","type":"inject","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Update StaticArrayVariables Values @1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.StaticArrayVariables.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":320,"y":1100,"wires":[["0625b0cf6f546a4a"]]},{"id":"6e1edc31687dde54","type":"change","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Get BooleanArray Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":1100,"wires":[["d708e6264cec0070"]]},{"id":"051e1f282076fed2","type":"switch","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":830,"y":1100,"wires":[["6e1edc31687dde54"]]},{"id":"de2a1c3e380f743b","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Discard Empty Datasets","info":"","x":820,"y":980,"wires":[]},{"id":"c74606c48ccf5a40","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Parse StaticData Values","info":"","x":1070,"y":980,"wires":[]},{"id":"053bda13f2a2eabe","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Display on Dashboard","info":"","x":1340,"y":980,"wires":[]},{"id":"277dcf430dc86996","type":"ui-text","z":"5b972161c4e0464e","g":"5afdbddf71507886","group":"3d4f386e812e8b5f","order":0,"width":0,"height":0,"name":"","label":"ByteAnalogItemArray","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1340,"y":1020,"wires":[]},{"id":"d708e6264cec0070","type":"ui-text","z":"5b972161c4e0464e","g":"5afdbddf71507886","group":"3d4f386e812e8b5f","order":0,"width":0,"height":0,"name":"","label":"BooleanArray","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1320,"y":1100,"wires":[]},{"id":"3d4f386e812e8b5f","type":"ui-group","name":"StaticData Values","page":"44d3feb2a1143d7b","width":"4","height":"1","order":3,"showTitle":true,"className":"","visible":"true","disabled":"false"}] ``` :: The values are derived from the `nodeId` values we stored in memory in our previous flow, via our `change` nodes in the previous flow. ![flow-context-nodeid.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/flow-context-nodeid.png){dataZoomable=""} As stated earlier, you reference a OPC UA topic by its `nodeId`. So we will use these node IDs to read actual values from our OPC nodes. In our first flow, we want to read the values in the `Simulation` folder at a 1 second interval. So we use an `inject` node with a `msg.topic` that references the `nodeId` corresponding to the `Simulation` folder. ![simulation-injection.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/simulation-injection.png){dataZoomable=""} That `msg.topic` tells the `OPC UA Browser` node what `nodeId` to browse. If we look at the debug output of the browser `msg.payload`, we can see that it produces an array of 7 objects, and an empty set array. ![simulation-debug.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/simulation-debug.png){dataZoomable=""} If we allow that empty array to be passed, that means all values will be reset to 0 on each read. So to prevent that from happening, we use a `switch` node to filter out the empty set. ![empty-check.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/empty-check.png){dataZoomable=""} Now only non-empty payloads will be passed, preventing the values being reset to 0 on each read. Now we can actually read the values. To do this, we use a `change` node again, referencing the non-empty payload and drilling down to the `value` that corresponds to the `name` of the node we want to read. In this case, we’re getting the value of the node `Counter` located in the `Simulation` folder. ![get-counter-value.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/get-counter-value.png){dataZoomable=""} Going back to our OPC Server, we can see that exactly where that value is derived below - ![sim-counter-server.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/sim-counter-server.png){dataZoomable=""} Now we add a `gauge` dashboard node to visualize the counter on the dashboard. In the OPC Server, it is shown that the counter increments in a range of 0-30 in 1 count increments. ![counter-properties.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/counter-propertie.png){dataZoomable=""} Now that we’ve gone through the full process of reading an OPC UA value and displaying it on the dashboard, we can apply the same logic other values published by the OPC UA Server, which are repeated in the remaining parts of the flow. The end result on the dashboard now looks like this - :video{ariaLabel="Video showing the OPC UA dashboard reading live values from the server" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} ## Write OPC UA Values To Server Using OpcUa-Item and Opc-Ua-Client Nodes The next flow writes OPC UA values to the server using dashboard UI elements. ![write-mydevice.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/write-mydevice.png){dataZoomable=""} You can import this flow into Node-RED using the code below: ::render-flow ```json [{"id":"3de6c861611c3afa","type":"group","z":"5b972161c4e0464e","name":"Write Mydevices values to OPC UA Server","style":{"label":true,"color":"#000000"},"nodes":["a66583d91b581cd8","3e8cb6e199012155","9fa33d1c9c621611","fb7f57b4da5883ae","9c5ff104eb9c8b10","77bcb828bec95336","afa83dbb46449d4a","fa08f0ed04296363","9f591797b56c565d"],"x":94,"y":1439,"w":792,"h":182},{"id":"a66583d91b581cd8","type":"OpcUa-Item","z":"5b972161c4e0464e","g":"3de6c861611c3afa","item":"ns=6;s=MySwitch","datatype":"Boolean","value":"","name":"Toggle MySwitch","x":470,"y":1520,"wires":[["3e8cb6e199012155"]]},{"id":"3e8cb6e199012155","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"3de6c861611c3afa","endpoint":"53f4394dbf12c6b7","action":"write","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Write MySwitch","x":720,"y":1520,"wires":[[],[]]},{"id":"9fa33d1c9c621611","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Dashboard Input","info":"","x":200,"y":1480,"wires":[]},{"id":"fb7f57b4da5883ae","type":"OpcUa-Item","z":"5b972161c4e0464e","g":"3de6c861611c3afa","item":"ns=6;s=MyLevel","datatype":"Double","value":"","name":"Modify MyLevel","x":460,"y":1580,"wires":[["9c5ff104eb9c8b10"]]},{"id":"9c5ff104eb9c8b10","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"3de6c861611c3afa","endpoint":"53f4394dbf12c6b7","action":"write","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Write MyLevel","x":720,"y":1580,"wires":[[],[]]},{"id":"77bcb828bec95336","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Call OPC UA Item","info":"","x":470,"y":1480,"wires":[]},{"id":"afa83dbb46449d4a","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Write OPC UA Item to Client","info":"","x":740,"y":1480,"wires":[]},{"id":"fa08f0ed04296363","type":"ui-switch","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"","label":"Toggle MySwitch","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"passthru":false,"topic":"topic","topicType":"msg","style":"","className":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":210,"y":1520,"wires":[["a66583d91b581cd8"]]},{"id":"9f591797b56c565d","type":"ui-slider","z":"5b972161c4e0464e","g":"3de6c861611c3afa","group":"ec0ecb26fde8db3e","name":"","label":"Modify MyLevel","tooltip":"","order":0,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":true,"min":"0","max":"100","step":1,"className":"","x":200,"y":1580,"wires":[["fb7f57b4da5883ae"]]},{"id":"53f4394dbf12c6b7","type":"OpcUa-Endpoint","endpoint":"opc.tcp://192.168.56.1:53530/OPCUA/SimulationServer","secpol":"None","secmode":"None","none":true,"login":false,"usercert":false,"usercertificate":"","userprivatekey":""},{"id":"ec0ecb26fde8db3e","type":"ui-group","name":"MyDevice Status & Control","page":"44d3feb2a1143d7b","width":"3","height":"1","order":4,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"44d3feb2a1143d7b","type":"ui-page","name":"OPC UA","ui":"5355e0c476f9da3b","path":"/opcua","icon":"home","layout":"grid","theme":"61eee6fc60281b9b","order":1,"className":"","visible":"true","disabled":"false"},{"id":"5355e0c476f9da3b","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"61eee6fc60281b9b","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}] ``` :: We have two values to write, a boolean value corresponding to the node object `MySwitch`, and an integer value corresponding to the object `MyLevel`. Therefore, we will use a toggle switch to toggle the `MySwitch`, and a slider to modify `MyLevel`. There’s no need to modify the toggle switch properties, other than giving it a name. The slider needs to have the range modified to match the range of the level, which is 0-100%. ![level-range.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/level-range.png){dataZoomable=""} For the `OpcUa-Item` nodes, copy the `NodeId` corresponding to each device, ![copy-node-id.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/copy-node-id.png){dataZoomable=""} and paste it into `OpcUa-Item` node. You must also ensure the data-type matches with the value you’re writing to. ![opcua-item.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/opcua-item.png){dataZoomable=""} The `Opc-Ua-Client` needs to have an endpoint and the action changed to `WRITE`. ![client-node.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/client-node.png){dataZoomable=""} The process is the same for `MySwitch` and `MyLevel`, the only difference being what `NodeId` is referenced in the `OpcUa-Item` node. When deployed, you can confirm values are being written to from the client to the server from the dashboard. :video{ariaLabel="Video showing values being written from the client to the server on the dashboard" autoPlay="true" height="390" loop="true" muted="true" playsInline="true" preload="none" width="800"} ## Read Alarms & Events from OPC UA Server Using OpcUa-Event and Opc-Ua-Client Nodes Our last flow we’ll show how to read OPC UA Alarms & Events. ![opc-event-flow.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/opc-event-flow.png){dataZoomable=""} You can import this flow into Node-RED using the code below: ::render-flow ```json [{"id":"a6e9abacd0bdf3b6","type":"group","z":"5b972161c4e0464e","name":"Read Alarms & Events From OPC UA Server","style":{"label":true,"color":"#000000"},"nodes":["90fb4ca64a642edf","b76f64786bc681c3","71e24b671bc03fb8","c7438df35b506470","c7e8919b636cb51d","5952b86dae22b056","04992b24a3836f19","325068cb935cd6d1","5b4d1bd8b342fc05","ba1ea89438335cb8","d662d662c5ccb9c1","1e3956200997581f","0b8ac86e5e4f9f8d","62b2e14ce0429eef"],"x":94,"y":1679,"w":1352,"h":282},{"id":"90fb4ca64a642edf","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Call OPC UA Item","info":"","x":470,"y":1820,"wires":[]},{"id":"b76f64786bc681c3","type":"OpcUa-Event","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","root":"ns=6;s=MyLevel.Alarm","activatecustomevent":false,"eventtype":"i=2041","customeventtype":"","name":"MyLevel Alarms","x":500,"y":1860,"wires":[["c7438df35b506470"]]},{"id":"71e24b671bc03fb8","type":"inject","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Trigger Alarm Event Capture","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":1860,"wires":[["b76f64786bc681c3"]]},{"id":"c7438df35b506470","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","endpoint":"53f4394dbf12c6b7","action":"events","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Get MyLevel Events","x":720,"y":1860,"wires":[["c7e8919b636cb51d","5952b86dae22b056","04992b24a3836f19"],[]]},{"id":"c7e8919b636cb51d","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Text","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Message.text","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":1800,"wires":[["d662d662c5ccb9c1","1e3956200997581f"]]},{"id":"5952b86dae22b056","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Time","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Time","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":1860,"wires":[["0b8ac86e5e4f9f8d"]]},{"id":"04992b24a3836f19","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Severity","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Severity","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1920,"wires":[["62b2e14ce0429eef"]]},{"id":"325068cb935cd6d1","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Parse Event Dataset","info":"","x":990,"y":1760,"wires":[]},{"id":"5b4d1bd8b342fc05","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Get OPC Events from Client","info":"","x":720,"y":1820,"wires":[]},{"id":"ba1ea89438335cb8","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Display Events on Dashboard","info":"","x":1240,"y":1720,"wires":[]},{"id":"d662d662c5ccb9c1","type":"ui-notification","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","ui":"5355e0c476f9da3b","position":"center center","colorDefault":true,"color":"#000000","displayTime":"3","showCountdown":true,"outputs":1,"allowDismiss":true,"dismissText":"Close","raw":false,"className":"","name":"Event Notification","x":1230,"y":1800,"wires":[[]]},{"id":"1e3956200997581f","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1240,"y":1840,"wires":[]},{"id":"0b8ac86e5e4f9f8d","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event Timestamp","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1280,"y":1880,"wires":[]},{"id":"62b2e14ce0429eef","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event Severity","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1270,"y":1920,"wires":[]},{"id":"53f4394dbf12c6b7","type":"OpcUa-Endpoint","endpoint":"opc.tcp://192.168.56.1:53530/OPCUA/SimulationServer","secpol":"None","secmode":"None","none":true,"login":false,"usercert":false,"usercertificate":"","userprivatekey":""},{"id":"5355e0c476f9da3b","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"ec0ecb26fde8db3e","type":"ui-group","name":"MyDevice Status & Control","page":"44d3feb2a1143d7b","width":"3","height":"1","order":4,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"44d3feb2a1143d7b","type":"ui-page","name":"OPC UA","ui":"5355e0c476f9da3b","path":"/opcua","icon":"home","layout":"grid","theme":"61eee6fc60281b9b","order":1,"className":"","visible":"true","disabled":"false"},{"id":"61eee6fc60281b9b","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}] ``` :: We use an inject node to trigger the `OpcUa-Event` node. In the properties of the event node, we get the `NodeId` from the `MyLevelAlarm` event from the OPC Server - ![mylevel-event.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/mylevel-event.png){dataZoomable=""} And copy that `NodeId` into the `OpcUa-Event` node. Event type will be `BaseEvent (all)`. ![event-node-properties.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/event-node-properties.png){dataZoomable=""} In the `Opc-Ua-Client` node, we set the `Action` to `EVENTS`. ![client-events.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/client-events.png){dataZoomable=""} If we stick a debug node on the output of the client event, we can see how the OPC Server annunciates events. ![event-debug.png](https://flowfuse.com/blog/2023/07/images/opc-ua-3/event-debug.png){dataZoomable=""} Every time `MyLevel` exceeds certain thresholds (10%, 30%, 70% and 90%) it will flag a `Level Exceeded` alarm. The event is timestamped and assigned a severity level, which we will record and put onto the dashboard. To make things simple, we’ll only track the last event. But in a production system, you’d likely want to store these events in a relational database (historian) to keep an alarm history. We’ll also include a notification pop-up when an alarm occurs to notify someone monitoring the dashboard a new alarm has occurred. Adding alarms and events to our dashboard creates the following result - :video{ariaLabel="Video showing alarms and events displayed on the dashboard" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} ## Using FlowFuse to Enhance Your Node-RED Application: Security, Scalability, and Robustness So, you've successfully built your Node-RED application, congratulations! But now, how do you ensure its security, scalability, and ease of collaboration? What if you want to invite your team to work on the application simultaneously or access it remotely? Enter [FlowFuse](https://flowfuse.com), a cloud-based platform designed to add production-grade features to your Node-RED applications. With FlowFuse, you can seamlessly integrate advanced security measures, scale your application as needed, and collaborate effortlessly with your team. It simplifies management and deployment, turning your Node-RED project into a robust, scalable solution. If you're interested in learning how to use Node-RED for professional use cases, check out our eBook: [Ultimate Beginner's Guide to Professionals](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/). For additional resources, visit our [Node-RED Learning Resources section](https://flowfuse.com/docs/node-red/core-nodes/), where you can explore integrations with different protocols, messaging services, databases, hardware, and much more. ## Conclusion In this final article, we went over building a OPC UA Client dashboard that can browse the address space, read values from an OPC Server, write values to an OPC Server, and get events from an OPC Server. This flow provides examples that can serve as a foundation for an interactive OPC Client application built in Node-RED. This now concludes the OPC UA Series. For a production-grade OPC UA client, maintained as a certified node with subscriptions, alarms, and historical access built in, see [FlowFuse's OPC UA client and server capabilities](https://flowfuse.com/integrations/opcua/#opc-ua-client-and-server-capabilities). full source code for this project - ::render-flow ```json [{"id":"ca62be3e01388319","type":"group","z":"5b972161c4e0464e","name":"Browse Hierarchical Address Space Structure & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["6b17b2da2b942bb4","61797eccf2785257","4d92d940177b6ee3","68a113d5893b7c01","d0c969b6a59fac3a","639da01fc957e547","29437ca7222d9a64","49983d5da0958bf2","49040d0cf1144f0a","e7c55f412ef86543","de21b7ad98a05833","2d56e9a431c21a3b","ac95bd0e2b304eec","6fdabcc2950ccf4e","1c49fa5142d2cf17","335878527020598c","7b208f2e8cba6205","52dd2e5dcddad58f","a5acdccfd2033aec","157322c9c360446d","78a012e5db377fd9"],"x":94,"y":139,"w":1172,"h":422},{"id":"6b17b2da2b942bb4","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":280,"wires":[["4d92d940177b6ee3","d0c969b6a59fac3a","639da01fc957e547"]]},{"id":"61797eccf2785257","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get Base Folder Structure","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":280,"y":280,"wires":[["6b17b2da2b942bb4"]]},{"id":"4d92d940177b6ee3","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Simulation Folder","rules":[{"t":"set","p":"Objects.Simulation.nodeId","pt":"flow","to":"payload[2].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[2].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":220,"wires":[["335878527020598c"]]},{"id":"68a113d5893b7c01","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Display on Dashboard","info":"","x":1140,"y":180,"wires":[]},{"id":"d0c969b6a59fac3a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyObjects Folder","rules":[{"t":"set","p":"Objects.MyObjects.nodeId","pt":"flow","to":"payload[4].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":340,"wires":[["52dd2e5dcddad58f"]]},{"id":"639da01fc957e547","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticData Folder","rules":[{"t":"set","p":"Objects.StaticData.nodeId","pt":"flow","to":"payload[3].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[3].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":280,"wires":[["7b208f2e8cba6205"]]},{"id":"29437ca7222d9a64","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":440,"wires":[["49040d0cf1144f0a","e7c55f412ef86543"]]},{"id":"49983d5da0958bf2","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get StaticData Folder Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"","payloadType":"date","x":270,"y":440,"wires":[["29437ca7222d9a64"]]},{"id":"49040d0cf1144f0a","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"AnalogItemArrays Folder","rules":[{"t":"set","p":"Objects.StaticData.AnalogItemArrays.nodeId","pt":"flow","to":"payload[1].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[1].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":460,"wires":[["157322c9c360446d"]]},{"id":"e7c55f412ef86543","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"StaticArrayVariables Folder","rules":[{"t":"set","p":"Objects.StaticData.StaticArrayVariables.nodeId","pt":"flow","to":"payload[6].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[6].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":400,"wires":[["a5acdccfd2033aec"]]},{"id":"de21b7ad98a05833","type":"change","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"MyDevice Object","rules":[{"t":"set","p":"Objects.MyObjects.MyDevice.nodeId","pt":"flow","to":"payload[0].item.nodeId","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.browseName.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":520,"wires":[["78a012e5db377fd9"]]},{"id":"2d56e9a431c21a3b","type":"inject","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Get MyObjects Object Structure","props":[{"p":"payload"},{"p":"topic","v":"Objects.MyObjects.nodeId","vt":"flow"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.5","topic":"","payload":"","payloadType":"date","x":270,"y":520,"wires":[["ac95bd0e2b304eec"]]},{"id":"ac95bd0e2b304eec","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"ca62be3e01388319","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":550,"y":520,"wires":[["de21b7ad98a05833"]]},{"id":"6fdabcc2950ccf4e","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Store & Parse nodeId & browseName","info":"","x":850,"y":180,"wires":[]},{"id":"1c49fa5142d2cf17","type":"comment","z":"5b972161c4e0464e","g":"ca62be3e01388319","name":"Global Address Space Folder Browse","info":"","x":410,"y":220,"wires":[]},{"id":"335878527020598c","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"Simulation","order":1,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":220,"wires":[[]]},{"id":"7b208f2e8cba6205","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticData","order":2,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":280,"wires":[[]]},{"id":"52dd2e5dcddad58f","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyObjects","order":5,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1110,"y":340,"wires":[[]]},{"id":"a5acdccfd2033aec","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"StaticArrayVariables","order":3,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1140,"y":400,"wires":[[]]},{"id":"157322c9c360446d","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"AnalogItemArrays","order":4,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1130,"y":460,"wires":[[]]},{"id":"78a012e5db377fd9","type":"ui-template","z":"5b972161c4e0464e","g":"ca62be3e01388319","group":"ef9998baf5f61e8a","page":"","ui":"","name":"MyDevice","order":6,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1100,"y":520,"wires":[[]]},{"id":"53f4394dbf12c6b7","type":"OpcUa-Endpoint","endpoint":"opc.tcp://192.168.56.1:53530/OPCUA/SimulationServer","secpol":"None","secmode":"None","none":true,"login":false,"usercert":false,"usercertificate":"","userprivatekey":""},{"id":"ef9998baf5f61e8a","type":"ui-group","name":" Address Space Folder Structure","page":"44d3feb2a1143d7b","width":"2","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"44d3feb2a1143d7b","type":"ui-page","name":"OPC UA","ui":"5355e0c476f9da3b","path":"/opcua","icon":"home","layout":"grid","theme":"61eee6fc60281b9b","order":1,"className":"","visible":"true","disabled":"false"},{"id":"5355e0c476f9da3b","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"61eee6fc60281b9b","type":"ui-theme","name":"Default Theme","colors":{"surface":"#0094ce","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"8557072f05e4bda0","type":"group","z":"5b972161c4e0464e","name":"Read Simulation Values & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["9659d40ac9063764","9f5b597ec8179fb4","a8d919f497fcff04","13f5c98b7fd5f5da","ec5dca5eb9d4971b","1780cb86597d3c67","1a2fcac87247cda4","4d9b758e39555124","da468bc150517fa6","82aa12173dd7bbca","57d8777e34b55b7b","10877909d1daf6fe","c4d4a3b0df372e4c","b0cf511f824f2a86","f2efc6b419414c9a"],"x":94,"y":599,"w":1372,"h":302},{"id":"9659d40ac9063764","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"8557072f05e4bda0","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":570,"y":760,"wires":[["ec5dca5eb9d4971b"]]},{"id":"9f5b597ec8179fb4","type":"inject","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Update Simulation Values @ 1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.Simulation.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":760,"wires":[["9659d40ac9063764"]]},{"id":"a8d919f497fcff04","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Read Simulation Values","info":"","x":460,"y":720,"wires":[]},{"id":"13f5c98b7fd5f5da","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Counter Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[1].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1070,"y":680,"wires":[["10877909d1daf6fe"]]},{"id":"ec5dca5eb9d4971b","type":"switch","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":790,"y":760,"wires":[["13f5c98b7fd5f5da","1780cb86597d3c67","1a2fcac87247cda4","4d9b758e39555124"]]},{"id":"1780cb86597d3c67","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Random Number Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[2].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1100,"y":740,"wires":[["c4d4a3b0df372e4c"]]},{"id":"1a2fcac87247cda4","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Sawtooth Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[3].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1080,"y":800,"wires":[["b0cf511f824f2a86"]]},{"id":"4d9b758e39555124","type":"change","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Get Sawtooth Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1080,"y":860,"wires":[["f2efc6b419414c9a"]]},{"id":"da468bc150517fa6","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Discard Empty Datasets","info":"","x":780,"y":720,"wires":[]},{"id":"82aa12173dd7bbca","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Parse Simulation Values","info":"","x":1070,"y":640,"wires":[]},{"id":"57d8777e34b55b7b","type":"comment","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Display on Dashboard","info":"","x":1340,"y":640,"wires":[]},{"id":"10877909d1daf6fe","type":"ui-gauge","z":"5b972161c4e0464e","g":"8557072f05e4bda0","name":"Counter","group":"af263064820fb7d0","order":0,"width":3,"height":3,"gtype":"gauge-half","gstyle":"needle","title":"gauge","units":"units","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#5cd65c"},{"from":"15","color":"#ffc800"},{"from":"30","color":"#ea5353"}],"min":0,"max":"30","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":1320,"y":680,"wires":[]},{"id":"c4d4a3b0df372e4c","type":"ui-text","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","order":0,"width":0,"height":0,"name":"Random Number","label":"Random Number","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1350,"y":740,"wires":[]},{"id":"b0cf511f824f2a86","type":"ui-chart","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","name":"","label":"Sawtooth","order":9007199254740991,"chartType":"line","category":"Sawtooth","categoryType":"str","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisProperty":"","ymin":"","ymax":"","action":"append","pointShape":"line","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"60","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"3","height":"4","className":"","x":1320,"y":800,"wires":[[]]},{"id":"f2efc6b419414c9a","type":"ui-chart","z":"5b972161c4e0464e","g":"8557072f05e4bda0","group":"af263064820fb7d0","name":"","label":"Sinusoid","order":9007199254740991,"chartType":"line","category":"Sawtooth","categoryType":"str","xAxisProperty":"","xAxisPropertyType":"msg","xAxisType":"time","yAxisProperty":"","ymin":"","ymax":"","action":"append","pointShape":"line","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"60","removeOlderPoints":"","colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"width":"3","height":"4","className":"","x":1320,"y":860,"wires":[[]]},{"id":"af263064820fb7d0","type":"ui-group","name":"Simulation values","page":"44d3feb2a1143d7b","width":"3","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"5afdbddf71507886","type":"group","z":"5b972161c4e0464e","name":"Read StaticData Values & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["e998aa804042128b","6c9b7d4d195a1e9a","cd097b744d0ec625","18d21607c87ab153","7b5143c4960f92a1","0625b0cf6f546a4a","9d899fbb4d1648b3","6e1edc31687dde54","051e1f282076fed2","de2a1c3e380f743b","c74606c48ccf5a40","053bda13f2a2eabe","277dcf430dc86996","d708e6264cec0070"],"x":84,"y":939,"w":1382,"h":202},{"id":"e998aa804042128b","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"5afdbddf71507886","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":630,"y":1020,"wires":[["7b5143c4960f92a1"]]},{"id":"6c9b7d4d195a1e9a","type":"inject","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Update AnalogItemArrays Values @ 1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.AnalogItemArrays.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":320,"y":1020,"wires":[["e998aa804042128b"]]},{"id":"cd097b744d0ec625","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Read StaticData Values","info":"","x":520,"y":980,"wires":[]},{"id":"18d21607c87ab153","type":"change","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Get ByteAnalogItemArray Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"$string(payload[0].item.value)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1070,"y":1020,"wires":[["277dcf430dc86996"]]},{"id":"7b5143c4960f92a1","type":"switch","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":830,"y":1020,"wires":[["18d21607c87ab153"]]},{"id":"0625b0cf6f546a4a","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"5afdbddf71507886","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":630,"y":1100,"wires":[["051e1f282076fed2"]]},{"id":"9d899fbb4d1648b3","type":"inject","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Update StaticArrayVariables Values @1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.StaticData.StaticArrayVariables.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":320,"y":1100,"wires":[["0625b0cf6f546a4a"]]},{"id":"6e1edc31687dde54","type":"change","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Get BooleanArray Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":1100,"wires":[["d708e6264cec0070"]]},{"id":"051e1f282076fed2","type":"switch","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":830,"y":1100,"wires":[["6e1edc31687dde54"]]},{"id":"de2a1c3e380f743b","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Discard Empty Datasets","info":"","x":820,"y":980,"wires":[]},{"id":"c74606c48ccf5a40","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Parse StaticData Values","info":"","x":1070,"y":980,"wires":[]},{"id":"053bda13f2a2eabe","type":"comment","z":"5b972161c4e0464e","g":"5afdbddf71507886","name":"Display on Dashboard","info":"","x":1340,"y":980,"wires":[]},{"id":"277dcf430dc86996","type":"ui-text","z":"5b972161c4e0464e","g":"5afdbddf71507886","group":"3d4f386e812e8b5f","order":0,"width":0,"height":0,"name":"","label":"ByteAnalogItemArray","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1340,"y":1020,"wires":[]},{"id":"d708e6264cec0070","type":"ui-text","z":"5b972161c4e0464e","g":"5afdbddf71507886","group":"3d4f386e812e8b5f","order":0,"width":0,"height":0,"name":"","label":"BooleanArray","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1320,"y":1100,"wires":[]},{"id":"3d4f386e812e8b5f","type":"ui-group","name":"StaticData Values","page":"44d3feb2a1143d7b","width":"4","height":"1","order":3,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"25f95391088d4a08","type":"group","z":"5b972161c4e0464e","name":"Read MyDevice Values & Display on Dashboard","style":{"label":true,"color":"#000000"},"nodes":["bfe4274963a84e2e","0662c62c7f0cfac0","249c223139c5779e","efb293f13af17dc1","507d2c11c7586957","3b7695a52a1bf6e0","594ec38acadc9673","d4e2915ba92db8a6","85619f0eab615ac7","cd4941a8db3edcb6","ad91d2ca81697fc2"],"x":94,"y":1179,"w":1292,"h":182},{"id":"bfe4274963a84e2e","type":"OpcUa-Browser","z":"5b972161c4e0464e","g":"25f95391088d4a08","endpoint":"53f4394dbf12c6b7","item":"","datatype":"","topic":"","items":[],"name":"OPC Client Namespace Browse","x":590,"y":1280,"wires":[["507d2c11c7586957"]]},{"id":"0662c62c7f0cfac0","type":"inject","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Read MyDevice Values @ 1 second","props":[{"p":"payload"},{"p":"topic","v":"Objects.MyObjects.MyDevice.nodeId","vt":"flow"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":290,"y":1280,"wires":[["bfe4274963a84e2e"]]},{"id":"249c223139c5779e","type":"comment","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Read MyDevice","info":"","x":480,"y":1240,"wires":[]},{"id":"efb293f13af17dc1","type":"change","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Get MyLevel Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1030,"y":1260,"wires":[["cd4941a8db3edcb6"]]},{"id":"507d2c11c7586957","type":"switch","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"empty check","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":810,"y":1280,"wires":[["efb293f13af17dc1","3b7695a52a1bf6e0"]]},{"id":"3b7695a52a1bf6e0","type":"change","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Get MySwitch Value","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[4].item.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1040,"y":1320,"wires":[["ad91d2ca81697fc2"]]},{"id":"594ec38acadc9673","type":"comment","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Display on Dashboard","info":"","x":1260,"y":1220,"wires":[]},{"id":"d4e2915ba92db8a6","type":"comment","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Parse MyDevice Values","info":"","x":1040,"y":1220,"wires":[]},{"id":"85619f0eab615ac7","type":"comment","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Discard Empty Datasets","info":"","x":800,"y":1240,"wires":[]},{"id":"cd4941a8db3edcb6","type":"ui-gauge","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"Level","group":"ec0ecb26fde8db3e","order":0,"width":3,"height":3,"gtype":"gauge-half","gstyle":"needle","title":"Level","units":"%","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#0094ce"},{"from":"25","color":"#0094ce"},{"from":"50","color":"#0094ce"},{"from":"100","color":"#0094ce"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":1250,"y":1260,"wires":[]},{"id":"ad91d2ca81697fc2","type":"ui-led","z":"5b972161c4e0464e","g":"25f95391088d4a08","name":"","group":"ec0ecb26fde8db3e","order":-1,"width":0,"height":0,"label":"Switch","labelPlacement":"left","labelAlignment":"flex-start","states":[{"value":"false","valueType":"bool","color":"#ff0000"},{"value":"true","valueType":"bool","color":"#00ff00"}],"allowColorForValueInMessage":false,"shape":"circle","showBorder":true,"showGlow":true,"x":1250,"y":1320,"wires":[]},{"id":"ec0ecb26fde8db3e","type":"ui-group","name":"MyDevice Status & Control","page":"44d3feb2a1143d7b","width":"3","height":"1","order":4,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"3de6c861611c3afa","type":"group","z":"5b972161c4e0464e","name":"Write Mydevices values to OPC UA Server","style":{"label":true,"color":"#000000"},"nodes":["a66583d91b581cd8","3e8cb6e199012155","9fa33d1c9c621611","fb7f57b4da5883ae","9c5ff104eb9c8b10","77bcb828bec95336","afa83dbb46449d4a","fa08f0ed04296363","9f591797b56c565d"],"x":94,"y":1439,"w":792,"h":182},{"id":"a66583d91b581cd8","type":"OpcUa-Item","z":"5b972161c4e0464e","g":"3de6c861611c3afa","item":"ns=6;s=MySwitch","datatype":"Boolean","value":"","name":"Toggle MySwitch","x":470,"y":1520,"wires":[["3e8cb6e199012155"]]},{"id":"3e8cb6e199012155","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"3de6c861611c3afa","endpoint":"53f4394dbf12c6b7","action":"write","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Write MySwitch","x":720,"y":1520,"wires":[[],[]]},{"id":"9fa33d1c9c621611","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Dashboard Input","info":"","x":200,"y":1480,"wires":[]},{"id":"fb7f57b4da5883ae","type":"OpcUa-Item","z":"5b972161c4e0464e","g":"3de6c861611c3afa","item":"ns=6;s=MyLevel","datatype":"Double","value":"","name":"Modify MyLevel","x":460,"y":1580,"wires":[["9c5ff104eb9c8b10"]]},{"id":"9c5ff104eb9c8b10","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"3de6c861611c3afa","endpoint":"53f4394dbf12c6b7","action":"write","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Write MyLevel","x":720,"y":1580,"wires":[[],[]]},{"id":"77bcb828bec95336","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Call OPC UA Item","info":"","x":470,"y":1480,"wires":[]},{"id":"afa83dbb46449d4a","type":"comment","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"Write OPC UA Item to Client","info":"","x":740,"y":1480,"wires":[]},{"id":"fa08f0ed04296363","type":"ui-switch","z":"5b972161c4e0464e","g":"3de6c861611c3afa","name":"","label":"Toggle MySwitch","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"passthru":false,"topic":"topic","topicType":"msg","style":"","className":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":210,"y":1520,"wires":[["a66583d91b581cd8"]]},{"id":"9f591797b56c565d","type":"ui-slider","z":"5b972161c4e0464e","g":"3de6c861611c3afa","group":"ec0ecb26fde8db3e","name":"","label":"Modify MyLevel","tooltip":"","order":0,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":true,"min":"0","max":"100","step":1,"className":"","x":200,"y":1580,"wires":[["fb7f57b4da5883ae"]]},{"id":"a6e9abacd0bdf3b6","type":"group","z":"5b972161c4e0464e","name":"Read Alarms & Events From OPC UA Server","style":{"label":true,"color":"#000000"},"nodes":["90fb4ca64a642edf","b76f64786bc681c3","71e24b671bc03fb8","c7438df35b506470","c7e8919b636cb51d","5952b86dae22b056","04992b24a3836f19","325068cb935cd6d1","5b4d1bd8b342fc05","ba1ea89438335cb8","d662d662c5ccb9c1","1e3956200997581f","0b8ac86e5e4f9f8d","62b2e14ce0429eef"],"x":94,"y":1679,"w":1352,"h":282},{"id":"90fb4ca64a642edf","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Call OPC UA Item","info":"","x":470,"y":1820,"wires":[]},{"id":"b76f64786bc681c3","type":"OpcUa-Event","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","root":"ns=6;s=MyLevel.Alarm","activatecustomevent":false,"eventtype":"i=2041","customeventtype":"","name":"MyLevel Alarms","x":500,"y":1860,"wires":[["c7438df35b506470"]]},{"id":"71e24b671bc03fb8","type":"inject","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Trigger Alarm Event Capture","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":1860,"wires":[["b76f64786bc681c3"]]},{"id":"c7438df35b506470","type":"OpcUa-Client","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","endpoint":"53f4394dbf12c6b7","action":"events","deadbandtype":"a","deadbandvalue":1,"time":10,"timeUnit":"s","certificate":"n","localfile":"","localkeyfile":"","securitymode":"None","securitypolicy":"None","useTransport":false,"maxChunkCount":1,"maxMessageSize":8192,"receiveBufferSize":8192,"sendBufferSize":8192,"name":"Get MyLevel Events","x":720,"y":1860,"wires":[["c7e8919b636cb51d","5952b86dae22b056","04992b24a3836f19"],[]]},{"id":"c7e8919b636cb51d","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Text","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Message.text","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":1800,"wires":[["d662d662c5ccb9c1","1e3956200997581f"]]},{"id":"5952b86dae22b056","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Time","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Time","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":1860,"wires":[["0b8ac86e5e4f9f8d"]]},{"id":"04992b24a3836f19","type":"change","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Event Severity","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.Severity","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1920,"wires":[["62b2e14ce0429eef"]]},{"id":"325068cb935cd6d1","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Parse Event Dataset","info":"","x":990,"y":1760,"wires":[]},{"id":"5b4d1bd8b342fc05","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Get OPC Events from Client","info":"","x":720,"y":1820,"wires":[]},{"id":"ba1ea89438335cb8","type":"comment","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","name":"Display Events on Dashboard","info":"","x":1240,"y":1720,"wires":[]},{"id":"d662d662c5ccb9c1","type":"ui-notification","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","ui":"5355e0c476f9da3b","position":"center center","colorDefault":true,"color":"#000000","displayTime":"3","showCountdown":true,"outputs":1,"allowDismiss":true,"dismissText":"Close","raw":false,"className":"","name":"Event Notification","x":1230,"y":1800,"wires":[[]]},{"id":"1e3956200997581f","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1240,"y":1840,"wires":[]},{"id":"0b8ac86e5e4f9f8d","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event Timestamp","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1280,"y":1880,"wires":[]},{"id":"62b2e14ce0429eef","type":"ui-text","z":"5b972161c4e0464e","g":"a6e9abacd0bdf3b6","group":"ec0ecb26fde8db3e","order":0,"width":0,"height":0,"name":"","label":"Latest MyLevel Event Severity","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":1270,"y":1920,"wires":[]}] ``` :: # How to Deploy a Basic OPC-UA Server in Node-RED - Part 1 (2026) This article is the first part of a series of OPC-UA content. Here, we will explain some basic concepts of OPC-UA as they apply to building a server in Node-RED, then walk through and deploy an example OPC-UA Server. ## What is OPC-UA? Open Platform Communications Unified Architecture (OPC UA) is an open, platform independent communication framework frequently utilized in industrial automation, and is considered one of the key protocol standards for Industry 4.0 and Industrial IoT (IIoT). The standard is developed and maintained by a consortium called the OPC Foundation, with recognizable industry names such as Siemens, Honeywell, Microsoft, Beckhoff, SAP, Yokogawa, ABB, Rockwell, and Schneider Electric. Because of OPC-UA’s wide industry acceptance, it is increasingly becoming natively supported on devices and systems spanning the entirety of the automation pyramid. !["Automation Pyramid"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/automation-pyramid.jpg "Automation Pyramid")*Image reference - [imagecontroltips.com](https://www.motioncontroltips.com/what-is-opc-ua-and-how-does-it-compare-with-industrial-ethernet/){rel=""nofollow""}* ## Fieldbus Model vs OPC-UA Information Model As of today, industrial ethernet fieldbuses dominate the field/device-level (level 0) and controller/PLC-level (level 1) of the automation pyramid. !["OPC-UA Pyramid"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/OPC-UA-pyramid-2.webp "OPC-UA Pyramid")*Image reference - [mdpi.com](https://www.mdpi.com/1424-8220/21/14/4656){rel=""nofollow""}* Fieldbuses such as Profinet, Ethernet/IP, and EtherCAT, employ deterministic, real-time communication, which is essential for mission-critical and safety-oriented automation tasks. OPC-UA is most commonly encountered at the SCADA level and above (level 2-4). However, with the inclusion of [Time Sensitive Networking (TSN) into the OPC-UA technology stack](https://www.tttech-industrial.com/resource-library/blog-posts/opc-ua-fx){rel=""nofollow""}, OPC-UA can be feasibly used for real-time communication all the way down to the device level. Traditionally, fieldbus protocols transmit only raw data from field devices (ie, a float to represent a pressure, or a boolean to represent the position of a switch). The fieldbus data gets pushed up the automation stack layer by layer, where eventually it will be converted to a format suitable for IT systems to consume (such as OPC-UA). !["Fieldbus Model"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/fieldbus-model.png "Fieldbus Model") In contrast to fieldbus protocols, OPC-UA represents automation data in the form of nodes. The framework for constructing nodes is referred to as the [OPC Information model](https://reference.opcfoundation.org/Core/Part5/v104/docs/){rel=""nofollow""}, and consists of pre-defined classes and methods that are programmed in the OPC Server address space. !["OPC Information Model"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-information-model.png "OPC Information Model") Devices can be described as objects that give a holistic view of the device, beyond simply the raw value. To construct a device object, we can take different individual attributes associated with a device, such as the transmitter raw value, transmitter fault flag, alarm setpoint, and combine them, similar to how user-defined datatypes (UDTs) are objects used to represent devices in PLCs. The information model also defines a folder structure, to allow devices information to reside in a structured hierarchy. Using the example temperature transmitter above, an example folder structure can be constructed as follows: `/Root/Objects/Calcinator 1 PLC/Temperature Transmitters/Tank 1 Temperature/Transmitter Value` This folder structure will be exposed via the OPC Client browser, allowing end-users to easily “drill down” to individual node information in a logical manner. !["OPC Client Browser"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-client-browser.png "OPC Information Model") In summary, OPC-UA represents a trade-off between complex information modeling, with the versatility for that data to be consumed by devices and systems all the way up the automation pyramid layers. The data does not have to pass through subsequent automation layers on the way up, nor does the data need to undergo any conversion along the way. !["OPC-UA Distributed Model"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/OPC-UA-distributed-model.jpg "OPC-UA Distributed Model")*Image reference - [ifr.org](https://ifr.org/post/faster-robot-communication-through-the-opc-robotics-companion-specification){rel=""nofollow""}* The OPC client simply needs to subscribe to the OPC Server endpoint url (ex. opc.tcp\://server.address), and the client will be able to browse the structured OPC data as it’s modeled in the server. Any client will receive the information in the same manner, regardless if it’s a PLC, SCADA, MES, or ERP system. This opens the possibility for horizontal and vertical system integration in a standardized manner. Additionally, the more information that is exposed about a device, the easier it is to track, and use said data to autonomously reconfigure, or pre-emptively take maintenance actions. ## Deploying an Example OPC-UA Server in Node-RED With some background on OPC-UA and how information is modeled in mind, we can take a look at the [node-red-contrib-opcua-server](https://flows.nodered.org/node/node-red-contrib-opcua-server){rel=""nofollow""} node, which is merely a compact version of the [node-red-contrib-opcua](https://flows.nodered.org/node/node-red-contrib-opcua){rel=""nofollow""} node that only focuses on the OPC-UA server and hence requires less dependencies. :cta-image{alt="Aperia Technologies stopped reprogramming controllers station by station with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/aperia-book-demo.png"} An [example flow](https://github.com/BiancoRoyal/node-red-contrib-opcua-server/blob/master/examples/server-with-context.json){rel=""nofollow""} is provided on github that can serve as a basis for understanding how a OPC-UA server is constructed. Let’s get the example server up and running. Deploying the example flow yields the following result - ![Compact Server Flow](https://flowfuse.com/blog/2023/07/images/opc-ua-1/compact-server-flow.png "Compact Server Flow") - an inject node is trigging the function `set flow context Inputs` at a one second interval, which creates 7 randomly generated float values and stores them as flow context variables, `isoInput2` - `isoInput8` (isolated inputs). The values will change to a new random number each time the node is injected. ```javascript flow.set('isoInput2', Math.random() + 12.0) flow.set('isoInput3', Math.random() + 13.0) flow.set('isoInput4', Math.random() + 14.0) flow.set('isoInput5', Math.random() + 15.0) flow.set('isoInput6', Math.random() + 16.0) flow.set('isoInput7', Math.random() + 17.0) flow.set('isoInput8', Math.random() + 18.0) ... ``` - another inject node is triggering the function `set flow context Outputs`, also at a one second interval, which creates another set of 7 randomly generated float values and stores them as flow context variables, `isoOutput2` - `isoOutput8` (isolated inputs). The values will change to a new random number each time the node is injected. ```javascript flow.set('isoOutput2', Math.random() + 2.0) flow.set('isoOutput3', Math.random() + 3.0) flow.set('isoOutput4', Math.random() + 4.0) flow.set('isoOutput5', Math.random() + 5.0) flow.set('isoOutput6', Math.random() + 6.0) flow.set('isoOutput7', Math.random() + 7.0) flow.set('isoOutput8', Math.random() + 8.0) ... ``` We can confirm the values are being stored in memory by checking the flow context data and pressing the refresh button. !["Screenshot showing the Context Data option"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/context-data-1.png "Screenshot showing the Context Data option") ![Screenshot showing the flow variables in the context data tab](https://flowfuse.com/blog/2023/07/images/opc-ua-1/context-data-2.png "Screenshot showing the flow variables in the context data tab") Each time we hit refresh, the values change, confirming that the values are randomly changing every second. The last, and most important part of the flow, is the `Compact-Server` node, which actually stands alone without any incoming or outgoing connections. !["Compact Server Node"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/compact-server-node.png "Compact Server Node") In the `Compact-Server` node properties, the first tab is `Settings`, and the two important properties here are `Port` and `Show Errors`. As can be seen in the node screenshot above, the node is reporting `active`, which means the server is configured correctly. ![Screenshot showing the Settings Tab of compact server node](https://flowfuse.com/blog/2023/07/images/opc-ua-1/settings-tab.png "Screenshot showing the Settings Tab of compact server node") The `Limits` tab specifies some default limits that we can configure if we like, but are not necessary to be modified for test purposes. The `Security` tab has one important option, `Allow Anonymous`. By default, anonymous access is enabled. ![Screenshot showing the Security Tab of compact server node](https://flowfuse.com/blog/2023/07/images/opc-ua-1/security-tab.png "Screenshot showing the Security Tab of compact server node") For a production system, we will want to enable security, but for test purposes, we will leave anonymous access enabled. `Users & Sets` tab is related to security and permissions. We can leave this empty for testing. The `Address Space` tab is where our server OPC Information Model is constructed, using classes and methods from the [node-opcua sdk](https://node-opcua.github.io/){rel=""nofollow""}. Breaking down the provided example code for further context, it starts with a function that is responsible for invoking the OPC-UA server, ```javascript const opcua = coreServer.choreCompact.opcua; ``` and then the namespace is created. ```javascript const namespace = addressSpace.getOwnNamespace(); ``` Further down, the variables that will be published by the server (which are our `isoInput` & `isoOutput` flow context variables) are initialized, ```javascript this.sandboxFlowContext.set("isoInput1", 0); this.setInterval(() => { flexServerInternals.sandboxFlowContext.set( "isoInput1", Math.random() + 50.0 ); }, 500); this.sandboxFlowContext.set("isoInput2", 0); this.sandboxFlowContext.set("isoInput3", 0); ... ``` and an OPC folder structure is defined. ```javascript coreServer.debugLog("init dynamic address space"); const rootFolder = addressSpace.findNode("RootFolder"); node.warn("construct new address space for OPC UA"); const myDevice = namespace.addFolder(rootFolder.objects, { "browseName": "RaspberryPI-Zero-WLAN" }); ... ``` Then, with our variables and folder structure defined, nodes are added to the namespace for each context variable. ```javascript const gpioDI1 = namespace.addVariable({ "organizedBy": isoInputs, "browseName": "I1", "nodeId": "ns=1;s=Isolated_Input1", "dataType": "Double", "value": { "get": function() { return new Variant({ "dataType": DataType.Double, "value": flexServerInternals.sandboxFlowContext.get("isoInput1") }); }, "set": function(variant) { flexServerInternals.sandboxFlowContext.set( "isoInput1", parseFloat(variant.value) ); return opcua.StatusCodes.Good; } } }); ... ``` Last, OPC views are defined. Views create custom hierarchies our OPC Client can browse as an alternative to the default folder structure. ```javascript const viewDI = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "RPIW0-Digital-Ins" }); const viewDO = namespace.addView({ "organizedBy": rootFolder.views, "browseName": "RPIW0-Digital-Outs" }); viewDI.addReference({ "referenceType": "Organizes", "nodeId": gpioDI1.nodeId }); ... ``` Finally, on the `Discovery` tab, we must define an endpoint for an OPC Client to subscribe to. The `Endpoint Url` follows the format `opc.tcp://
:port`. Our port was defined on the `Settings` tab, which by default, is port `54845`. The address will be either the url or ip address of your Node-RED instance. In my case, it’s 192.168.0.114. So my Endpoint Url = `opc.tcp://192.168.0.114:54845` ![Screenshot showing the Discovery Tab of compact server node](https://flowfuse.com/blog/2023/07/images/opc-ua-1/discovery-tab.png "Screenshot showing the Discovery Tab of compact server node") Once the endpoint url is added, deploy the flow, and confirm the server is reporting “active”. ![Screenshot showing the Active Tab of compact server node](https://flowfuse.com/blog/2023/07/images/opc-ua-1/compact-server-active.png "Screenshot showing the Active Tab of compact server node") ## Connect to Example OPC-Server Using OPC-UA Browser To connect to our OPC endpoint, we need an OPC Client. Prosys provides a [free OPC-UA Browser ](https://www.prosysopc.com/products/opc-ua-browser/){rel=""nofollow""}that supports Windows, Linux, and Mac OS. To test our Server, the Windows version of Prosys OPC-UA Browser will be utilized. To connect to our Node-RED OPC server, enter the endpoint url and press “connect to server”. !["Screenshot showing the OPC Client"](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-client-connect.png "Screenshot showing the OPC Client") It will ask for security. Remember that we allowed anonymous access, so the default security mode of `None` is the correct option. Once connected, we can browse our OPC Server. ![OPC Client UI](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-client-ui.png) If we navigate to `Objects → RaspberryPI-Zero-WLAN → GPIO → Inputs`, we can see a list of inputs that correspond to the `isoInput` context variables defined in the example flow, which are randomly generated numbers. Clicking `I1` we can see the value in real-time, along with some additional properties. ![OPC Client Node](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-client-node.png "OPC Client Node") If we go to `Views`, we can see the custom hierarchy defined in the example server, which divides the data by Digital-Ins and Digital-Outs. ![OPC Client View](https://flowfuse.com/blog/2023/07/images/opc-ua-1/opc-client-view.png) ## Summary In this article, we compare OPC-UA to traditional fieldbus protocols, explain the importance of the OPC UA Information Model to understand how data is modeled in the address space of an OPC Server, and then walk through and deploy an example compact OPC-UA Server flow. This isn't just a lab exercise: [Opto 22, one of the original vendors behind the OPC standard, ships Node-RED pre-installed on its groov EPIC edge controllers](https://flowfuse.com/customer-stories/opto22-embraces-node-red/), putting this same OPC-UA server pattern into production on the factory floor. In our next article, we will build a custom OPC-UA Server in Node-RED with data pulled from an Allen Bradley PLC over Ethernet/IP, using the PLC data to develop a custom OPC UA Information Model programmed in the OPC server address space. For FlowFuse's production-ready OPC UA server, maintained as a certified node rather than a hand-rolled flow, see [hosting an OPC UA server](https://flowfuse.com/integrations/opcua/#opc-ua-client-and-server-capabilities) on the FlowFuse OPC UA overview. # How to add images to Node-RED dashboards when using FlowFuse (2026) Using images in your Node-RED dashboards can significantly improve your users' experience. The most common method to add images to dashboards is to store them within the filesystem of an Node-RED instance but sometimes that's not an option. How can you easily use images when working in a containerized environment such as Docker, or Kubernetes? We will also explore latest feature from FlowFuse that makes this step super easy. When designing a dashboard, images allow you to significantly enrich your content. Some examples include: - displaying maps to guide engineers to a problem which needs resolving. - displaying pictures of specific hardware on a factory-floor which needs to be checked. - displaying physical tools which should be used to resolve a problem. ### Prerequisites Before we begin, ensure you have the following custom nodes installed: - [@flowfuse/node-red-dashboard](https://flows.nodered.org/node/@flowfuse/node-red-dashboard){rel=""nofollow""} - A set of dashboard nodes for Node-RED. We will use this dashboard to demonstrate how to quickly display images using static assets. If you're a beginner and want to dive deeper, refer to [Getting started with FlowFuse Dashboarad](https://flowfuse.com/blog/2024/03/dashboard-getting-started/). - [node-red-contrib-string](https://flows.nodered.org/node/node-red-contrib-string){rel=""nofollow""} - A string manipulation node based on the lightweight stringjs library. - [node-red-node-base64](https://flows.nodered.org/node/node-red-node-base64){rel=""nofollow""} - A Node-RED node to encode and decode data to and from base64. ## Easily Add Images to Node-RED Dashboards with FlowFuse’s Static Asset Service [FlowFuse's static assets](https://flowforge.com/docs/user/static-asset-service/){rel=""nofollow""} service provides a simple way to manage images and other assets in Node-RED. Follow these steps to quickly add images to your Node-RED dashboard. :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} ### Steps to Add Images Using the Static Asset Service: ##### 1. Access the Static Assets Service - Log into your FlowFuse account, navigate to your **Node-RED instance**, and click on the **Static Assets Service** tab. ##### 2. Create a New Folder (Optional) - Click the **New Folder** button to create a folder that will help you organize your assets. Provide a folder name and **confirm** the creation. ##### 3. Upload Your Image - Enter the folder (or skip this step if not using folders), click **Upload**, select your image file, and **confirm**. ##### 4. Copy the Image Path - Once uploaded, click the **copy icon** next to the image to get the path for use in Node-RED. ##### 5. Set Up the Image Flow in Node-RED - Open the Node-RED editor for the relevant instance. - Drag an `inject` node onto the canvas and set it to trigger immediately when the flow is deployed. - Drag a `read file` node, paste the copied file path in the **Filename** field, and set the output to Single buffer object. ##### 6. Prepare the Image for Display - Add a `string` node to convert the buffer to a base64 string. Set **From** as `msg.filename` and adjust the **Method** to `getmost`. - Add a `change` node and configure it to add the elements shown in the following image: :br![The change node showing added elements](https://flowfuse.com/blog/2023/07/images/change-node.png "The change node showing added elements") ##### 7. Display the Image in the Dashboard - Drag a `ui-template` node onto the canvas. - Add the code in `ui-template` with an `` tag, configuring the `src` attribute with `msg.payload`, as shown in the following code. Alternatively, you can use the following code directly if you want to display the image in the top-left corner of your dashboard header: ```javascript ``` ##### 8. Connect the Nodes - Finally connect the nodes in the following order: the **output** of the `inject` node to the **input** of the `read file` node, then link to the `string node`, followed by the `change` node, and finally to the `ui-template` node. :br`inject → read file → string → change → ui-template` ::render-flow ```json [{"id":"e50f7c57189d62f8","type":"group","z":"d4aa6dd5b63a56de","style":{"stroke":"#b2b3bd","stroke-opacity":"1","fill":"#f2f3fb","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#32333b"},"nodes":["3bf4c71150bd0524","4d3ca1b96c181645","315948cc6a2cc9e8","fcf061d4cc732e2e","6563d8715af4cb06","d40caa36040de881"],"x":514,"y":699,"w":1752,"h":82},{"id":"3bf4c71150bd0524","type":"ui-template","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","group":"","page":"","ui":"25f447d87d1ce5c9","name":"Display image","order":0,"width":0,"height":0,"head":"","format":"\n\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"widget:ui","className":"","x":2160,"y":740,"wires":[[]]},{"id":"4d3ca1b96c181645","type":"inject","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":630,"y":740,"wires":[["315948cc6a2cc9e8"]]},{"id":"315948cc6a2cc9e8","type":"file in","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","name":"","filename":"Images/ff-logo--wordmark--light.png","filenameType":"str","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":880,"y":740,"wires":[["d40caa36040de881"]]},{"id":"fcf061d4cc732e2e","type":"change","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","name":"Add the file type to the mimetype, add to image content","rules":[{"t":"set","p":"mimetype","pt":"msg","to":"\"data:image/\"&msg.filetype&\";base64,\"","tot":"jsonata"},{"t":"set","p":"output","pt":"msg","to":"msg.mimetype&msg.payload","tot":"jsonata"},{"t":"move","p":"output","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1820,"y":740,"wires":[["3bf4c71150bd0524"]]},{"id":"6563d8715af4cb06","type":"string","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","name":"Get file type from file name","methods":[{"name":"getRightMost","params":[{"type":"str","value":"."}]}],"prop":"filename","propout":"filetype","object":"msg","objectout":"msg","x":1480,"y":740,"wires":[["fcf061d4cc732e2e"]]},{"id":"d40caa36040de881","type":"base64","z":"d4aa6dd5b63a56de","g":"e50f7c57189d62f8","name":"Convert Buffer to Base 64 String","action":"","property":"payload","x":1210,"y":740,"wires":[["6563d8715af4cb06"]]},{"id":"25f447d87d1ce5c9","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-control","ui-notification"],"showPathInSidebar":false,"showPageTitle":false,"titleBarStyle":"default"}] ``` :: Using the FlowFuse Static Assets service is highly beneficial when you want to display images in Node-RED dashboards, as it saves time compared to alternative solutions. However, it’s important to note that moving Node-RED instances through a DevOps pipeline currently does not support handling static assets. This feature is expected in future updates. If you want to manage images effectively within your Node-RED dashboards, consider the alternative solutions discussed in this blog, ensuring that the movement of instances does not affect the usage of these assets. ## Why not just store them in Node-RED's host operating system? Storing images locally can work well when you can access and edit the images on an operating system, but that approach doesn't scale if you are moving instances through a DevOps pipeline. It can also not work well when deploying to environments where you don't have easy access to the host operating system. How can we include images in dashboards, and be confident that a given build of an application will show the correct images, no matter where your Node-RED instances are hosted? ## Inspiration There are various solutions to this problem, I wanted to share one I came across when working with a FlowFuse customer recently. I've modified the flows to make them more general in design, but the underlying principal is the same. I asked if I was OK to credit the customer but they said there was no need. Thanks for the inspiration, kind customer! ## Solution explanation There are three key sections to this solution: 1. Pull the images we need from URLs 2. Store those images in the temporary filesystem of Node-RED 3. Serve up those images as needed in the dashboard It is possible for us to skip step 2, but I wanted to have the images stored locally, in the Node-RED instance. storing the images locally will improve the loading times of the dashboard. This is especially beneficial when your dashboard is dynamically displaying relevant images, e.g. to show an image of a specific machine which needs to be attended to. The key benefit of pulling the images from URLs this way is, no matter where you are running Node-RED, the correct images will be shown in your dashboard. ## Prequsite Before moving forward, ensure you have the following nodes installed, as the flows shared later will require them: - [node-red-contrib-calc](https://flows.nodered.org/node/node-red-contrib-calc){rel=""nofollow""} - A Node-RED node to perform basic mathematical calculations. - [node-red-contrib-image-output](https://flows.nodered.org/node/node-red-contrib-image-output){rel=""nofollow""} - A simple way to preview and examine images in your flows. - [node-red-contrib-os](https://flows.nodered.org/node/node-red-contrib-os){rel=""nofollow""} - Nodes for obtaining system information like CPU usage. - [node-red-contrib-string](https://flows.nodered.org/node/node-red-contrib-string){rel=""nofollow""} - A string manipulation node based on the lightweight stringjs library. - [@flowfuse/node-red-dashboard](https://flows.nodered.org/node/@flowfuse/node-red-dashboard){rel=""nofollow""} - A set of dashboard nodes for Node-RED. - [node-red-node-base64](https://flows.nodered.org/node/node-red-node-base64){rel=""nofollow""} - A Node-RED node to encode and decode data to and from base64. ## File and file-in nodes I've included the flows as json below so you can try them out yourself. Please note, I'm using FlowFuse's own [file and file-in nodes](https://flowfuse.com/docs/user/filenodes/) in these examples. If you want to use these flows on hosting other than FlowFuse, you will need to replace the nodes with the standard Node-RED file and file-in nodes. ## The flows The first flow takes image URLs in an array, each image is downloaded, processed, then saved to the local file storage. Let's take a look at the flow: ::render-flow ```json [{"id":"6b8059f703d0f574","type":"group","z":"c6f2a894be05d857","name":"Write the images to disk from the URLs","style":{"label":true},"nodes":["04fb6911559797a0","8a3c077f0f85a905","22c5026dd58e418b","6fcca5cfee2bcb89"],"x":38,"y":53,"w":1004,"h":434},{"id":"04fb6911559797a0","type":"group","z":"c6f2a894be05d857","g":"6b8059f703d0f574","name":"Inject the image URLs to download","style":{"label":true},"nodes":["e635ceb0577a86d5","29fe40a054be5b2b","1e81a35c27aae6ad"],"x":74,"y":79,"w":502,"h":82},{"id":"e635ceb0577a86d5","type":"inject","z":"c6f2a894be05d857","g":"04fb6911559797a0","name":"Send in image URLs as an array","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[\"https://openjsf.org/wp-content/uploads/sites/84/2023/02/ff-logo-wordmark-light_4x.png\",\"https://nodered.org/images/nr-image-1.png\",\"/img/screen-pseudo-overview-2QvTVle3Mr-384.avif\"]","payloadType":"json","x":250,"y":120,"wires":[["29fe40a054be5b2b"]]},{"id":"29fe40a054be5b2b","type":"split","z":"c6f2a894be05d857","g":"04fb6911559797a0","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":450,"y":120,"wires":[["1e81a35c27aae6ad"]]},{"id":"1e81a35c27aae6ad","type":"link out","z":"c6f2a894be05d857","g":"04fb6911559797a0","name":"link out 3","mode":"link","links":["f582702ec222069c"],"x":535,"y":120,"wires":[]},{"id":"8a3c077f0f85a905","type":"group","z":"c6f2a894be05d857","g":"6b8059f703d0f574","name":"Download the images","style":{"label":true},"nodes":["453f3b9d7d312bd2","ecbd7b1a410ecd9d","4d650baa2118036e","f582702ec222069c"],"x":84,"y":179,"w":532,"h":82},{"id":"453f3b9d7d312bd2","type":"http request","z":"c6f2a894be05d857","g":"8a3c077f0f85a905","name":"Get the image","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":460,"y":220,"wires":[["4d650baa2118036e"]]},{"id":"ecbd7b1a410ecd9d","type":"change","z":"c6f2a894be05d857","g":"8a3c077f0f85a905","name":"Set URL to download","rules":[{"t":"move","p":"payload","pt":"msg","to":"url","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":220,"wires":[["453f3b9d7d312bd2"]]},{"id":"4d650baa2118036e","type":"link out","z":"c6f2a894be05d857","g":"8a3c077f0f85a905","name":"link out 1","mode":"link","links":["8bc38803dec97185"],"x":575,"y":220,"wires":[]},{"id":"f582702ec222069c","type":"link in","z":"c6f2a894be05d857","g":"8a3c077f0f85a905","name":"link in 3","links":["1e81a35c27aae6ad"],"x":125,"y":220,"wires":[["ecbd7b1a410ecd9d"]]},{"id":"22c5026dd58e418b","type":"group","z":"c6f2a894be05d857","g":"6b8059f703d0f574","name":"Save the images to the local storage","style":{"label":true},"nodes":["95c819560d22f394","0fe7f013b6356c5d","7edc6cb2b0d243db","829bfc8e6e293ada","8bc38803dec97185","98980d88013c6e12"],"x":84,"y":279,"w":932,"h":82},{"id":"95c819560d22f394","type":"base64","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"convert to base64","action":"str","property":"payload","x":250,"y":320,"wires":[["829bfc8e6e293ada"]]},{"id":"0fe7f013b6356c5d","type":"file","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"Write file to storage","filename":"filename","filenameType":"msg","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":850,"y":320,"wires":[["98980d88013c6e12"]]},{"id":"7edc6cb2b0d243db","type":"string","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"Get filename from the URL","methods":[{"name":"getRightMost","params":[{"type":"str","value":"/"}]}],"prop":"responseUrl","propout":"filename","object":"msg","objectout":"msg","x":620,"y":320,"wires":[["0fe7f013b6356c5d"]]},{"id":"829bfc8e6e293ada","type":"image","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"preview","width":"150","data":"payload","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":420,"y":320,"wires":[["7edc6cb2b0d243db"]]},{"id":"8bc38803dec97185","type":"link in","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"link in 1","links":["4d650baa2118036e"],"x":125,"y":320,"wires":[["95c819560d22f394"]]},{"id":"98980d88013c6e12","type":"link out","z":"c6f2a894be05d857","g":"22c5026dd58e418b","name":"link out 2","mode":"link","links":["1e94b5bab542830a"],"x":975,"y":320,"wires":[]},{"id":"6fcca5cfee2bcb89","type":"group","z":"c6f2a894be05d857","g":"6b8059f703d0f574","name":"Output a debug once all images have been processed","style":{"label":true},"nodes":["d1a6feea3ac829c6","119f8008752bc4fb","1e94b5bab542830a"],"x":64,"y":379,"w":382,"h":82},{"id":"d1a6feea3ac829c6","type":"debug","z":"c6f2a894be05d857","g":"6fcca5cfee2bcb89","name":"debug 140","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":330,"y":420,"wires":[]},{"id":"119f8008752bc4fb","type":"join","z":"c6f2a894be05d857","g":"6fcca5cfee2bcb89","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":190,"y":420,"wires":[["d1a6feea3ac829c6"]]},{"id":"1e94b5bab542830a","type":"link in","z":"c6f2a894be05d857","g":"6fcca5cfee2bcb89","name":"link in 2","links":["98980d88013c6e12"],"x":105,"y":420,"wires":[["119f8008752bc4fb"]]}] ``` :: We've now downloaded the images we need, and saved them to our local storage, to make them load more quickly when a user views them in the dashboard. Onto the second flow, which will get the images from the local storage and then load them into the dashboard. Let's take a look at it: ![Get the images from the local storage and place them in a dashboard](https://flowfuse.com/blog/2023/07/images/load-images-from-disk-and-show-in-dashboard.png "Get the images from the local storage and place them in a dashboard") You can import this flow into Node-RED using the code below: ::render-flow ```json [{"id":"596006fe79275d55","type":"group","z":"d4aa6dd5b63a56de","name":"Get the images from the filestore and display in the Dashboard","style":{"label":true},"nodes":["be421f8265c4c090","d2dc98b4f3d6b968","b9029adc6097bd14","1e54acecec6bd411","99aaee673fc70bc2"],"x":1228,"y":3733,"w":974,"h":654},{"id":"be421f8265c4c090","type":"group","z":"d4aa6dd5b63a56de","g":"596006fe79275d55","name":"Get the images from the local storage","style":{"label":true},"nodes":["c698b2945583e126","ab22b978a2f5d80f","2836914f9b643e34","8e32aba67d9ac9d4"],"x":1264,"y":3899,"w":492,"h":82},{"id":"c698b2945583e126","type":"image","z":"d4aa6dd5b63a56de","g":"be421f8265c4c090","name":"preview","width":"150","data":"payload","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1620,"y":3940,"wires":[["8e32aba67d9ac9d4"]]},{"id":"ab22b978a2f5d80f","type":"file in","z":"d4aa6dd5b63a56de","g":"be421f8265c4c090","name":"Read file from storage","filename":"payload","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":1440,"y":3940,"wires":[["c698b2945583e126"]]},{"id":"2836914f9b643e34","type":"link in","z":"d4aa6dd5b63a56de","g":"be421f8265c4c090","name":"link in 4","links":["a9958e1e4b07191a"],"x":1305,"y":3940,"wires":[["ab22b978a2f5d80f"]]},{"id":"8e32aba67d9ac9d4","type":"link out","z":"d4aa6dd5b63a56de","g":"be421f8265c4c090","name":"link out 5","mode":"link","links":["bbdd3a2e737e1074"],"x":1715,"y":3940,"wires":[]},{"id":"d2dc98b4f3d6b968","type":"group","z":"d4aa6dd5b63a56de","g":"596006fe79275d55","name":"Prepare each image to be shown in the dashboard","style":{"label":true},"nodes":["54f0e7fddf447d10","d856077cc6d37093","bbdd3a2e737e1074","2d29c953bcdab787"],"x":1264,"y":3999,"w":832,"h":82},{"id":"54f0e7fddf447d10","type":"change","z":"d4aa6dd5b63a56de","g":"d2dc98b4f3d6b968","name":"Add the file type to the mimetype, add to image content","rules":[{"t":"set","p":"mimetype","pt":"msg","to":"\"data:image/\"&msg.filetype&\";base64,\"","tot":"jsonata"},{"t":"set","p":"output","pt":"msg","to":"msg.mimetype&msg.payload","tot":"jsonata"},{"t":"move","p":"output","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1810,"y":4040,"wires":[["2d29c953bcdab787"]]},{"id":"d856077cc6d37093","type":"string","z":"d4aa6dd5b63a56de","g":"d2dc98b4f3d6b968","name":"Get file type from file name","methods":[{"name":"getRightMost","params":[{"type":"str","value":"."}]}],"prop":"filename","propout":"filetype","object":"msg","objectout":"msg","x":1460,"y":4040,"wires":[["54f0e7fddf447d10"]]},{"id":"bbdd3a2e737e1074","type":"link in","z":"d4aa6dd5b63a56de","g":"d2dc98b4f3d6b968","name":"link in 5","links":["8e32aba67d9ac9d4"],"x":1305,"y":4040,"wires":[["d856077cc6d37093"]]},{"id":"2d29c953bcdab787","type":"link out","z":"d4aa6dd5b63a56de","g":"d2dc98b4f3d6b968","name":"link out 6","mode":"link","links":["c5e4febe48363987"],"x":2055,"y":4040,"wires":[]},{"id":"b9029adc6097bd14","type":"group","z":"d4aa6dd5b63a56de","g":"596006fe79275d55","name":"Send the images to the correct section of the dashboard","style":{"label":true},"nodes":["fa873d7bccd2b6be","c5e4febe48363987","d80e5a54a7c5b2f8","fb0c76a6934b0a7f","4e4105a9774c7b5c","917c842a1b46ad4c"],"x":1264,"y":4099,"w":912,"h":162},{"id":"fa873d7bccd2b6be","type":"switch","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","name":"Send the image to the correct section of the dashboard","property":"filename","propertyType":"msg","rules":[{"t":"eq","v":"ff-logo-wordmark-light_4x.png","vt":"str"},{"t":"eq","v":"screen-pseudo-overview-2QvTVle3Mr-384.avif","vt":"str"},{"t":"eq","v":"nr-image-1.png","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":1550,"y":4180,"wires":[["917c842a1b46ad4c"],["4e4105a9774c7b5c"],["fb0c76a6934b0a7f"]]},{"id":"c5e4febe48363987","type":"link in","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","name":"link in 6","links":["2d29c953bcdab787"],"x":1305,"y":4180,"wires":[["fa873d7bccd2b6be"]]},{"id":"d80e5a54a7c5b2f8","type":"link out","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","name":"link out 7","mode":"link","links":["c9de7384c9ca672f"],"x":2135,"y":4180,"wires":[]},{"id":"fb0c76a6934b0a7f","type":"ui-template","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","group":"ec62d482d77f7908","page":"","ui":"","name":"Display the image on the Dashboard","order":6,"width":"3","height":"1","head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1930,"y":4220,"wires":[["d80e5a54a7c5b2f8"]]},{"id":"4e4105a9774c7b5c","type":"ui-template","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","group":"ec62d482d77f7908","page":"","ui":"","name":"Display the image on the Dashboard","order":5,"width":"3","height":"1","head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1930,"y":4180,"wires":[["d80e5a54a7c5b2f8"]]},{"id":"917c842a1b46ad4c","type":"ui-template","z":"d4aa6dd5b63a56de","g":"b9029adc6097bd14","group":"ec62d482d77f7908","page":"","ui":"","name":"Display the image on the Dashboard","order":2,"width":0,"height":0,"head":"","format":"","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1930,"y":4140,"wires":[["d80e5a54a7c5b2f8"]]},{"id":"ec62d482d77f7908","type":"ui-group","name":"Default","page":"550c9ac3b2ed01c9","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false"},{"id":"550c9ac3b2ed01c9","type":"ui-page","name":"Home","ui":"25f447d87d1ce5c9","path":"/","icon":"home","layout":"grid","theme":"c68088445147719b","breakpoints":[{"name":"Default","px":0,"cols":3},{"name":"Tablet","px":576,"cols":6},{"name":"Small Desktop","px":768,"cols":9},{"name":"Desktop","px":1024,"cols":12}],"order":1,"className":"","visible":true,"disabled":false},{"id":"25f447d87d1ce5c9","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-control","ui-notification"],"showPathInSidebar":false,"showPageTitle":false,"titleBarStyle":"default"},{"id":"c68088445147719b","type":"ui-theme","name":"Theme Name","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}},{"id":"1e54acecec6bd411","type":"group","z":"d4aa6dd5b63a56de","g":"596006fe79275d55","name":"Output a debug once all images have been processed","style":{"label":true},"nodes":["c9de7384c9ca672f","b073cd4970e6ddf2","c1b52f44fd4260b3"],"x":1254,"y":4279,"w":392,"h":82},{"id":"c9de7384c9ca672f","type":"link in","z":"d4aa6dd5b63a56de","g":"1e54acecec6bd411","name":"link in 7","links":["d80e5a54a7c5b2f8"],"x":1295,"y":4320,"wires":[["b073cd4970e6ddf2"]]},{"id":"b073cd4970e6ddf2","type":"join","z":"d4aa6dd5b63a56de","g":"1e54acecec6bd411","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":1380,"y":4320,"wires":[["c1b52f44fd4260b3"]]},{"id":"c1b52f44fd4260b3","type":"debug","z":"d4aa6dd5b63a56de","g":"1e54acecec6bd411","name":"debug 141","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1530,"y":4320,"wires":[]},{"id":"99aaee673fc70bc2","type":"group","z":"d4aa6dd5b63a56de","g":"596006fe79275d55","name":"Inject the image files' names","style":{"label":true},"nodes":["11787d1ab6c8a9ec","49b8f3cce48dd5a9","a9958e1e4b07191a","c452f250e1303c69","08b30802899573a6"],"x":1254,"y":3759,"w":782,"h":122},{"id":"11787d1ab6c8a9ec","type":"inject","z":"d4aa6dd5b63a56de","g":"99aaee673fc70bc2","name":"Inject","props":[],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","x":1350,"y":3800,"wires":[["c452f250e1303c69"]]},{"id":"49b8f3cce48dd5a9","type":"split","z":"d4aa6dd5b63a56de","g":"99aaee673fc70bc2","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":1910,"y":3800,"wires":[["a9958e1e4b07191a"]]},{"id":"a9958e1e4b07191a","type":"link out","z":"d4aa6dd5b63a56de","g":"99aaee673fc70bc2","name":"link out 4","mode":"link","links":["2836914f9b643e34"],"x":1995,"y":3800,"wires":[]},{"id":"c452f250e1303c69","type":"change","z":"d4aa6dd5b63a56de","g":"99aaee673fc70bc2","name":"Image file names as an array","rules":[{"t":"set","p":"payload","pt":"msg","to":"[\"ff-logo-wordmark-light_4x.png\",\"screen-pseudo-overview-2QvTVle3Mr-384.avif\",\"nr-image-1.png\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":1720,"y":3800,"wires":[["49b8f3cce48dd5a9"]]},{"id":"08b30802899573a6","type":"ui-event","z":"d4aa6dd5b63a56de","g":"99aaee673fc70bc2","ui":"25f447d87d1ce5c9","name":"Update images on dashboard open","x":1420,"y":3840,"wires":[["c452f250e1303c69"]]}] ``` :: I have also included some simple dashboard elements you can view alongside the images. Let's take a look at the dashboard: :video{ariaLabel="The dashboard showing our images alongside other standard elements" autoPlay="true" height="656" loop="true" muted="true" playsInline="true" preload="none" width="646"} If you import these flows into Node-RED, you should see the images automatically loaded into the dashboard when you view it. You can also replace the URLs and file paths to try using some different images if you'd like to. ## More things to try In this example, the images are static but it's simple to load images depending on the state of the flow. As mentioned in this article's introduction, you could display context aware images guiding the user of the dashboard to a specific location on a map, to complete a maintenance task. If you're interested in seeing examples of dynamic image loading please comment below. ## Conclusion Images can significantly enhance dashboards, but ensuring their proper display in different Node-RED hosting environments, especially within DevOps pipelines, can be challenging. The techniques discussed here enable effective use of images in dashboards, even within containerized setups. Additionally, if you are using FlowFuse, the new features simplify adding and managing static assets. I'd love to hear your comments and suggestions on this article. please tell us what you think about this article, and how you might use these techniques in the comments section below. # Creating a Historical Data Dashboard with InfluxDB and Node-RED Every new dashboard is met with the fast-following request, “can we save this data and somehow look back on it?” Yes, you can, and let’s use InfluxDB to make it happen! :product-update-note Edge devices are often polling sensors at regular intervals and are a perfect candidate to be paired with a database purpose-built for time-series data, like InfluxDB. Let’s capture some data, create a live chart, store the data, and then create a GUI for retrieving the data. Here’s a screenshot of the dashboard we will create, which is divided into two sections. The first section displays live data, while the second section consists of fields that enable users to query the database and retrieve historical data. Looking at the live data, the chart depicts a sinusoidal graph that represents the scale measurements used for quality assurance in the aggregate production process at an automated mining operation. The graph showcases fluctuations in weight over time, indicating variations in the samples being weighed. This monitoring process ensures the quality and consistency of the aggregates being produced. The historical data shows a snippet of this information that was retrieved from InfluxDB. !["Screenshot showing the historical dashboard"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/dashboard-1.png "Screenshot showing the historical dashboard") :br Here is a screenshot of the simple Node-RED flow to create that dashboard. We will dive into the details through this article, and, by the end, you will be able to create this flow yourself. ![Screenshot showing the historical dashboard flow](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/flow-2.png "Screenshot showing the historical dashboard flow") ## Capturing serial port data The live view is fed by data coming from a simple scale with a serial interface. This [Brecknell LPS-150](https://www.brecknellscales.com/wp-content/uploads/2022/09/LPS-Series_u_en_fr_501724-1.pdf){rel=""nofollow""} scale will auto power-on, remembers the last tare setting, and continuously sends its reading via RS-232, so it is a great unit to use for unattended IoT projects. On the Node-RED side, a serial node can be configured to capture this incoming data. If your device running Node-RED doesn’t have an RS-232 port, there are many variations of RS-232-to-USB cables to help you connect. This scale is sending data at a very high-speed interval so it is important to use a “delay” node before the rest of your flow gets bogged down. Below, I have configured the serial port node with the same settings that were used to set up the scale. These settings are commonly documented as "9600 8N1" in shorthand. In serial communication it is necessary for the two devices to have the exact same settings or the data becomes garbled. The incoming stream of ASCII text is divided using the hex value 0x0D, which corresponds to the return character. This character is used as a delimiter to separate the individual chunks of text within the incoming data stream. !["Screenshot showing the serial port node config"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/serial-port-node-3.png "Screenshot showing the serial port node config") :br With this “delay” node, we now have a new message from the scale at a rate of 1 msg per 5 seconds !["Screenshot showing the delay node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/delay-4.png "Screenshot showing the delay node properties tab") :br The debugger allows us to see the raw data as it is captured. !["Debugger"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/debugger-5.png "Debugger") :br Unfortunately, these values are not in a friendly form to work with. Ideally, we want our payload to just be a number, not this string with odd characters, extra spaces, and the units. !["Screenshot showing the change node setting payload"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/change-node-6.png "Screenshot showing the change node setting payload") :br :cta-image{alt="Power Workplace relies on FlowFuse for scalability, reliability and security audits - book a demo" cta="demo" src="https://flowfuse.com/images/cta/power-workplace-book-demo.png"} We need to extract the numeric part of the string using a regular expression with a “change” node and the JSONata expression `$number($match(msg.payload, /-?(\d+(\.\d+)?)/, 10).match)`. `$match` and `/-?(\d+(\.\d+)?)/` help the function pull out the numeric components of the string and `$number` parses these components to be an actual number data type. Here are the properties of the “change” node. When we look in the debugger we see the payload specified as a “number” and the value displayed in blue, both indications that we have successfully extracted the weight as the correct data type. !["Screenshot showing the output type in the debug panel"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/number-7.png "Screenshot showing the output type in the debug panel") ## Setting up serverless InfluxDB in the cloud Now we have some live data, let’s store it using InfluxDB. Below are the steps to set up an account with the InfluxDB free service. Navigate to {rel=""nofollow""} and let’s begin. Click on “Get Started for Free” under Cloud, InfluxDB Cloud Serverless. !["Screenshot showing the Influxdb cloud 'Getting started' button"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/run-influxdb-8.png "Screenshot showing the Influxdb cloud 'Getting started' button") For this example the Free plan will work fine. !["Screenshot showing the influxdb cloud tiers"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/free-influxdb-9.png "Screenshot showing the influxdb cloud tiers") Create a bucket to store the data. !["Screenshot showing the 'Go to buckets' button"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/bucket-influxdb-10.png "Screenshot showing the 'Go to buckets' button") !["Screenshot showing the 'Create Bucket' button"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/load-data-influxdb-11.png "Screenshot showing the 'Create Bucket' button") Generate a token to direct the calls from Node-RED to your InfluxDB account when they hit the InfluxDB server: !["Screenshot showing the 'Genrate token' button"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/token-influxdb-12.png "Screenshot showing the 'Genrate token' button") I selected “Generate All Access API Token,” but eventually you will want a custom, more restricted approach. !["Screenshot showing the 'prompt' asing to enter the description for token"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/generate-token-influxdb-13.png "Screenshot showing the 'prompt' asing to enter the description for token") Copy your token and do not share it! (mine will be deleted later) ## Connecting Node-RED to InfluxDB Navigate to “Manage Palette” in the Node-RED hamburger menu in the upper right corner of the flow editor. I did a search for InfluxDB and selected the most popular one, “node-red-contrib-influxdb” by looking at the number of downloads per week at {rel=""nofollow""}. When you are just starting out, it can be a smart decision to go with the popular option. The popularity indicates a level of trust and adoption within the community, making it a reliable choice for beginners !["Screenshot showing the influxdb node in the manage palette"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/install-contrib-influxdb-14.png "Screenshot showing the influxdb node in the manage palette") :br After installing this package you will see three new nodes in your flow editor. ![Screenshot showing the installed influxdb nodes in the palette](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/influxdb-nodes-15.png "Screenshot showing the installed influxdb nodes in the palette") Drag and drop the “influxdb out” node into your flow, double click on it, and start filling out the needed fields. The naming convention of `test<>` works well for initial setups to make it clear what names should go where. !["Screenshot showing the influxdb-out node config"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/influxdb-out-node-16.png "Screenshot showing the influxdb-out node config") :br It was a little unclear what URL to use with this serverless option, but I guessed it was the same as the URL for the InfluxDB resource center account page, “{rel=""nofollow""}” and it worked. Then, enter the API token that was generated earlier. !["image\_tooltip"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/influxdb-node-17.png "image_tooltip") :br The “influxdb out” node is now ready to start storing payloads. The documentation for the InfluxDB nodes at {rel=""nofollow""} gives more detail as to extra options, such as tags, that you might want to attach to your data being stored. In this simple example, we are just going to send the “influxdb out” node a number via the msg.payload. ![Screenshot showing the flow sending data to InfluxDB and to a dashboard chart widget"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/flow-influxdb-out-18.png "Screenshot showing the flow sending data to InfluxDB and to a dashboard chart widget") :br Here is a chart of the live data which is also being stored. !["Screenshot showing the dashboard with live data chart"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/live-data-chart-19.png "Screenshot showing the dashboard with live data chart") :br The InfluxDB Data Explorer helps you create a SQL call and allows you to run it right in the browser so you can verify that your data is being stored correctly. !["Screenshot showing the InfluxDB Explorer"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/influxdb-explorer-20.png "Screenshot showing the InfluxDB Explorer") :br ## Creating a historical data GUI Now we have our data being stored, but we aren’t quite finished. We still want an easy way to pull this information up and for it to be presented in a chart, just like the live data. Here is the Dashboard group we will create for this GUI. !["Screenshot showing the GUI for historical data"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/historical-data-21.png "Screenshot showing the GUI for historical data") And here is the flow to create it. !["Screenshot showing the flow of historical data GUI."](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/data-flow-22.png "Screenshot showing the flow of historical data GUI") A “template” node creates a convenient way to create a plain text output with variable properties within. Below you can see that `msg.query` is created from a string of text with “rangeStart” and “rangeEnd” dynamically inserted using the “mustache” syntax. More information about how to query InfluxDB can be found here: {rel=""nofollow""}. !["Screenshot showing the template node"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/template-node-23.png "Screenshot showing the template node") :br Using the "Form" dashboard node is an easy way to collect all the required information for our query. We need to be able to enter in a date and time to start gathering the data, and a window to know how long a range of values to pull. !["Screenshot of the form widget"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/form-node-24.png "Screenshot of the form widget") Here is the code from the “time/date” function node. A bit of juggling of local time versus UTC time is needed to allow the user to intuitively query the correct data for their timezone. !["Screenshot of the function node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/function-node-25.png "Screenshot of the function node properties tab") :br Here is the “change” node used to create the msg.rangeEnd. The JSONatta expression is `$fromMillis($toMillis(msg.rangeStart) + msg.payload.window * 60 * 1000)`. The expression combines the milliseconds from the `msg.rangeStart` with the calculated milliseconds in the “Window (minutes)” from the GUI. !["Screenshot of the change node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/change-node-26.png "Screenshot of the change node properties tab") :br Now that the query is coming back from InfluxDB, let’s break down how to transform this data object into one that can be read by the “chart” node. Below, we see on the left column what the object looks like from InfluxDB and on the right we see how it must be structured to be viewed in the chart. !["Screenshot showing the formated data in the debug panel"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/data-format-27.png "Screenshot showing the formated data in the debug panel") Rob Marcer has a great article on working with persistent chart data found here: [/blog/2023/05/persisting-chart-data-in-node-red/](https://flowfuse.com/blog/2023/05/persisting-chart-data-in-node-red/). We can use a series of nodes from the Node-RED core package to transform this data. !["Screenshot showing the flow to transform the data"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/transform-data-28.png "Screenshot showing the flow to transform the data") :br First, a “switch” node is used to determine if the response InfluxDB contains any data so that we can either format the data properly, or clear the chart and indicate “No Data.” !["Screenshot of the switch node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/switch-node-28.png "Screenshot of the switch node properties tab") :br The “Label” field in the “chart” node can also be dynamically created with the mustache syntax. !["Screenshot of the chart widget setting label dynamically"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/chart-node-29.png "Screenshot of the chart widget setting label dynamically") :br If the “is not empty” “switch” node sees an empty payload, this “change” node sets the payload to an empty array, clearing the chart, and sets the `msg.title` to “No Data” so users know their query, though successful, returned an empty set of values. !["Screenshot of the change node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/change-node-30.png "Screenshot of the change node properties tab") :br The parameters for the “split” node can be left as-is. !["Screenshot of the split node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/split-node-31.png "Screenshot of the split node properties tab") :br In the “chartData” “change” node, will pull out the two values we need for the chart, milliseconds since the UNIX epoch for the x-value and the measurement from the scale for the y-value. A simple JSONatta expression helps us transform the date from a string to milliseconds for the x-value. !["Screenshot of the change node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/change-node-32.png "Screenshot of the change node properties tab") :br The “join” node just needs to be set to “Combine each” msg.chartData object and configured “to create” an array. !["Screenshot of the join node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/join-node-33.png "Screenshot of the join node properties tab") :br The final “change” node, “format,” is where we prescribe the format needed for the “chart” node, [{"series": [""],"data":\[\[]],"labels": [""] }], and finally we insert our `msg.chartData` array into that structure. Notice `msg.title` is now set to “Data Received.” !["Screenshot of the change node properties tab"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/change-node-33.png "Screenshot of the change node properties tab") :br And, there you have it. You can query the same range of data found on the live chart to ensure the code is working and then you can use the dashboard to pull up historical data, way in the past from what is shown on the live chart. !["Screenshot of the dashboard showing the historical and live data chart"](https://flowfuse.com/blog/2023/07/images/influxdb-historical-data/final-dashboard-34.png "Screenshot of the dashboard showing the historical and live data chart") Your current version is clear and concise, great for a blog or call-to-action section. Here's a slightly refined version with improved flow, punctuation, and tone for polish, while keeping the structure intact: ## Final Thoughts You’ve successfully built a powerful dashboard that captures real-time data and stores it in InfluxDB for historical analysis. This setup provides valuable insights into your process data and helps identify trends over time. If you are planning to run this dashboard in production, a few important questions may come to mind: How do you ensure it runs reliably 24/7? How do you deploy it to other locations? What happens if someone accidentally breaks the flow? This is where **FlowFuse** comes in. It takes your existing Node-RED flows and adds: - Automatic backups and instant recovery - Easy deployment to multiple locations - Version control for safe and trackable updates - Team collaboration without conflicts - Remote instance management, and much more Your flows continue to work just as they are, FlowFuse simply makes them production-ready. [Try FlowFuse free for 30 days](https://app.flowfuse.com/account/create){rel=""nofollow""} and see how it transforms Node-RED into a scalable, enterprise-ready platform. # FlowFuse is now available on AWS Marketplace Many customers want to run FlowFuse in their own cloud environment, AWS being a great example. Today we're excited to announce that FlowFuse is now available from the [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-3ycrknfg67rug?sr=0-1&ref_=beagle&applicationId=AWSMPContessa){rel=""nofollow""}. This makes it very easy for customers to install and run Node-RED and FlowFuse within minutes. FlowFuse allows organizations to reliably deliver Node-RED applications in a continuous, collaborative and secure manner. Customers running FlowFuse on AWS Cloud will benefit from FlowFuse's features, including: - Team collaboration for Node-RED developers, allowing multiple developers to work together on a single Node-RED instance, including the ability to have an audit log of changes. - DevOps deliver pipelines that support a software development lifecycle for Node-RED development. Pipelines can be setup to establish development, test and production environments for Node-RED instances. - Snapshot of Node-RED instances to create a version history of changes to Node-RED applications. This also includes the ability to rollback to a previous version of a Node-RED instance. FlowFuse on the AWS Marketplace is available under the Apache License c2.0 open source license. Customers can use FlowFuse free of charge but will need to pay AWS EC2 usage for hosting FlowFuse. FlowFuse offers commercially licensed Premimum and Enterprise tiers that includes enterprise oriented features, including: - 24/7 technical support - Single Sign-on - High Availability for Node-RED applications - Remote device management Customers can easily upgrade to the premium or enterprise tier by obtaining a commercial license from FlowFuse. To understand what FlowFuse can do for your use-case, please [book a demo](https://flowfuse.com/book-demo). FlowFuse on AWS Marketplace is available [immediately](https://aws.amazon.com/marketplace/pp/prodview-3ycrknfg67rug?sr=0-1&ref_=beagle&applicationId=AWSMPContessa){rel=""nofollow""}. Please refer to our [documentation](https://flowfuse.com/docs/install/docker/aws-marketplace/#installing-flowfuse-from-aws-market-place) and give it a try and let us know what you think. # Community News August 2023 Welcome to the FlowFuse newsletter for August 2023, a monthly roundup of what’s been happening with FlowFuse and the wider Node-RED community. ## New Release Last week we released FlowFuse 1.10, featuring improvements to our device management solutions and the new ability to import environment variable templates. Read about the details of FlowFuse 1.10 in our [release announcement](https://flowfuse.com/blog/2023/08/flowforge-1-10-release/). ## Upcoming events ### Getting Started with OPC-UA and Node-RED OPC-UA is a popular communication protocol used to communicate industrial data between different types of hardware and software. Our next webinar show how to use Node-RED to create an OPC-UA client that can read OPC data and visualize the data in Node-RED. We are glad to welcome Mika Karaila, Research Director @ Valmet Automation and creator of the OPC-UA nodes, as our webinar speaker. [Sign-up today](https://flowfuse.com/webinars/2023/getting-started-opcua-node-red/) to join us on July 27. ## From our Blog - Our developer advocate, Richard Meyer, published a series of articles on OPC-UA: - Part 1: [How to Deploy a Basic OPC-UA Server in Node-RED](https://flowfuse.com/blog/2023/07/how-to-deploy-a-basic-opc-ua-server-in-node-red/) - Part 2: [How to Build a Secure OPC-UA Server for PLCs in Node-RED](https://flowfuse.com/docs/node-red/protocol/opc-ua/) - Part 3: [How to Build an OPC UA Client Dashboard in Node-RED](https://flowfuse.com/blog/2023/07/how-to-build-a-opc-client-dashboard-in-node-red/) - [First Pre-Alpha Release of the new Node-RED Dashboard](https://flowfuse.com/blog/2023/07/dashboard-0-1-release/) - update on FlowFuse's work to develop the next generation of Node-RED dashboard. - [How to add images to Node-RED dashboards when using FlowFuse](https://flowfuse.com/blog/2023/07/images-in-node-red-dashboards/) - some tips on how to add images to a dashboard when running Node-RED instances in a docker environment, like FlowFuse. - [Creating a Historical Data Dashboard with InfluxDB and Node-RED](https://flowfuse.com/blog/2023/07/influxdb-historical-data/) - an in-depth article on how to store historical data in InfluxDB that can be visualize with Node-RED dashboard. ## From the Community - **Featured Node**: [Buffer Parser](https://flows.nodered.org/node/node-red-contrib-buffer-parser){rel=""nofollow""} - a really useful node for parsing buffers/arrays that are common in industrial data. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} # Dashboard 2.0 - Community Update Welcome to the latest Node-RED Dashboard 2.0 update. We've added lots of new widgets, cleaned up compatibility issues alongside Dashboard 1.0 and made strides to improve the events system linking the Node-RED editor with the Dashboard. I firstly need to begin with a *"Thank You"* to the dozens of pre-alpha users we've had so far. Thanks for being patient whilst we're shipping fast and breaking things. We've had some great feedback, and we're working hard to implement is as best as possible. With all of the changes we've been making, we've also made the decision to jump to minor version numbers, and so, **0.1.0 is available now**. Below you'll find a summary of the changes we've made since our [last community update](https://flowfuse.com/blog/2023/07/dashboard-0-1-release). ## New Widgets ### Template ([docs](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html)) Steve has been doing some incredible work on the new `ui-template` widget. This widget allows you to create your own custom components using raw HTML, but also works with any of the components in the [Vuetify](https://vuetifyjs.com/en/components/all/){rel=""nofollow""} component library. It's a powerful tool that will enable users to be creative with their own widgets that are not currently available with the standard set of widgets. ![Examples of ui-template](https://dashboard.flowfuse.com/images/node-examples/ui-template.png) The Template node also provides access to two built-in functions that can be used to send data back to Node-RED: - **send(msg)**: Outputs a message (defined by the input to this function call) from this node in the Node-RED flow. - **submit()**: Send a `FormData` object when attached to a `
` element. The created object will consist of the `name` attributes for each form element, corresponding to their respective `value` attributes. ### Toggle Switch ([docs](https://dashboard.flowfuse.com/nodes/widgets/ui-switch.html)) ![Examples of ui-switch](https://dashboard.flowfuse.com/images/node-examples/ui-switch.png) Adds a toggle switch to the user interface that can be rendered with a label, and traditional toggle switch, or, as in Dashboard 1.0, can be a square element with an icon & color provided. ## Fixes & Other Changes ### Sidebar As requested on multiple occasions by the community when we released v0.0.4 of Dashboard 2.0, we've now added a side menu, as per Dashboard 1.0. Currently, this *just* provides a link to the Dashboard UI, but gives us a canvas on which to expand functionality in the future. ### Improved Events System We've re-structured the hierarchy of the events system to make it more streamlined. Now, the `ui-base` manages comms via single channels dedicated to each event type, and the widget's ID is then used as a topic. Previously, we had a separate channel for each `action:id`. If you're interested in learning more about our events architecture, you can read about it [here](https://dashboard.flowfuse.com/contributing/guides/events.html){rel=""nofollow""} in the docs. ### Documentation Updates It's not glamorous, but it's important. We've made sure that all documentation and help text inside Node-RED is fully up to date for the Dashboard 2.0 nodes. We've also include rendered examples for all widgets in our [online documentation](https://dashboard.flowfuse.com/){rel=""nofollow""} too. We've also made sure that any legacy options that had been transferred over from Dashboard 1.0 that haven't been fully implemented yet are temporarily hidden. This means, any options you're seeing, *should* be working. If they're not - it's a bug. ## What's Next? We have a lot of things to keep us busy, we are documenting them all in GitHub, and have made public our [planning board](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""}. You can see what we're working on, what's coming up next, and what we've got planned for the future. As always, we're open to ideas, feedback & contributions. If you'd like to get involved, please check out our GitHub Repository [here](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. ## Join Our Team If you'd like to be paid to directly contribute to Dashboard 2.0, we are hiring for a 2-3 month position to do just that: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} # FlowFuse 1.10 Release Now Available FlowFuse 1.10 release includes improvements to device management and importing environment variable templates. ## Import Environment Variable Templates [#2372](https://github.com/FlowFuse/flowfuse/issues/2372){rel=""nofollow""} FlowFuse 1.10 now allows users to import environment variable templates. This makes it much easier and less error prone to maintain and add new environment variables to Node-RED instances running on FlowFuse. ## DevOps Pipelines now can include devices [#2243](https://github.com/FlowFuse/flowfuse/issues/2243){rel=""nofollow""} DevOps pipelines have proven very popular for creating dev/test/production environments for Node-RED flow development. Now, devices can be associated with a pipeline so when a snapshot is created it can be pushed to all the devices associated with the pipeline. This will improve the overall quality and reliability of Node-RED development for remote devices. ## Devices can now access the team library [#2294](https://github.com/FlowFuse/flowfuse/issues/2294){rel=""nofollow""} Team libraries allow Node-RED development team to share common flows and nodes through a shared library. Until the 1.10 release, Node-RED running remotely on a device did not have access to the team library. This limitation is now removed so device development can benefit from reusing standard flows. ## Other New Features - Add description of device type field [#2428](https://github.com/FlowFuse/flowfuse/issues/2428){rel=""nofollow""} - Improve reliability of device editor [#2483](https://github.com/FlowFuse/flowfuse/issues/2483){rel=""nofollow""} - Improve error feedback from device editor tunnel [#2473](https://github.com/FlowFuse/flowfuse/issues/2473){rel=""nofollow""} ## Bug Fixes - Improve visualization of Last Seen & Last Known with large amounts of devices. [#2380](https://github.com/FlowFuse/flowfuse/issues/2380){rel=""nofollow""} - Fix billing information error in FlowFuse Cloud [#2416](https://github.com/FlowFuse/flowfuse/issues/2416){rel=""nofollow""} - Fix T\&C checkbox on sign-up page [#2419](https://github.com/FlowFuse/flowfuse/issues/2419){rel=""nofollow""} ## Community Contributions Thanks to our community members for their contributions to this release. - [dfulgham](https://github.com/dfulgham){rel=""nofollow""} - Added support for annotation substitutions [#95](https://github.com/FlowFuse/flowforge-driver-k8s/pull/95){rel=""nofollow""} - [elanaviter](https://github.com/elenaviter){rel=""nofollow""} - Editors: allow optional service account linkage [#92](https://github.com/FlowFuse/flowforge-driver-k8s/pull/92){rel=""nofollow""} ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our [Product Roadmap Page](https://flowfuse.com/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.10. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go the the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # FlowForge is now FlowFuse We are happy to announce that FlowForge is changing its name to FlowFuse. Changing our corporate identity wasn’t our top priority but a recent trademark challenge has promoted us to create a new brand for our company. :cta-image{alt="Walk through your FlowFuse setup with our team - book a demo" cta="demo" src="https://flowfuse.com/images/cta/book-a-demo.png"} We believe that this new name better reflects our core mission and aspirations. Just as electricity fuses elements to create energy, the FlowFuse platform fuses together data, ideas, processes, and technologies to generate a powerful force of innovation and transformation. Selecting a new name for a company or product is never easy. Some of the requirements we set for a new name were: 1) it should be reasonably close to FlowForge so it will be easier to transition the brand identity, 2) it should even better reflect our mission, and 3) keeping the FF acronym will help with environment variable prefixes :-). I think we have accomplished all the above and am delighted to move forward as FlowFuse. **What to Expect Next** Our team is heads down building a platform to allow organizations to reliably deliver Node-RED applications in a continuous, collaborative, and secure manner. We'll continue to release regular updates packed with new features and enhancements that keeps moving us towards this goal. A complete rebranding is a big piece of work - today marks the start of a process as we introduce the new FlowFuse name and updated [website](https://flowfuse.com). Over the next number of days and weeks we'll continue to roll this change out, including our social media accounts and other accounts. The product branding will be changed over the next couple of releases, including FlowForge Cloud and FlowForge open source edition. The underlying software will be the as-if you’re running the next FlowForge release, though now called FlowFuse. We'll share more technical details of this when any changes are made. We are excited by the future FlowFuse presents to our customers and the industry. Node-RED and FlowFuse is a powerful combination that gives access to industrial data to transform organizations and drive forward innovation across industries. Join us as we continue our journey. # FlowFuse 1.11 makes it easier to get started with FlowFuse and Node-RED FlowFuse 1.11 introduces a new starter tier for FlowFuse Cloud that makes it easier to get started with FlowFuse and Node-RED. :product-update-note ## New FlowFuse Cloud Starter Tier [#2328](https://github.com/FlowFuse/flowfuse/issues/2328){rel=""nofollow""} It is now easier for Node-RED developers to get started with FlowFuse and Node-RED. The new starter tier allows developers to use two Node-RED instances and two remote device deployments. Ideal for creating proof of concepts or running a home automation system with Node-RED. FlowFuse provides a cloud hosted version of Node-RED so developers don't need to worry about Node-RED installation or operation. This makes it a lot easier to get started with Node-RED and easier to maintain a running instance. ## FlowFuse API access now possible via personal access tokens [#14](https://github.com/FlowFuse/flowfuse/issues/14){rel=""nofollow""} FlowFuse APIs are now accessible via personal access tokens (PAT). This makes it possible to create automation scripts that interact with the FlowFuse platform using the API and authenticate the scripts with the PAT. ## Usability Improvements to Device Management [#2294](https://github.com/FlowFuse/flowfuse/issues/2334){rel=""nofollow""} A number of usability improvements have been added to the FlowFuse device management solution to make it more flexible and intuitive to use. These improvements include being able to associate devices at the application level allowing for easier editing of Node-RED instances on edge devices. ## FlowFuse Rebranding [#119](https://github.com/orgs/FlowFuse/projects/1?pane=issue&itemId=34719640){rel=""nofollow""} Earlier in August, [FlowForge announced](https://flowfuse.com/blog/2023/08/flowforge-is-now-flowfuse/) a change to our company and product name to FlowFuse. Work has begun to change the product branding to FlowFuse. The UI has been rebranded and the remaining points will be changed in the next release. ## Other New Features - Add ability to add a description to an application and display it in the portal [#2279](https://github.com/FlowFuse/flowfuse/issues/2279){rel=""nofollow""} - UI Improvements to device management [#2427](https://github.com/FlowFuse/flowfuse/issues/2427){rel=""nofollow""} - Improve landing page for documentation [#842](https://github.com/FlowFuse/website/issues/842){rel=""nofollow""} - Restructure of user interface navigation [#2474](https://github.com/FlowFuse/flowfuse/issues/2474){rel=""nofollow""} ## Bug Fixes - Device running old snapshot [#132](https://github.com/FlowFuse/device-agent/issues/132){rel=""nofollow""} ## What's next? We're always working to enhance your experience with FlowFuse. Here's how you can stay informed and contribute: - **Roadmap Overview**: Check out our [Product Roadmap Page](https://flowfuse.com/changelog/) to see what we're planning for future updates. - **Entire Roadmap**: Visit our [Roadmap on GitHub](https://github.com/orgs/FlowFuse/projects/5){rel=""nofollow""} to follow our progress and contribute your ideas. - **Feedback**: We're interested in your thoughts about FlowFuse. Your feedback is crucial to us, and we'd love to hear about your experiences with the new features and improvements. Please share your thoughts, suggestions, or report any [issues on GitHub](https://github.com/FlowFuse/flowfuse/issues/new/choose){rel=""nofollow""}. Together, we can make FlowFuse better with each release! ## Try it out We're confident you can have self managed FlowFuse running locally in under 30 minutes. You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running 1.11. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go to the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # Why the Automation Pyramid blocks digital transformation - The Role of Unified Namespace A few years ago, I wrote an [article](https://www.linkedin.com/pulse/iiot-circle-marian-raphael-demme/){rel=""nofollow""}, in German, detailing my understanding of how the Automation Pyramid, a widely adopted reference model for the IT landscape of manufacturing firms, is essentially hindering digital transformation. Now, as conversations around the Unified Namespace (UNS) and particular frameworks continue to evolve, I revisit my earlier notions, review the latest updates to reference frameworks, and update my article. :cta-image{alt="Talk to our team about replacing point-to-point wiring with certified OPC-UA and Modbus connections - no custom drivers" cta="demo" src="https://flowfuse.com/blog/2023/08/images/auto-pyramid-cta-1.png"} ## The Pyramid’s Dilemma The Automation Pyramid is grounded in the standard [ISA-95](https://www.isa.org/products/ansi-isa-95-00-01-2010-iec-62264-1-mod-enterprise){rel=""nofollow""}, which aligns with [IEC 62264](https://www.iso.org/standard/57308.html){rel=""nofollow""} and [DIN EN 62264](https://www.beuth.de/en/standard/din-en-62264-1/207270059){rel=""nofollow""}. It delineates the functional hierarchy within a manufacturing enterprise. Over 25 variations of the Automation Pyramid exist in academic literature, all of them fundamentally mapping to the same core concept, tracing back to the [Computer-integrated manufacturing](https://en.wikipedia.org/wiki/Computer-integrated_manufacturing){rel=""nofollow""} (CIM)-Pyramid of the 1970s. Although ISA-95 does not explicitly refer to a pyramid, it introduces five functional hierarchical levels often visualized as a pyramid. #### ISA-95 - Visualization ![ISA95](https://flowfuse.com/blog/2023/08/images/UNS/ISA95.svg) #### Automation Pyramid - Visualization ![Automation Pyramid](https://flowfuse.com/blog/2023/08/images/UNS/Automation-Pyramid.png) Source: Katti, Badarinath. (2020). Ontology-Based Approach to Decentralized Production Control in the Context of Cloud Manufacturing Execution Systems. 10.13140/RG.2.2.11486.46402. A notable critique of ISA-95 is the absence of some operational functions and hierarchical levels commonly seen in manufacturing, leading to a rigidity that limits its applicability. This inflexibility has been acknowledged in a more recent framework, called the ["Reference Architectural Model Industry 4.0"](https://www.isa.org/intech-home/2019/march-april/features/rami-4-0-reference-architectural-model-for-industr){rel=""nofollow""} RAMI 4.0 ([IEC PAS 63088](https://www.beuth.de/en/norm/pd-iec-pas-63088/272832590){rel=""nofollow""}). As a result, the authors' introduced a ["Smart Grid Architecture Model"](https://syc-se.iec.ch/wp-content/uploads/2019/10/Reference_Architecture_final.pdf){rel=""nofollow""} (SGAM) with three primary dimensions: Life Cycle & Value Stream ([IEC 62890](https://www.vde-verlag.de/iec-normen/248992/iec-62890-2020.html){rel=""nofollow""}), Hierarchy Levels ([IEC 62264](https://www.iso.org/standard/57308.html){rel=""nofollow""} and [IEC 61512](https://www.vde-verlag.de/iec-normen/216764/iec-61512-4-2009.html){rel=""nofollow""}), and six main layers displaying the functional architecture of the asset and the separation into physical and digital world. :video{ariaLabel="RAMI4.0" autoPlay="true" height="292" loop="true" muted="true" playsInline="true" preload="none" width="520"} However, my primary critique revolves around another issue – the structure and proposed communication methodology. Models based on layers, where each tier represents a functional area and could be covered by one or more applications, almost always lead to three fundamental problems: ### Problem 1: Information Loss and Transaction Costs In the traditional model, data collection flows upward from Levels 0 to 4, while planning goes downward from Level 4 to 0. Information traversing from Level 0 to Level 4 has to pass through at least four stages. Despite the theoretical lossless transmission of information, the practical scenario inevitably results in some degree of information loss between levels. The result is that the original information from Level 0 arrives at Level 4 late, altered, or not at all. **Example:** In a manufacturing plant, multiple sensors at Level 1 detect a sudden event. By the time this information passes through intermediary layers (e.g. PLC, SCADA, MES) to reach Level 4 where a planning decision can be made, it is delayed and distorted due to the multiple transitions. The factory might suffer damage before proper actions are taken because the original data didn't arrive on time or at all. ### Problem 2: The Expense of One-to-One Connections The Automation Pyramid is based on different layers. Consequently, one-to-one connections between IT systems become a necessity for data transfer between levels. For example, Level 3 IT systems need at least two connections to the adjacent levels. This can lead to thousands of one-to-one interfaces between IT systems, incurring exorbitant costs for projects and maintenance. **Example:** In a semiconductor company, the Manufacturing Execution System (MES) serves as a critical intermediary in the Automation Pyramid. It must be integrated both with PLCs at the lower level for real-time control and with the ERP system at the higher level for business planning. This complex integration leads to the creation of numerous one-to-one connections. Furthermore, in implementing Industry 4.0 use cases like analytical applications, MES data is often required, creating even more connections. The multitude of connections complicates the system, making changes extremely difficult and maintenance intensive. This inflexibility becomes a barrier to adaptability and growth, hindering the efficient digital transformation of the manufacturing process. ### Problem 3: AI's Dependence on Data Artificial intelligence (AI) requires extensive, well-organized data. Given the current architecture, data would have to be collected and prepared from case to case for each individual system and level. This would invariably lead to numerous new one-to-one connections, offering no flexibility. Hence, AI and the Automation Pyramid can only collaborate in a significantly restricted manner. **Example:** A car manufacturing firm aims to leverage a neural network for predictive maintenance. Within the constraints of the existing Automation Pyramid's architecture, the positioning for such an application is nonexistent. To train the neural network and subsequently analyse the data, a consolidation of varying hierarchical data is essential, such as sensory input, maintenance records, production scheduling plans, etc. Under the current architecture, the introduction of this application precipitates the creation of a multitude of new one-to-one connections. Consequently, it underscores the pressing need to rethink the structural paradigms. ## IIoT Circle and Unified Namespace To overcome the limitations of traditional industrial data architecture, a paradigm shift towards a modern distributed architecture is necessary. Rather than allowing data to exist in silos within and across layers of the technology stack, data should be made accessible in a unified manner, creating a single, centralized repository. This approach facilitates a single centralized source for all enterprise systems to access the required data for their operations. This framework, which I have been calling the IIoT Circle, modernizes the original idea of the Automation Pyramid. A "Unified Namespace" operates as the core element that processes, and permits data streams to be loaded and exported from other systems. All other applications communicate exclusively through the Unified Namespace, requiring only a single interface to be maintained per application. ![IToT Circle Image](https://flowfuse.com/blog/2023/08/images/UNS/IIoT-Circle.svg) In essence, Unified Namespace serves as the main data exchange hub within an organization. It structures, organizes, and maintains a real-time flow of data from a variety of sources, becoming the indisputable source of truth across the business. It simplifies data integration, eliminating the frequently convoluted, layered approach of traditional data systems. ### Single Source of Truth The Unified Namespace breaks down the linear and deterministic data structure, which create data silos restricted to their specific systems. Instead, Unified Namespace centralizes data from across the entire organization. This results in a 'single source of truth' - a consolidated, current, and comprehensive overview of the organization's data. ### The Organizational Structure Unified Namespace organizes data using a semantic hierarchy, similar to a meticulously arranged file share system. It can use the [ISA-95 part 2](https://www.isa.org/products/ansi-isa-95-00-02-2018-enterprise-control-system-i){rel=""nofollow""} or the RAMI 4.0 Hierarchy Level standards to structure the hierarchy. This data organization facilitates navigation, management, and decision-making. ### The Pub-Sub Approach The Publish-Subscribe (Pub-Sub) model facilitates communication that decouples the sender (publisher) from the receiver (subscriber), providing an efficient communication protocol to avoid one-to-one connections. It offers flexibility and scalability as it allows for one-to-many and many-to-one communications, enabling data to flow freely between systems. ## A Necessity for Open Source Moreover, in this discourse on the Unified Namespace, we cannot overlook the role of open-source. Owning foundational digital services, such as the Unified Namespace, is a necessity for any corporation embarking on its digital transformation journey. This ownership provides a solid foundation, allowing companies to chart their destinies. To avoid the constraining bounds of vendor lock-in, which can significantly limit a company's digital capabilities; open-source or self-developed software offers the best recourse. By its nature, open-source promotes transparency, collaboration, and freedom of use. These aspects are fundamental to fostering innovation and continuous improvement. As exemplified by the [MING Stack](https://flowfuse.com/blog/2023/02/ming-blog/), open source software can and should be incorporated into every level of the hierarchy. ## Summary – Advancing Current Standards The lag of standards behind the latest innovation is an open secret, a problem rooted in the nature and development of these standards. However, maintaining and updating these standards remains crucial as many people adhere to them. ISA-95 Part 6 mentions a Messaging Service Model (MSM) and proposes a "publish-subscribe" model as an option for transactions. This is a great step in the right direction. My recommendation for ISA-95 is to further develop Part 6 to clearly delineate the implementation pattern of the Unified Namespace. Additionally, ISA-95 Part 1 should make explicit references to the communication pattern detailed in Part 6 and transition from a layer model to a cycle, with the Unified Namespace as an integral part of the framework. RAMI 4.0's Communication Layer is rather abstract. It suggests the use of OPC-UA for everything in manufacturing, from "Product" to "Work Center". For "Enterprise" and "Connected World", it states "still undecided". My improvement suggestion is to define the "Communication Layer" new and to be more technology-agnostic. Be more explicit about what needs to be done and more flexible about how to do it. # Our Open Source offering is a tier, not our competition More than once we’ve been in discussion with prospective customers on what tier is the right tier for their current Node-RED adoption. The question is likely to come up "Why wouldn't we just use the open source version of FlowFuse?". The implicit discussion created is one that is alike the question: “Why wouldn’t we go with your competition?”. For FlowFuse, and most other open-core companies like us, the open licensed and free to use core is tier, not competition. In the traditional sense, the prospective customer is right. By the definition, a customer **buys** goods or services by exchanging it for **money**. For FlowFuse’s open-source edition, which is free as in beer and free as in speech, no money changes hands. It can be installed and run by anyone. Once the software is being installed and used, we consider we've gained a new customer. There’s an agreement in place, the Apache 2.0 license, and value is obtained by the customer. The only missing component compared to an ‘ordinary’ sale is the lack of money from the customer to FlowFuse. The fact that no monetary value is exchanged, like the situation where a customer picks the competitor, doesn’t make the open source tier competition. It is just a free tier. Another reason it's really a tier is that the core of the product is the same. In many open-core products, the path to upgrade from the open source license product to the paid tier is much alike customers are used to on SaaS models. In the reals of self-managed software that's mostly uploading a license and at times a few configuration steps. Furthermore, the open tier is a tier as the customer choses to not adopt all capabilities. They're leaving value on the table. Either this is because it's not quite clear what the value is or if the higher tiers provides enough business value to warrant the expense. Or the adoption journey for the customer doesn't yet require the full featured tiers. What’s unique about open source software, is that customers can exchange value towards the company and community building the software in other forms: by opening issues, updating documentation, advocating for the OSS variant, among other ways. While this is not **money**, it is significant for a young company like FlowFuse. ## Challenges with a open core free tier That's not to say that an open source tier is a silver bullet for a company. For one, it's hard to track how many users a software package has, and who these users are. For example; FlowFuse has [Telemetry](https://flowfuse.com/docs/admin/telemetry/#usage-telemetry), though it can be turned off. Nor do we know who hosts this software. Another challenge is around product and feature packaging. At FlowFuse and other open core companies it's uncommon to move features from the open tier to paid tier only. If this choice has been made it's a done deal, even when the product team got it wrong. Usually the initial thoughts are therefor to move all features into the paid tiers. However, this hampers long term growth as adoption of paid features are adopted later or not at all. We follow the [Open-Core buyer based model](https://opencoreventures.com/blog/2023-01-open-core-standard-pricing-model){rel=""nofollow""} to segregate the value, about which I'll write a post next time. Photo by [Matt Hardy](https://unsplash.com/@matthardy). # FlowFuse announces a Node-RED stack for Industry 4.0 applications on ctrlX AUTOMATION FlowFuse is pleased to announce they are now offering Node-RED plus select third party nodes from the Bosch Rexroth ctrlX World marketplace. FlowFuse is pleased to announce they are now offering Node-RED plus select third party nodes from the [Bosch Rexroth ctrlX Store](https://developer.community.boschrexroth.com/t5/Store-and-How-to/FlowFuse-Node-RED/ba-p/82135){rel=""nofollow""} marketplace. Rexroth customers building Industry 4.0 applications will now have a trusted vendor, in FlowFuse, to provide support and updates for using open source Node-RED in production. By partnering with FlowFuse, customers can reduce their risk of using Node-RED in production by relying upon FlowFuse Node-RED experts to assist with any development or production issues. [Node-RED](https://nodered.org/){rel=""nofollow""} is a popular open source low-code development environment widely used in industries for collecting and processing industrial data to deliver Industry 4.0 applications. FlowFuse is uniquely positioned to partner with ctrlX customers looking to use Node-RED in production. FlowFuse CTO Nick O’Leary is the co-creator and project leader of Node-RED. FlowFuse employs many Node-RED experts who have years of experience helping customers with successful deployment of Node-RED applications. The FlowFuse package offered in the ctrlX World marketplace will provide ctrlX customers the following benefits: - Support for Node-RED development and production deployments - Support for third party nodes of popular industrial protocols including: Modbus, OMRON, S7 and MC Protocol. FlowFuse is the only vendor providing professional technical support for Node-RED on the ctrlX platform. The package is available today at ctrlX World. Interested customers should contact their Rexroth sales representative for purchasing details. Interested customers can also contact FlowFuse directly at for additional information about the offering. # How ChatGPT improves Node-RED Developer Experience ChatGPT has the potential to have a significant impact on the Node-RED community. It is a powerful language model that can be used to generate flows, interpret them, and provide documentation, maybe soon even write the flow! The combination of ChatGPT, or generative AI at large, with Node-RED can significantly improve the developer experience with Node-RED. In this post we’ll review what the community has already built. ## How generative AI like ChatGPT is used for Node-RED ### Function node <3 ChatGPT ChatGPT, and other models, can write code for you, much like [GitHub CoPilot](https://github.com/features/copilot){rel=""nofollow""} or [GitLab Duo](https://about.gitlab.com/gitlab-duo/){rel=""nofollow""}. As Node-RED is ‘low-code’ the ability for generative AI to write the required code for you creates a paradigm shift to ‘no-code’! ![Example of Chat GPT to generate contents of a function node](https://flowfuse.com/blog/2023/09/images/chatgpt-fcn-example.png) At FlowFuse we’ve written about this [before](https://flowfuse.com/blog/2023/05/chatgpt-nodered-fcn-node/), and published a [plugin](https://github.com/FlowFuse/node-red-function-gpt){rel=""nofollow""}. This node allows flow developers to be more productive and efficient. While this works only for the function node, there’s countless other possibilities to describe a flow in text and import a ChatGPT generated flow that are on the horizon! ### Flow Interpretation When developing larger projects with multiple tabs, it’s important to understand what each tab contributes to the full project. This problem is compounded when the flows are developed by a team or the time between the flow was last updated is higher. ![ChatGPT Flow Interpretation](https://raw.githubusercontent.com/node-red-jp/node-red-contrib-plugin-chatgpt/main/infotab.png) [Kazuhito-san](https://www.linkedin.com/in/kazuhitoyokoi/){rel=""nofollow""} wrote a module for Node-RED to interpret the flow, nodes, and their order into a well structured documentation. Through a click of a button it's generated by the well-known OpenAI model. This is especially interesting as it's thus able regenerate it when changes were made by the developers. It’s a plugin that requires very little setup, and can be found in the [flow library](https://www.npmjs.com/package/node-red-contrib-plugin-chatgpt){rel=""nofollow""}. ### Lots of plugins The ecosystem of Node-RED has always been a fast adopter of new technology. There's nodes for [ChatGPT](https://flows.nodered.org/node/node-red-contrib-chatgpt){rel=""nofollow""}, [Google's Bard](https://flows.nodered.org/node/node-red-contrib-bard){rel=""nofollow""}, and many more. These plugins genernally let you build on top of these models, and don't nessecairly improve the developer experience. It's however a great source of inspiration! ### Further discussion These were three examples of how generative AI is used in the Node-RED community. Please let us know if you're using ChatGPT or other AI models with Node-RED? And what would be the killer feature for Node-RED and AI? # Community News September 2023 Welcome to the FlowFuse newsletter for September 2023, a monthly roundup of what’s been happening with FlowFuse and the wider Node-RED community. ## New Name The big news this month is that [FlowForge is now FlowFuse](https://flowfuse.com/blog/2023/08/flowforge-is-now-flowfuse/). Yes, we have changed our name but we are still focus on delivering great products for the Node-RED community. :product-update-note ## New Releases - Node-RED 3.1 has been [released](https://nodered.org/blog/2023/09/06/version-3-1-released){rel=""nofollow""}. Among the changes are Mermaid chart support, locking flows, and much more. - Last week, FlowFuse 1.11 was released. This release included personal access tokens for FlowFuse API and new tiers for FlowFuse Cloud, including a new starter tier. Check out the details in our [announcement](https://flowfuse.com/blog/2023/08/flowfuse-1-11-release/). ## Upcoming events ### Celebrate 10 Years of Node-RED and What’s New in 3.1 and Beyond Hard to believe that Node-RED was launched 10 years ago. We want to celebrate this great accomplishment and also show off the new Node-RED 3.1 release. Join Nick O'Leary for the 10 year celebration and here what is coming next. [Sign-up today](https://flowfuse.com/webinars/2023/node-red-10-years/) to join us on September 21. ## From our Blog - The Node-RED Dashboard 2.0 project is making excellent progress. Two updates were published in the last month. Make sure you check out the latest release and provide feedback. - [Dynamic Markdown, Tables & Notebooks with Dashboard 2.0](https://flowfuse.com/blog/2023/09/dashboard-notebook-layout/) - [Dashboard 2.0 - Community Update](https://flowfuse.com/blog/2023/08/dashboard-community-update/) - [FlowFuse announces a Node-RED stack for Industry 4.0 applications on Bosch Rexroth ctrlX Automation](https://flowfuse.com/blog/2023/09/bosch-rexroth-announce/) - a fully supported Node-RED stack for Bosch customers. - [FlowFuse is now available on the AWS Marketplace](https://flowfuse.com/blog/2023/08/aws-marketplace-announce/) - making it easier to run FlowFuse on the AWS cloud. - [Our Open Source offering is a tier, not our competition](https://flowfuse.com/blog/2023/08/open-source-is-a-tier-not-competition/) - some insight from our CEO into our open source strategy. - [Why the Automation Pyramid blocks digital transformation - The Role of Unified Namespace](https://flowfuse.com/blog/2023/08/isa-95-automation-pyramid-to-unified-namespace/) - some insight from the FlowFuse product manager into the role of a Unified Namespace. ## From the Community - A new plugin for the generation of [Matter devices witin Node-RED](https://flows.nodered.org/node/@node-red-matter/node-red-matter){rel=""nofollow""} has been published. ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} - [Developer Relations Engineer - Manufacturing & Industrial Automation](https://boards.greenhouse.io/flowfuse/jobs/4958271004){rel=""nofollow""} # Dynamic Markdown, Tables & Notebooks with Dashboard 2.0 Whilst we're still busy backporting through the existing Dashboard 1.0 features, we did want to highlight some new features we've built in Dashboard 2.0 released this week. In our v0.4.0 release, we've introduced a new "Notebook" layout, alongside a new Table widget. The Notebook layout is designed to allow users to create Dashboards structured like a Notebook (most often seen with the likes of [Jupyter Notebooks](https://jupyter.org/){rel=""nofollow""} or [ObservableHQ](https://observablehq.com/){rel=""nofollow""}). Here we will deepdive into the Notebook layout, and show how, alongside our new **Markdown Node** ([docs](https://dashboard.flowfuse.com/nodes/widgets/ui-markdown.html){rel=""nofollow""}), **Table Node** ([docs](https://dashboard.flowfuse.com/nodes/widgets/ui-table.html){rel=""nofollow""}) and others, it's becoming easier to create dynamic and interactive Dashboards. *Note: If you're not familiar with Markdown, it's a simple markup language that allows you to format text. You can learn more about it [here](https://www.markdownguide.org/cheat-sheet/){rel=""nofollow""}.* ## Dashboard Hierarchy As a quick introductory note ahead of our below guide, each Dashboard is structured accordingly: - **Widget**: An individual functional block, e.g. button, chart, slider - **Group**: A collection of widgets that render together - **Page**: A single page/tab in your Dashboard. Each page can have it's own Layout, in this case we'll use "Notebook" - **UI**: Contains a collection of pages, deployed from Node-RED, provides the basic side navigation to switch between Pages. ## Building a Notebook ![Example Notebook created in Dashboard](https://flowfuse.com/blog/2023/09/images/db-notebook-example.png) To get started, drop your first widget (in this case, we'll add a `ui-markdown`) onto the Node-RED canvas. This in turn will prompt us to create our first Group/Page/Dashboard which we can name and configure accordingly. Let's add the following Markdown to our first widget: ```md # Markdown Content Here we can render dynamic Markdown content that is easily _styled_. We can inject `msg.payload`. For example, here is a timestamp updating every second: {{ msg.payload }} ``` The joy of `ui-markdown` in Dashboard 2.0 is *dynamic* content, i.e. content that can be updated by passing messages to the `ui-markdown` node. We can wire an `inject` node, set it up to repeat every second, and connect it to `ui-markdown`. Now, our Markdown content will automatically update show this value. ![Screenshot to show how an inject node can drive content of a ui-markdown node](https://flowfuse.com/blog/2023/09/images/db-notebook-inject.png) Resulting in: :video{ariaLabel="Dynamic markdown with an updating timestamp every 1 second" autoPlay="true" height="318" loop="true" muted="true" playsInline="true" preload="none" width="1314"} ## Adding More Widgets Because the Notebook is *just* a layout, we can still wire together any of the available widgets and existing nodes and display them accordingly. Let's wire a `ui-button`, HTTP Request, and `ui-table` node. When we click the button, it will perform the HTTP request, and then render the response in the table. For this, we're going to use the Random Jokes API, and in particular, a call to `https://official-joke-api.appspot.com/jokes/ten` which will return 10 random jokes. ![Screenshot showing a simple Button > HTTP Request > Table flow](https://flowfuse.com/blog/2023/09/images/generate-jokes-flow.png) We can also re-order the widgets on the page using the Dashboard 2.0 sidebar (as you could in Dashboard 1.0). ![Screenshot to show how an inject node can drive content of a ui-markdown node](https://flowfuse.com/blog/2023/09/images/db-notebook-order.png) The above effort results in the following output in our Notebook: ![Screenshot to show how an inject node can drive content of a ui-markdown node](https://flowfuse.com/blog/2023/09/images/db-notebook-jokes-table.png) ## What else is new in 0.4.0? The above demonstrates just a few of the new features in the 0.4.0 Release, but we've also added [other fixes and improvements](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.4.0){rel=""nofollow""}. In particular, I want to call out Steve's great work on implementing custom class injection, the first of our new ["Dynamic Properties"](https://dashboard.flowfuse.com/user/dynamic-properties.html){rel=""nofollow""}, of which there will be more (e.g. visibility, disabled, etc.) to come. As always, thanks for reading and your interested in Dashboard 2.0. If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} # Share & Preview Flows on flows.nodered.org For years, Node-RED's website has provided functionality to share flows through [flows.nodered.org](https://flows.nodered.org){rel=""nofollow""} This week, we at FlowFuse have contributed a new feature to the site that allows users to visually preview those flows, and embed/share those flows in articles and on forum posts. ## Visual Flow Previews A huge thank you for this work needs to go Gerrit Riessen's work published on his [Open Mind Map Blog](https://blog.openmindmap.org/){rel=""nofollow""}. He recently open-sourced some great work to GitHub ([repo](https://github.com/gorenje/node-red-flowviewer-js){rel=""nofollow""}), and with some adaptation and collaboration, we've been able to utilise this as a foundation for the functionality we've added into the flows site. Adding this to [flows.nodered.org](https://flows.nodered.org){rel=""nofollow""} will make it far easier to learn how others use Node-RED, and to share your own flows with others too. The embedding functionality should also make talking about Node-RED in your own articles & forums much easier. ### Example: Simple Flow Here's a demonstration of a simple `Inject` > `Debug` node: :iframe{allow="clipboard-read; clipboard-write" height="200px" src="https://flows.nodered.org/flow/500ee13719e54e42493c8ec96fa733b6/share?height=100" style="border: none;" width="100%"} ### Example: Subflows, Groups, Links & Switches Here's a non-functional flow that just demonstrates how FlowViewer renders the range of node types available in Node-RED: :iframe{allow="clipboard-read; clipboard-write" height="500px" src="https://flows.nodered.org/flow/82a8602b615740491d30c083e5292e5f/share" style="border: none;" width="100%"} ## Sharing & Embedding Flows Any flow on [flows.nodered.org](https://flows.nodered.org){rel=""nofollow""} now has a `Share Flow` option in the `Actions` section on the right side of the flows page. Clicking this will provide you with an iframe like: ```html ``` Which you can paste/embed into any website or blog post. Nick has also [enabled the Node-RED forums to support these embeds too](https://discourse.nodered.org/t/previewing-flows-on-the-flow-library/){rel=""nofollow""}, and is also how we've embeded the above flows too. If you want more control over the sizing of the viewer, you can also include a `?height=` query parameter on the `src` value of the `iframe`. You may also need to hardcode the `height` property of the `iframe` itself to account for this change, depending on where you're embedding it to. For example: ```html ``` We know it's still not perfect, and there's plenty more we can do with it, but hopefully this is a welcome contribution to the Node-RED community. # Modernize your legacy industrial data Industrial systems generate valuable data, but legacy protocols like Modbus or non-IIoT standards often make it hard to use. Bridging this gap is essential to connect traditional systems with the modern Industrial Internet of Things (IIoT). Whether it’s Modbus registers, serial communication without a protocol, or standards like Siemens S7 and Mitsubishi MC-Protocol, the challenge lies in making sense of this raw information. With tools like Node-RED and the node-red-contrib-buffer-parser, you can turn complex, outdated data streams into usable formats that power IIoT innovation. ### Legacy Industrial Data: Modbus Modbus is one of the most widely used industrial protocols. Originally developed in the late 1970s, it has been a popular choice for industrial communication ever since. However, its data format can be challenging to work with in the context of modern IIoT applications. Modbus typically represents data in 16-bit unsigned registers, making it necessary to convert this data into more usable formats like Signed integer, Float, Signed and Unsigned Long, String, or even individual bits. ### A short primer on data types Before we dive into how to make sense of Modbus data, let's take a quick look at some of the common data types we have to deal with. #### 16-bit unsigned 16-bit unsigned data is an integer that can only be positive. It can represent values from 0 to 65535. For example, the number 12345 is represented as `0x3039` in hexadecimal or `0011000000111001` in binary. #### 16-bit signed 16-bit signed data is an integer that can be positive or negative. It can represent values from -32768 to 32767. For example, the number -12345 is represented as `0xCFC7` in hexadecimal or `1100111111000111` in binary. #### 32-bit data 32-bit data, like 16-bit data can mean many things. It could be signed or unsigned, or even a floating point number. For example, the number 12345 is represented as `0x00003039` in hexadecimal or `00000000000000000011000000111001` in binary. Typically, 32-bit data is represented as two 16-bit registers. Therefore, when dealing with 32-bit data, you need to combine two 16-bit registers to get the full value. #### Endianness Endianness, particularly in the context of data communications, refers to the order of bytes and how they are stored or transmitted. There are two types of endianness: big-endian (BE) and little-endian (LE). In big-endian, the most significant byte is first, while in little-endian, the least significant byte is first. For example, the number 12345 is represented as `0x3039` in a big-endian word and `0x3930` in little-endian word. This can often cause confusion and complicate the process of converting Modbus data into more usable formats. ### Node-RED and node-red-contrib-buffer-parser to the rescue Node-RED is an open-source flow-based development tool for visual programming. It's particularly well-suited for IIoT applications because of its versatility and extensive library of nodes. One such node, `node-red-contrib-buffer-parser`, provides a solution to the legacy data conversion challenge. This powerful Node-RED module allows you to parse a Buffer of bytes or an Array of integer data (which, by no coincidence, the popular module `node-red-contrib-modbus` outputs), and convert it into various data types. It can output pretty much any data type, including byte-swapped data, WORD swapped data, masked/shifted/scaled data, and even individual bits. Here's a quick overview of how it works: 1. **Data Parsing**: Start by setting up a Modbus READ node in Node-RED to retrieve data from your industrial device. Then, use the buffer parser node to parse the Modbus data. 2. **Data Conversion**: With the buffer parser, you can easily convert the 16-bit unsigned data into more meaningful formats. Whether you need to translate it into Float, Long, String, or even extract specific bits, this tool makes the process straightforward. 3. **Publishing to MQTT, influxDB, a dashboard, an IIoT system**: Once your data is in a usable format, Node-RED enables you to publish it to many places. MQTT (Message Queuing Telemetry Transport), a popular protocol for IIoT communication is a perfect example. This makes your data accessible to other IIoT systems and applications for further analysis and action. ### Unlocking the Potential of Legacy Data By leveraging Node-RED and buffer parser, you can bridge the gap between legacy industrial protocols and the IIoT world. This means you can extract valuable insights from your existing infrastructure without the need for costly hardware upgrades or replacements. In the era of the Industrial Internet of Things, making sense of your industrial data is no longer a daunting challenge. With the right tools approach, you can unlock the full potential of your legacy data and drive efficiency, productivity, and innovation in your industrial processes. Yey! ### 3 quick demos of Node-RED and the buffer parser node in action Here are 3 quick demonstrations that barely scratch the surface of possibilities: #### Example 1: Modbus to MQTT Converting an array of 16-bit unsigned integers to String, Float and a scaled integer and passing them to an MQTT broker in 4 nodes! :video{ariaLabel="Legacy data to MQTT" autoPlay="true" height="376" loop="true" muted="true" playsInline="true" preload="none" width="1575"} #### Example 2: Modbus to InfluxDB Converting an array of 16-bit unsigned integers to String, Float and a scaled integer for publishing to influxDB! ![Legacy data to influxDB](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-to-influx.png)![Legacy data to influxDB2](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-to-influx2.png) #### Example 3: Modbus data on a dashboard Converting an array of 16-bit unsigned integers to String, Float and a scaled integer for publishing to a dashboard! ![Legacy data to dashboard](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-to-dashboard.png) ### Simplify Your Node-RED Operations with FlowFuse While Node-RED is a fantastic tool for data collection, transformation, and analysis, integrating it into a production environment can sometimes feel like navigating a maze. Whether you’re deploying Node-RED on a server, ensuring secure remote access for your team, or managing a sprawling network of thousands of instances, it’s easy to feel overwhelmed. That’s where FlowFuse steps in to make your life easier. FlowFuse is designed to tackle these challenges head-on. It enhances Node-RED with features that simplify collaboration, strengthen security, and provide scalable deployment options. Imagine having a robust system that not only keeps your Node-RED applications running smoothly but also scales effortlessly with your needs. With FlowFuse, you gain access to a comprehensive suite of production-ready [features](https://flowfuse.com/platform/features/) designed to streamline your Node-RED workflows and boost overall performance. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and experience FlowFuse's features** ### Learn More We will be publishing follow-up blog posts with more details, best practices and examples on how to use Node-RED to make sense of your industrial data. In the meantime, you can learn more about these tools by visiting the following links: - [Node-RED blog posts](https://flowfuse.com/blog/node-red/) - [Node-RED videos](https://www.youtube.com/playlist?list=PLpcyqc7kNgp09XeRx_cae1fEIOloPqM1C){rel=""nofollow""} - [Buffer Parser Node](https://flows.nodered.org/node/node-red-contrib-buffer-parser){rel=""nofollow""} # Modernize your legacy industrial data. Part 2. In [part 1 of this series](https://flowfuse.com/blog/2023/09/modernize-your-legacy-industrial-data/), I introduced the topic of working with legacy industrial data from the likes of Modbus and older, non IIoT protocols and putting it to work in an IIoT world. We looked at some of the challenges and how Node-RED with `node-red-contrib-buffer-parser` node can help. In this article, I will dive a little deeper into the topic and discuss some of the finer details. I hope to demonstrate a smarter approach that can make a huge difference to data accuracy, performance and maintainability while significantly reducing developer time. Not only that, ending with a no-code solution. ## Obtaining Industrial Data In order to convert the legacy data to a format more suited to IIoT we first need to *grab* that data. Node-RED has core nodes that can help you and many more contribution nodes exist that provide access to a wide range of industrial devices. To give you an idea, `node-red-contrib-modbus`, `node-red-contrib-s7comm`, `node-red-contrib-omron-fins`, `node-red-contrib-mcprotocol`, `node-red-contrib-df1` and `node-red-contrib-cip-st-ethernet-ip` are just some of the PLC data access nodes available. But getting the data is just the beginning, it's the methods and considerations you need to make that can make the difference between success and failure. Read on... ## Data consistency An often overlooked aspect of working with legacy industrial data is the consistency of the data being read. In the context of this article, consistency means that the multiple values that make up a related data set are read in a way that they are all valid to one another at the point in time it was read. Let's take a look at a simple example. We have a process PLC recording production metrics and wish to get this data from its Modbus interface for reporting and decision making. The PLC has 5 values that we need to read: | Register | Value | Description | Data Type | | -------- | ----- | --------------------- | ---------- | | 1, 2 | 10 | Part Count | UINT32 | | 3, 4 | 2.5 | Cycle Time (sec) | UINT32/100 | | 5, 6 | 30 | Production Time (sec) | UINT32/100 | | 7, 8 | 20 | Run Time (sec) | UINT32/100 | | 9, 10 | 11 | Stoppage Time (sec) | UINT32/100 | In the above data sample, we can see the total production time is 30 seconds and the run time is 20 seconds. The expectation is that the Stoppage Time should be 10 seconds. However, as we can see, Stoppage Time in this sample is 11 seconds. That is because this data is not consistent. ### Why is the data inconsistent? The most common reason for the data inconsistency is that the data is being read from the PLC while the PLC is running. The data is changing as it is being read. Typically this happens when a developer, unfamiliar with the protocol or end device, begins by reading the data individually. Here is how this journey might look: *Image 1: individual reads*![image showing 10 individual reads](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-pt2-demo1.png) That is a lot of nodes and a lot of duplication! What is worse, is that the developer continues down this path and begins converting the data ready for publishing to MQTT. Here is how this might evolve: *Image 2: individual reads with data processing*:video{ariaLabel="Video showing 10 individual reads with data processing" autoPlay="true" height="684" loop="true" muted="true" playsInline="true" preload="none" width="1030"} Yippie! We have the data, it works, we publish it to MQTT, job done. Right? Unfortunately, no. The data is inconsistent. ### So whats the big deal? Inconsistent data is not useful data and errors can get compounded over time. This leads to bad decisions being made and a loss of confidence in the data. Ultimately, this leads to the data being ignored and the opportunities to make improvements are lost. Loss of improvements means loss of money! Not only that, from a developer or maintainers perspective, it has many problems: 1. Lots of error prone manual configuration (yes, I made several mistakes while creating the example) 2. Hard coded register addresses 3. Duplication 4. Inextensible - what if we need to read more registers? 5. Inconsistent data - as discussed above 6. Slow - each read takes time 7. Inefficient use of network bandwidth - each read requires a request and response packet ### How can we make the data consistent? The most obvious solution is to stop the PLC before reading the data. However, I am faily certain your boss will not be super pleased with stopping the manufacturing process. The next best thing is for the PLC to sample and store the data in an internal memory buffer, waiting, unchanging, to be collected. Unfortunately, this too is not always possible either due to limited in-house skills, locked down PLCs or simply because the PLC does not have the memory to store the data. The next best thing to do is to read relative data as quickly as possible and in one block. ### A quick side-bar (timing is ... everything) In many protocols, including Modbus, data must be polled. Each poll, depending on many factors, can take a number of milliseconds. Lets say, on a relatively quiet serial network, a single register poll takes 25 milliseconds (assume 9600 baud, 1 byte/ms, request packet size 8 bytes, response packet size 7 bytes, 10ms latency for the PLC to receive, process and respond to the request) - If we read 10 registers individually, then the time taken to read the 10 registers is 250 milliseconds. - If we actually needed a more realistic number of registers, say 32, then the time taken to read them individually is a whopping 800 milliseconds. In the world of PLCs, that is an eternity. Now, lets look at that from a different approach. If we were to read the 32 registers in one go, the time taken based on the above constants would be 87 milliseconds. That is over 9 times faster than reading the registers individually. The improvements don't stop there either, the number of total bytes transferred on your network is reduced by 84% and, more importantly, the data is consistent since it was read from the same scan of the PLC scan. *Image 3: A comparison of individual reads vs block reads*![A data table comparing polls vs block read](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-pt2-table.png) OK, back to the topic at hand. ## Getting the data in a more consistent way Here is the same example as above but this time we are reading the data in one go: *Image 4: Getting data in one block*![image showing 1 read for 10 registers](https://flowfuse.com/blog/2023/09/images/industrial-legacy-data-pt2-demo1b.png) Note how much simpler this is? Not only that, it is easier to maintain, faster, more extensible and most importantly, the data is consistent. Great, lets move on. ## Processing the data in readiness for IIoT: MQTT Now that we have good data, we need to process it in readiness for IIoT. In this example, we are going to publish the data to an MQTT broker as individual topics. This is a common approach as it allows the data to be easily consumed by other systems and applications. Using `node-red-contrib-buffer-parser` we can easily convert the data into more meaningful formats. The first, instinctive approach is to fan out the data and process it individually: *Image 5: block reads, individual processing*:video{ariaLabel="Video showing 1 modbus poll with individual processing" autoPlay="true" height="636" loop="true" muted="true" playsInline="true" preload="none" width="926"} This may be fine for a handful of registers but it soon becomes unwieldy and unmaintainable. But lets be smarter about this. We know that the data is consistent and we know that we can read it in one go. So, lets process it in one go too: *Image 6: block reads, smart processing, no-code solution*:video{ariaLabel="Video showing 1 modbus poll with smart processing" autoPlay="true" height="621" loop="true" muted="true" playsInline="true" preload="none" width="886"} ## Node-RED in Production Node-RED is a powerful tool widely used in IoT and IIoT industries, including manufacturing, automotive, textiles, and building management. It excels at collecting, transforming, visualizing, and analyzing data. While integrating Node-RED into production environments offers numerous benefits, it often involves complex tasks such as deploying the server, managing security, and ensuring scalability. These initial setup challenges can be overwhelming and time-consuming. FlowFuse simplifies these tasks by providing a unified platform for managing all Node-RED instances. It enhances collaboration, ensures security, and supports scalability, making deployment and management more efficient. With features like [snapshots](https://flowfuse.com/docs/user/snapshots/), team collaboration tools, one-click deployment, and [multi-factor authentication](https://flowfuse.com/docs/user/user-settings/#security), FlowFuse streamlines the process and enhances the operational capabilities of Node-RED in production settings. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and experience FlowFuse's features** ## Wrap up I hope this article has given you some food for thought and some ideas on not only simplifying your journey to IIoT but also the pitfalls to avoid along the way. A parting thought, there are times when the data is not contiguous. There are ways to deal with this too but that is for another day. P.S. I will post the flows used for the examples above in the comments below. If you have any questions or comments, please reach out there too. # Updating our branding across GitHub, npm and Dockerhub Following our rename to FlowFuse last month, we are about to take the next set of steps to complete the rebrand. This time, focussed on the technical assets we produce. Rebranding a company isn't a small undertaking, especially when your company name is also your product name. When we announced our [new name last month](https://flowfuse.com/blog/2023/08/flowforge-is-now-flowfuse/) we prioritised updating the website, our documentation and social media presences. All of the most visible things relating to the company name. But we knew that wasn't the whole job done. The name `flowforge` still appears in the technical resources we use and the artefacts we publish. Changing them is not as simple a task as changing some words on a website, so it has taken a bit more time to get our plans in place for this next step. I wanted to highlight the set of changes we'll be making in the coming days to complete this migration. For the vast majority of users, expecially those using FlowFuse Cloud, these changes will be completely transparent. However, if you are contributing to any of our open source components, or consuming our npm or Docker packages directly, then please read on. There are four areas we need to migrate. ### GitHub Organization As a company everything we do revolves around our GitHub Organization. Our source code, release planning, this website, and far more all live there. Step one of our migration will be renaming the organization to `FlowFuse`, so instead of `https://github.com/flowforge` we will now live at `https://github.com/FlowFuse`. Renaming organizations on GitHub, whilst not something done lightly, is well catered for. Many existing urls should get automatically redirected - so any existing links will still work. We will, of course, do the work to update any urls in our docs. ### NPM package names We publish a number of packages to the public Node.js Package Manager (npm) repository under the `@flowforge` name. After this week's release is done, we'll be updating all of our packages to publish under the `@flowfuse` name and no longer updating the packages under the old name. This will impact anyone who has installed any of our components directly from `npm`. For example, the Device Agent or Node-RED Dashboard 2.0. We will provide specific upgrade instructions for each of the affected components once the move is done. ### Docker Images We publish container images to Dockerhub under the `flowforge` name. Once we've updated our npm package names, we'll also be updating our container tags to use the new name. If you are using our helm or Docker Compose projects, we'll have a new release that will help get you moved over to the new image names. Likewise our Digital Ocean and AWS Marketplace offerings will be updated - and instructions provided for existing users to migrate over. ### FlowFuse Cloud The final step we have to make is to move FlowFuse Cloud over to its new home at `app.flowfuse.com`. We have to co-ordinate the update with all of our customers who use SSO to login to ensure they can continue to access the platform. Once that is done, it will be a seamless transition for everyone. Existing Node-RED instances will continue to use the `*.flowforge.cloud` domain, but then all new instances will use the `*.flowfuse.cloud` domain. # Tulip Operation Calling Event Report This week I attended the Tulip event called [Operations Calling](https://www.operationscalling.com/){rel=""nofollow""} in Boston. Here are some quick observations from the event. For those that might not know [Tulip](https://tulip.co/){rel=""nofollow""}, they are a Boston based company focused on frontline operations in the manufacturing and industrial automation industry. They provide the software to help factory workers be more efficient at their job. Some people refer to this category as MES but Tulip avoids this term since MES can carry a lot of negative baggage from legacy MES vendors. Some quick thoughts on the event: - I would estimate about 300 people attended the event. It was held at Tulip’s office, which is actually in an old Ford factory. The offices and environment were very cool. - There was a large emphasis on Tulip partners and the ecosystem. Tulip knows it needs to be open and play well with others. There were 27 different vendors showing in their partner pavillion. Pretty impressive given the size of Tulip. - Tulip has a no-code environment for setting up their software. However, they realize you also need low-code, like Node-RED, for integrating hardware and software into Tulip. I attended two sessions that discussed how Node-RED is an important part of the Tulip integration and customization strategy. Node-RED was also being used in some of their demos stations. - Tulip talks about ‘citizen developers’ and ‘composable apps’. This is the future of manufacturing. Organizations need to start thinking in terms of smaller applications that do a specific task that can also be rolled up into something bigger at a later time. These small applications need to be developed by the people closest to the problem, not the IT team or a system integrator. - During the closing panel, it was stated that ‘Today, Excel is the most popular digital automation platform used in industry.’ There are a lot of spreadsheets being used to collect and visualize data. However, the industry needs to find a better way. Node-RED is an important part of the solution. The people who can run Excel spreadsheets can easily use Node-RED. However, using Node-RED will let them do even more than what Excel allows them to do. Operation Calling was definitely an event worth attending and I hope to return next year. # What are FlowFuse Blueprints? Starting today, FlowFuse Blueprints are available on FlowFuse Cloud. Additionally, upon request, all our Teams and Enterprise Self-Hosted customers gain access to this collection. But what exactly are FlowFuse Blueprints? ## FlowFuse Blueprints FlowFuse Blueprints aim to make the Node-RED experience more accessible for newcomers, while also offering a treasure trove of fresh ideas for seasoned Node-RED users. When setting up a new Node-RED instance, you now have the option to choose a blueprint tailored for specific use cases. For example, our "ANDON Operator Terminal" blueprint can be selected, and it will automatically configure the Node-RED instance, sparing you the need to start from scratch. While these templates are powerful out-of-the-box, they're also fully customizable, allowing you to tweak them to suit your unique requirements. Ultimately, blueprints speed up the learning curve for new users and expedite the solution-building process for experienced ones. ### How to use Blueprints? All our FlowFuse Cloud users can select a Blueprint directly while creating a new Node-RED instance. Self-hosted customers can request access to our blueprints via a [support ticket](https://flowfuse.com/support/). ## The first three Blueprints In the coming weeks, we'll be releasing a multitude of blueprints tailored for diverse use cases. However, we decided to start with with three foundational manufacturing applications designed with the [Node-RED Dashboard 2.0](https://flowfuse.com/platform/dashboard/). ### ANDON Operator Terminal The Andon Operator Terminal is designed to be at the start of an Andon process, allowing end-users to report any issues with the cell to a supervisor. ![ANDON Blueprint Screenshot](https://flowfuse.com/blog/2023/10/images/ANDON1.png) ### Performance Overview Dashboard The Performance Overview Dashboard Blueprint provides a real-time snapshot of key performance metrics, delivering a comprehensive overview of manufacturing operations for a specific station or entire line. ![Performance Overview Screenshot](https://flowfuse.com/blog/2023/10/images/performance-dashboard.png) ### OEE Calculator If automatic calculations are not feasible, the OEE Calculator Blueprint enables end-users to manually input production data to compute the Overall Equipment Effectiveness (OEE) for a given machine. ![OEE Calculator Screenshot](https://flowfuse.com/blog/2023/10/images/dashboard-data.png) # What are Certified Nodes? We are thrilled to introduce a new feature for our Teams and Enterprise Tier customers - **Certified Nodes for Node-RED**. This new offering is designed to reinforce your flows's robustness by granting you access to Node-RED nodes that stand up to our rigorous quality and security standards. ## What is a certified node? A certified node is a module from the Node-RED library that undergoes a certification process, ensuring it adheres to standards that address three core pillars: **Quality** - Testing phases for each node. - Operational reliability and compatibility. **Security** - Proactive resolution of potential vulnerabilities. - Revocation of certification for nodes falling short on security, with prompt notifications to affected customers. **Support** - Ambitious aim towards effective issue resolution. - Assistance for troubleshooting. ## How to use certfied nodes? Accessing certified nodes is straightforward, they're integrated directly within your Node-RED palette manager, simplifying selection and implementation. All new instances since October 26, 2023, have automatic access to the catalogue. If you want to add Certified Nodes to one of your existing instances, just [contact us](https://flowfuse.com/support/). ![Node-RED palette manager](https://flowfuse.com/blog/2023/10/images/certified-nodes.png) ## What kind of Nodes are included? Our initial roll-out includes a curated assortment of certified nodes. We're continuously expanding our library, with upcoming weeks bringing a wider array of options. Should you find a gap in your desired functionalities, we encourage you to [reach out](https://discourse.nodered.org/c/vendors/flowfuse/24/){rel=""nofollow""}. Your feedback drives our journey forward, influencing the nodes we introduce next. - Linting for Node-RED - Enhanced debugging for Node-RED - FlowFuse Snapshot Plugin - Node-RED Dashboard 2.0 - E-Mail Communication - Base64 converter - Buffer Parser - PostgreSQL - InfluxDB - Omron PLC - MC Protocol (Mitsubishi PLCs) ### What will follow in the coming weeks? - Siemens PLC - Modbus - Mssql - Rockwell and Allen Bradley - OPC-UA - MongoDB - MySQL - WhatsAPP, Telegram, Slack # Innovate from within - Why manufacturing must embrace Citizen Developers In the early days when I was working as a solution architect, I found myself in a peculiar position, observing an intriguing relationship between Operational Technology (OT) and Information Technology (IT) departments. While OT departments struggled to develop digital solutions, the IT departments held an unspoken authority in this domain, leading to a paradoxical dynamic that often seemed to hinder rather than facilitate progress. As a solution architect working within this environment, I found myself uniquely positioned to bridge the gap between these two groups while also helping them find common ground on which they could build successful projects together. To do so effectively, I needed to understand each group's perspective and determine how best they could collaborate without compromising either side's autonomy or objectives. This task was not always easy but ultimately proved rewarding once achieved successfully. ## A Tale of Shadow IT In the best-case scenarios, IT departments considered themselves as vendors, and OT, the customers. However, the IT-driven, off-the-shelf solutions seldom proved to be the right fit for the problems at hand. This mismatch often led to the birth of shadow IT systems, solutions that were not sanctioned by IT, yet were deemed necessary to maintain operational efficiency. In this realm of shadow IT, I've observed countless solutions running on desktop PCs, hidden from the eyes of IT departments, solving problems in manufacturing quickly, a situation born of necessity, with which no one could be satisfied. I cannot deny that Node-RED, an open-source low-code programming tool, has very often been one of these shadow IT systems, enabling data modification, lightweight HMI creation, and empowering the OT workforce. However, this under-the-radar approach is fraught with dangers. With unprotected systems and without official oversight, the potential for security breaches is a real and present danger. I would wager that even today, a simple scan on port 1880 would reveal a multitude of open Node-RED environments. This double-edged sword of innovation and insecurity highlights the urgent need for a paradigm shift. ## Why are Citizen Developers Important in Manufacturing? Citizen developers are individuals who create applications and software solutions for their organizations without any background in computer programming. These citizen developers leverage low-code platforms to build apps that automate tasks, streamline processes, and improve existing systems' user experiences. Citizen developers are a growing trend in the industry, serving as a key factor in harmonizing IT and OT interests. They bring an array of benefits that can make businesses more efficient and cost-effective. From reducing development costs to increasing productivity, citizen developers offer significant value to organizations seeking ways to stay competitive. 1. **Acceleration of Software Development:** Low-code platforms significantly expedite the software development process, enabling more efficient realization of business requirements. By quickening development, these platforms free up time and resources for design and innovation, leading to higher-quality software that better serves the business's needs. 2. **Crafting Bespoke Applications:** By empowering non-professional coders with the right tools and resources, organizations can democratize digital solutions. This not only facilitates the creation of custom systems that fit seamlessly into existing infrastructures but also enables rapid ideation and development, even for those without prior coding experience. 3. **Enhancement of Operational Processes and Customer Experiences:** With the automation capabilities of low-code platforms and the innovative solutions generated by citizen developers, operational processes can be optimized, and customer experiences significantly improved. The synergy between these elements can lead to efficiency gains through automation and the delivery of more effective, customer-centric solutions. This shift toward embracing citizen development within manufacturing represents a unique opportunity where both sides benefit significantly; not only do OT departments gain access to custom applications tailored precisely for their needs, but IT teams can lead guidance for self-empowerment, resulting in greater efficiency through automation. ## Democratize Application Development with Node-RED For the past decade, Node-RED has been a pioneer in low-code tools, democratizing application development in manufacturing. It provides powerful data collection, transformation, and visualization capabilities, simplifying the application-building process. As an open-source tool with no associated costs or licensing fees, Node-RED is [increasingly popular](https://flowfuse.com/blog/2023/03/integration-platform-for-edge-computing/#the-standard-for-edge-computing-and-plcs) among manufacturers seeking to quickly develop custom applications without extensive coding knowledge or experience. FlowFuse takes things one step further, offering organizations a way to officially incorporate and scale Node-RED, making it compliant, governed, and secure within the existing solution landscape. ## The Strategic Imperative of Citizen Development For industry decision-makers, this is a call to action. It is imperative to nurture this growing community within your workforce, providing them with the tools, platforms, and, importantly, the organizational support they require. The empowerment of citizen developers could very well be the deciding factor in your organization's ability to stay competitive, agile, and innovative in a rapidly evolving market. Looking to the future, the success of modern manufacturing lies in its people and their ability to solve problems with the right tools at their fingertips. Citizen development has the potential to break down the barriers. Organizations that recognize and invest in this potential will undoubtedly lead the charge. [One large US manufacturing company has already made this bet](https://flowfuse.com/customer-stories/manufacturing-digital-transformation/), with its project leader describing Node-RED's low-code paradigm as a way to decentralize innovation and let subject matter experts build their own digital systems. # Community News October 2023 Welcome to the FlowFuse newsletter for October 2023, a monthly roundup of what’s been happening with FlowFuse and the wider Node-RED community. ## Quicker Releases for FlowFuse Cloud and New Changelog FlowFuse is committed to delivery new features to our customers and community as fast as possible. We are now rolling out new builds of FlowFuse to FlowFuse Cloud more regularly than the previous monthly release. This will allow FlowFuse Cloud users to benefit from the latest enhancements as they become available. The self-hosted version of FlowFuse will continue to have releases every 4 weeks. To allow FlowFuse Cloud users to keep up to date on the new changes, we have started a [Changelog](https://flowfuse.com/changelog/) to announce newly deployed features. ## Recent Changelog Updates - [DevOps Pipeline with action selection](https://flowfuse.com/changelog/2023/09/devops-actions/) - [Usability improvements to Device Management](https://flowfuse.com/changelog/2023/09/snapshots-devices/) - [Introducing the Enterprise tier](https://flowfuse.com/changelog/2023/09/introduction-enterprise-tier/) - [API Endpoints for DevOps Pipeline](https://flowfuse.com/changelog/2023/09/pipeline-api/) - [Private Node Support](https://flowfuse.com/blog/2023/10/use-private-custom-nodes-with-flowfuse/) ## Upcoming events ### Dashboard 2.0 - Where we are, and what’s next? The next generation of the Node-RED dashboard is starting to mature. Development work on Dashboard 2.0 started earlier this year and amazing progress has been made already. Join Joe Pavitt, leader of the Dashboard 2.0 project, who will demonstrate the new dashboard project and discuss future plans during our October webinar. [Sign-up today](https://flowfuse.com/webinars/2023/dashboard-20/) to join us on October 26. ## From our Blog - [Custom Vuetify components for Dashboard 2.0](https://flowfuse.com/blog/2023/10/custom-vuetify-components-dashboard/) - the new dashboard project allows for custom UI components - [Updating our branding across GitHub, npm and Dockerhub](https://flowfuse.com/blog/2023/09/rebranding-our-components/) - our GitHub, npm and DockerHub locations are now called FlowFuse, due to the rename from FlowForge. - [How ChatGPT improves Node-RED Developer Experience](https://flowfuse.com/blog/2023/09/chatgpt-for-node-red-developers/) - lots of interest in ChapGPT in the Node-RED community. A quick review of different integrations. - [Share & Preview Flows on flows.nodered.org](https://flowfuse.com/blog/2023/09/flow-viewer/) - A new way to visualize flows in web pages. - [Charting REST API Data in a Dashboard](https://flowfuse.com/docs/node-red/integration-technologies/rest/) - A short tutorial on how to gather data from a REST API for a dashboard. - Modernize your legacy industrial data - Two part series on making sense of your industrial data. - [Part 1](https://flowfuse.com/blog/2023/09/modernize-your-legacy-industrial-data/) - [Part 2](https://flowfuse.com/blog/2023/09/modernize-your-legacy-industrial-data-part2/) ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} # Custom Vuetify components for Dashboard 2.0 Vuetify is a library of UI components using Vue. This saves the developers of Dashboard 2.0 a lot of time, but it can also help you, the end-user. As Vuetify is now included, it can be used to include *any* of their components. So in this post we're going to use a few of these to teach you how to use any of them. Let's install the [Dashboard 2.0 package](https://dashboard.flowfuse.com/getting-started.html){rel=""nofollow""} if you want to follow along. When that's done, let's figure out how to build custom components on dashboards. ## Custom components While going through the list of components on [Vuetify](https://vuetifyjs.com/en/components/all/){rel=""nofollow""} there's several examples that aren't natively implemented in Dashboard 2.0. One example we'll use in a dashboard in this post is the [Progress circular](https://vuetifyjs.com/en/components/progress-circular/){rel=""nofollow""} to build a count down timer. The documentation explains which elements one can change, in this case the size and width. Having set those to the values you'd want in your dashboard, the HTML is generated for you, in my case it's: ```html ``` ### Using the template node Like the [template core node](https://flowfuse.com/docs/node-red/core-nodes/function/template/), the dashboard package comes with [a template node of its own](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html){rel=""nofollow""}. If we take the HTML from the Vuetify docs pages and copy it in a template node the spinner will show up on the dashboard. !["Custom widget on Dashboard 2.0"](https://flowfuse.com/blog/2023/10/images/custom-element-dashboard.png "Custom widget on Dashboard 2.0") ## Dynamic templates While a custom element on a page is cool, and shows you can inject arbitrary HTML on a Dashboard, it's even better if we could make the element dynamic. So let's start with a first dynamic element. The quickest way to get that done is have an [`Inject`](https://flowfuse.com/docs/node-red/core-nodes/common/inject/) node output a random number every second. So let's hook up an Inject, with `msg.payload`'s output being a JSONata expression `$round($random() * 100)` to generate a random number. And let's make sure it sends a message every second. Then we need to update the template node to the following snippet: ```html ``` The difference is subtle, but important. Instead of hard-coding the `model-value` to 20, the tag has changed name and it's set to `msg.payload`. The latter makes the value dynamic. Changing `model-value` to `v-model` is due to leaking implementation details of Dashboard 2.0. It uses VueJS to provide, among other features, easy updating of components. If components are dynamic, *always use `v-model`*. This allows VueJS to pick up changes made dynamically. :video{ariaLabel="Progress spinner, random values" autoPlay="true" height="664" loop="true" muted="true" playsInline="true" preload="none" width="462"} ### Finishing the count down timer This is mostly a programmers job, but it's not hard, so let's get to it. A button would be great to reset the timer, and for the sake of this post we can hardcode the deadline to 1m from the button press. When dragging in a button node, connect it to a [change](https://flowfuse.com/docs/node-red/core-nodes/function/change/) node. In the change node set the flow variable `flow.deadline` to the timestamp. The Inject node from earlier needs updating to inject the `flow.deadline`. All that's left is calculating how many seconds passed, and normalizing 60 seconds to the range between 0-100. The complete flow is: ::render-flow ```json [{"id":"ce9bb8f74e3fc934","type":"ui-template","z":"24065a0aadb305e3","group":"8fa772a709ae3316","dashboard":"e5a3f4cdb11e5e3b","page":"5bedf7f49d5a6037","name":"Progress spinner","order":0,"width":0,"height":0,"format":"\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":810,"y":80,"wires":[[]]},{"id":"8f3e6631414aa096","type":"inject","z":"24065a0aadb305e3","name":"Inject deadline","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"deadline","payloadType":"flow","x":140,"y":80,"wires":[["293cd6f9d727fa02"]]},{"id":"bd9032719d24a53d","type":"ui-button","z":"24065a0aadb305e3","group":"8fa772a709ae3316","name":"","label":"Reset","order":0,"width":0,"height":0,"passthru":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"date","topic":"deadline","topicType":"msg","x":170,"y":140,"wires":[["61ef83d8b06ff626"]]},{"id":"61ef83d8b06ff626","type":"change","z":"24065a0aadb305e3","name":"","rules":[{"t":"set","p":"deadline","pt":"flow","to":"","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":140,"wires":[[]]},{"id":"293cd6f9d727fa02","type":"change","z":"24065a0aadb305e3","name":"Secs since reset","rules":[{"t":"set","p":"payload","pt":"msg","to":"($millis() - msg.payload)/1000","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":80,"wires":[["9742da7e74fd3cd2"]]},{"id":"9742da7e74fd3cd2","type":"range","z":"24065a0aadb305e3","minin":"0","maxin":"60","minout":"0","maxout":"100","action":"clamp","round":false,"property":"payload","name":"Seconds to percentages","x":570,"y":80,"wires":[["ce9bb8f74e3fc934"]]},{"id":"8fa772a709ae3316","type":"ui-group","name":"Group Name","page":"5bedf7f49d5a6037","width":"6","height":"1","order":"","disp":true},{"id":"e5a3f4cdb11e5e3b","type":"ui-base","name":"UI Name","path":"/dashboard"},{"id":"5bedf7f49d5a6037","type":"ui-page","name":"Page Name","ui":"e5a3f4cdb11e5e3b","path":"/","layout":"grid","theme":"8240fbe7c09bc81c"},{"id":"8240fbe7c09bc81c","type":"ui-theme","name":"Theme Name","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}] ``` :: # Integrate your own widgets with Dashboard 2.0 With a new release, comes new features for Dashboard 2.0, and the focus of this release has been on improving the developer experience for those building third-party widgets for Dashboard 2.0. Dashboard 1.0 had a hugely popular ecosystem of third party widgets (e.g. `ui-worldmap`, `ui-svg`) and something we've been keen to support is a platform where these widgets (and more) can be built and used within Dashboard 2.0 too. Whilst we can't support the existing Dashboard 1.0 extensions directly (given that we're now VueJS-based, rather than AngularJS), we hope that the framework, documentation and this article, will help springboard the community to build new (and transfer over old) widgets for Dashboard 2.0. ## Building from `ui-template` As with Dashboard 1.0, we've utilised the flexibility of our `ui-template` node here to enable third-party integrations. If you're used the new `ui-template` in Dashboard 2.0 already, you'll know that you can provide raw Vue (HTML) content and it'll render that into your Dashboard. In 0.6.0, we've added *a lot* of new functionality to the guts of `ui-template`, which we can then extend with our third-party widgets. This new functionality includes: - **Custom Dependencies** - Injection of external widget dependencies (e.g. other JavaScript libraries) via ``. - **On Input** - `onInput` defines behaviour of the widget in Dashboard when it receives a message in Node-RED. - **On Load** - `onMounted` defines functionality when a widget first loads in Dashboard. - **Custom Functions** - Define general functions that can be called from within your widget at any point of your choosing - **Extend Built-In Events** - Our built in `send` function can be called within your widget's template, and will send a message back to Node-RED, with any content of your choosing. - **Custom SocketIO Event Handlers** - If you want to extend the communication between Dashboard and Node-RED, you can emit your own SocketIO events from Dashboard, and have respective handlers for those events in Node-RED. We also have plans to expose more of this new functionality to the `ui-template` interface itself within Node-RED, but for now it's mostly available when developing third-party widgets. ## Useful Resources If you're interested in building integrations, then we've also built a couple of resources to help you get started: - [Widget Development Guide](https://dashboard.flowfuse.com/contributing/widgets/third-party.html){rel=""nofollow""} - A guide for how to structure your own widgets, and - [Example Integration (Repo)](https://github.com/FlowFuse/node-red-dashboard-example-node){rel=""nofollow""} - We've open sourced a very simple `ui-example` node that demonstrates how you can build your own widget for Dashboard 2.0, that utilises all of the features highlighted above. ## What else is new in 0.6.0? Whilst we focussed this article on the third-party integrations, we did also squeeze quite a lot more into the 0.6.0 release too with plenty [other fixes and improvements](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.6.0){rel=""nofollow""}, including the separation of the Dash oard 2.0 nodes into a new "Dashboard 2" category in the Node-RED palette. As always, thanks for reading and your interested in Dashboard 2.0. If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} # Embracing Innovation: Build vs Buy in MES Manufacturing companies often struggle to choose between building custom MES solutions or buying ready-made software. Both options have their strengths, but they can fall short of providing the flexibility and control engineers need. A hybrid approach, combining the best of both worlds, is changing the game. In this article, we’ll explore how Node-RED and FlowFuse make it easier to create MES systems that are efficient, adaptable, and perfectly suited to your needs. ## The Hybrid Approach: A Best of Both Worlds Strategy Much like the practice of outsourcing machine installation while specifying detailed requirements, a hybrid approach to MES systems leverages the expertise of SI companies while retaining control over critical aspects of system design. This approach recognizes that automation engineers possess valuable insights into the unique needs of their manufacturing processes. They can specify crucial details such as wire coloring schemes, downtime conventions, Andon board standards, PLC types, and even the code used on PLCs. ## Node-RED: The MES Code's Best Friend Node-RED emerges as a game-changer in this paradigm. It is an open-source, flow-based development tool renowned for its ability to connect everything, from PLCs to relational databases to firewalls, empowering engineers to create MES system logic visually. Node-RED simplifies the process by enabling engineers to drag and drop nodes and wires to define the logic flow. This visual approach not only streamlines development but also enhances the transparency of the codebase. ## The Power of Standardization and Code Clarity One of the primary reasons behind Node-RED's appeal is its ability to standardize code. Just as Ladder Logic and Function Block Diagrams have long been favored for their clarity, Node-RED fosters a coding environment where engineers can easily understand and build upon each other's work. This standardization ensures that MES systems remain in sync with the rest of the plant, making troubleshooting and maintenance more efficient. ## FlowFuse: Bridging the Gap to Deployment When it comes to deploying MES systems, FlowFuse enters the scene as a vital companion to Node-RED. FlowFuse is a deployment platform that seamlessly integrates with Node-RED, allowing for effortless deployment of applications to a customer's environment. Its user-friendly interface makes it easy for automation engineers to manage and scale their MES systems. ## Pioneering the Future: The Strategic Adoption of Node-RED and FlowFuse Embracing Node-RED and FlowFuse in MES system development is not just a technical choice; it's a strategic one. By adopting this hybrid approach, manufacturing companies position themselves as thought leaders in the industry. They demonstrate a commitment to innovation, transparency, and adaptability. ## Elevate Your MES Strategy: Embrace Node-RED and FlowFuse Today In conclusion, the answer to the build vs. buy dilemma for MES systems is not one or the other, it's both. Node-RED and FlowFuse offer a dynamic partnership that empowers manufacturing companies to craft MES solutions that are tailored to their needs while harnessing the expertise of System Integration companies. The call to action is clear: Embrace Node-RED and FlowFuse as the catalysts for your manufacturing innovation journey. For more information about FlowFuse and how it can revolutionize your MES system deployment, visit FlowFuse.com Start building MES systems that are not just efficient but also future-ready, and become a trailblazer in the world of industrial manufacturing. # Service Disruption Report for October 11th, 2023 On October 11th, 2023, we had an issue where users were not able to access the Node-RED editor, recieving a 'Access Denied' error message. This post examines the issue that was hit, the timeline of events and what we've done to resolve it. ## Summary As part of our company rebranding, we planned a migration for our FlowFuse Cloud platform from `app.flowforge.com` to `app.flowfuse.com`. We applied this change in co-ordination with our customers using Single Sign On as it required an update to their configuration to match. This was done on Tuesday October 10th and all confirmed working with those customers. On Wednesday October 11th we received two reports that separate users could not access their Node-RED editors. We quickly identified the issue was related to how the Node-RED editors authenticated users against the platform for non-SSO users. A workaround was identified to ensure users were logged in via the new domain. We then looked at options to mitigate this for other users. We could not roll back the domain name migration as it would have required co-ordinated action with multiple SSO customers - who were not otherwise impacted by this issue. ## Resolution We tested various approaches of adding automatic redirection from one domain to the other. Due to the fact all existing Instances and Devices had the old domain name hardcoded into their settings, we were limited in what we could do here. We ultimately applied a single redirect for `https://app.flowforge.com` to `https://app.flowfuse.com` - without any redirecting of paths beneath either domain. This URL is only accessed by real users when coming to log into the platform. By redirecting at that point in time, it ensures they are logged into the new domain and everything works as expected. For users with active sessions on the old domain, a simple log out and log back in will get them on to the new domain. ## Next Steps Having resolved the immediate issue we looked at how this situation came to happen and why it wasn't caught in our preparation for the migration. We have a staging environment where we verify any changes before they get applied to production. We all have SSO enabled in that environment and a small number of test users without SSO enabled. Our testing had focused on the SSO users which, by virtue of the SSO process, ensured they ended up logged into the new domain. The testing done with non-SSO users was more limited and didn't hit the right combination of having existing log sessions on one or other of the domains to match the scenario hit by our customers. We also identified some items to follow up on around how the existing Instances and Devices handle HTTP redirects. Currently, the Device Agent is not configured to follow redirects. That is a change we have [added to the backlog](https://github.com/FlowFuse/device-agent/issues/182){rel=""nofollow""}. ## Timeline *All times are BST.* **Tuesday October 10th** We updated the platform's primary domain name to `app.flowfuse.com` whilst keeping `app.flowforge.com` active. This was done in co-ordination with our SSO customers who needed to make an update to their SSO configuration at the same time. Both customers reported success following the change. Our own validation demonstrated we could login via our own SSO, access editors, and devices continued to work as before (in particular, device editor and snapshot gathering). **Wednesday October 11th** - **11:50** and **11:57** - we received two support request from a user getting an 'access denied' error when trying to access an editor. - **12:09** - Workaround shared with both customers to login via the new domain first - **12:30** - We applied a blanket redirect for `app.flowforge.com` to `app.flowfuse.com`. - **12:50** - We then reduced the scope of the redirect so that devices would not be impacted - **13:14** - A secondary issue with logging into the editor when logged in on the new domain was reported internally. - **13:44** - Reverted all of the redirect handling whilst reviewing the problems with the previous redirects. - **14:20** - We applied a redirect to just the root of the domain and documented for our support channel No further reports were received after this time. # How to Use Private Custom Nodes in FlowFuse? With version 1.12 of FlowFuse, it is now possible to use your custom nodes. In this article, we'll explain how to do that. ::div{.blog-update-notes} **UPDATE:** Since this article was published, we've made this even easier on FlowFuse! Now, FlowFuse includes a private registry for all Team and Enterprise Tier customers, so there is no need to host and manage your own. You can view our documentation on this feature [here](https://flowfuse.com/docs/user/custom-npm-packages/) :: What do we mean by custom nodes? Typically, Node-RED nodes are hosted publicly on the npmjs registry, making them accessible to everyone for download and contribution. However, there are use cases where you may not want to share your developed nodes publicly. In such scenarios, it becomes necessary to run your own private Node-RED catalog and npm repository. This approach allows you to manage your custom nodes securely and efficiently. ## Step 1 - Setting Up a Private npm Repository Before you can use custom nodes, you'll need a place to store them. ### Option 1 - Service Provider Choose a public service provider, like [npmjs](https://www.npmjs.com/){rel=""nofollow""}, that allows you to host private packages and upload your node module. ### Option 2 - Verdaccio Another option is to use Verdaccio, a lightweight private npm proxy registry that allows you to run your own registry. #### Installing Verdaccio 1. Install Verdaccio using npm: ```sh npm install -g verdaccio ``` 2. Run Verdaccio: ```sh verdaccio ``` This will start Verdaccio on `http://localhost:4873` #### Configuring Verdaccio The default configuration supports scoped packages and allows any user to access all packages, although only authenticated users can publish. If necesarry you can edit the Verdaccio configuration file, usually found at **\~/.config/verdaccio/config.yaml**. Refer to the [documentation](https://verdaccio.org/docs/configuration/){rel=""nofollow""} for all configuration options. It is important that if you intend to use a private NPM registry with FlowFuse Cloud, the registry will need to be publicly exposed to the internet. Please make sure you understand how to secure it appropriately. #### Publish your package 1. Create a user ```sh npm adduser --registry http://localhost:4873/ ``` 2. Publish you package ```sh npm publish --registry http://localhost:4873/ ``` ## Step 2 - Creating Your Private Node-RED Catalog There are several ways to generate your own `catalogue.json`, which is necessary for Node-RED to understand which packages are available where. Below, we'll show you two of the many options to create and host a `catalogue.json`. ### Option 1 - Web App To create and host a Node-RED catalog, we recommend the package [`node-red-private-catalogue-builder`](https://github.com/hardillb/node-red-private-catalogue-builder){rel=""nofollow""}. The container accepts the following environment variables: - PORT - Which port to listen on (defaults to 3000) - HOST - Which local IP Address to bind to (defaults to 0.0.0.0) - REGISTRY - A host and optional port number to connect to the NPM registry (defaults to http\:/ registry:4873) - KEYWORD - The npm keyword to filter on (defaults to Node-RED) **It presents 2 HTTP endpoints** - /update - a POST to this endpoint will trigger a rebuild of the catalogue - /catalogue.json - a GET request returns the current catalogue The `/update` endpoint can be used with the Verdaccio [notification](https://verdaccio.org/docs/notifications){rel=""nofollow""} events to trigger the catalogue to automatically when nodes are added or updated. ```yaml notify: method: POST headers: [{'Content-Type': 'application/json'}] endpoint: http://localhost:3000/update content: '{"name": "{{name}}", "versions": "{{versions}}", "dist-tags": "{{dist-tags}}"}' ``` ### Option 2 - Node-RED You can also use a FlowFuse Node-RED instance and the [`node-red-contrib-catalogue`](https://flows.nodered.org/node/node-red-contrib-catalogue){rel=""nofollow""} package to generate and host your `catalogue.json` file. :iframe{allow="clipboard-read; clipboard-write" height="100%" src="https://flows.nodered.org/flow/1f01a92fdbd4172c75fcb88b44e64954/share" style="border: none;" width="100%"} ## Step 3 - FlowFuse configuration Next, you'll need to add all the details to your FlowFuse instance configuration. 1. Add the Catalog: Go to your **Instance** -> **Settings** -> **Palette**. Here, you'll have the option to add a catalogue.json. You'll need to provide the URL from which the catalogue.json can be accessed. For example: **{rel=""nofollow""}** It is import to remember that this URL must be accessible from the browser running the Node-RED editor and when used with FlowFuse (or any other Node-RED editor accessed via HTTPS) it must be served with HTTPS. 2. Modify the npmrc File: You'll need to configure where to find the packages from the catalog, possibly specifying a scope. ```text # Set a new registry for a scoped package @myscope:registry=https://mycustomregistry.example.org ``` If necessary, set authentication-related configurations. See the [documentaion](https://docs.npmjs.com/cli/v9/configuring-npm/npmrc#auth-related-configuration){rel=""nofollow""} for details. 3. Save and Restart Your Node-RED Instance: The new npm modules should now be visible in the Node-RED Palette Manager. # Integrate with ChatGPT Assistants with Node-RED ## Introduction to the World of GPTs and AI Assistants In the ever-evolving landscape of artificial intelligence, Generative Pre-trained Transformers (GPTs) have emerged as groundbreaking tools. These advanced AI models, developed by OpenAI, are capable of understanding and generating human-like text, offering vast possibilities across numerous applications. GPTs learn from various internet texts, enabling them to respond to queries with human-like understanding. Among the most intriguing developments in this field are AI Assistants. These are specialized applications of GPTs, accessible through an API, designed to enhance and streamline various tasks. Tasks that include code interpreter, functions, retrieval, and leveraging uploading files to interact with. Unlike traditional GPTs, which primarily focus on generating text, AI Assistants can interact, comprehend, and assist in real-time, making them invaluable in industries ranging from manufacturing to finance to healthcare. [TLDR: Give me the Flows](https://flows.nodered.org/flow/073548c276832e804f037f3212014e60){rel=""nofollow""} ## Node-RED and AI Assistants The integration of Node-RED with AI Assistants brings a unique set of advantages. By leveraging Node-RED's user-friendly platform, developers and citizen developers can easily harness the power of AI Assistants. This integration allows for creation of bespoke solutions tailored to specific industry needs, ranging from automated customer service to advanced data analytics. The real-world impact is substantial – imagine a manufacturing line where real-time data is seamlessly integrated with a prescriptive AI-driven decision-making prompt, enhancing efficiency and reducing downtime. In healthcare, it provides patients with real-time updates to their personal data and provides contextual information, while in retail, it could enhance customer engagement through personalized interactions. The future shaped by these technologies is one where automation and intelligence converge, leading to unprecedented levels of efficiency and innovation in various sectors. ## Experience the Integration Firsthand We invite you to explore the possibilities firsthand. Try out the flows we've created and share your feedback. This is your getting started package. In the provided flows, you can do the following: ![OpenAI Assistant integration on Node-RED](https://flowfuse.com/blog/2023/11/images/ai-flows.png) 1. **Create Assistant**: This flow creates a new assistant. It starts with an inject node that sets the assistant's name, instructions, tools, and model. The HTTP request node then sends a POST request to the OpenAI API to create the assistant. The assistant's ID is stored in the flow context for later use. 2. **List Assistants**: This flow lists all the assistants that have been created. It starts with an inject node that triggers the flow. The HTTP request node sends a GET request to the OpenAI API to retrieve the list of assistants. The results are then displayed in the debug node. 3. **Delete Assistant**: This flow deletes an assistant. It starts with an inject node that sets the assistant's ID. The template node constructs the URL for the HTTP request node, which sends a DELETE request to the OpenAI API to delete the assistant. The results are then displayed in the debug node. 4. **Adjust Assistant Instructions and Models**: This flow adjusts the instructions and model of an assistant. It starts with an inject node that sets the assistant's ID, new instructions, and new model. The change node prepares the payload for the HTTP request node, which sends a POST request to the OpenAI API to update the assistant. The results are then displayed in the debug node. 5. **Create Thread and Run**: This flow creates a new thread and runs it. It starts with an inject node that sets the assistant's ID and the message to be sent. The subflow node then handles the creation of the thread, sending of the message, and retrieval of the response. The results are then displayed in the debug node. How do you envision leveraging this integration in your day-to-day operations or within your industry? Your insights are valuable in shaping the future of our industry. Begin your journey [here.](https://flows.nodered.org/flow/073548c276832e804f037f3212014e60){rel=""nofollow""} ## Embracing the Future of AI and Automation Integrating Node-RED with OpenAI's Assistants is a testament to the ever-evolving landscape of technology. It represents a step towards a future where powerful AI tools are within reach of a wider audience, enabling the creation of bespoke, flexible, and resilient applications across industries. By embracing this integration, we open doors to innovation and efficiency previously unimagined. *Always consult with management before uploading company data to public services like ChatGPT.* # Node-RED Builder a GPT (Alpha) by FlowFuse When ChatGPT was first released, my expectations were quite low. I had grown accustomed to the usual industry buzz for AI and ML that often led to underwhelming solutions. Naturally, I approached ChatGPT with similar reservations. It wasn't until a few months after its announcement that I decided to give it a try. To my surprise, within just 10 minutes, I found myself so captivated that I decided to purchase the pro version. On November 6th, OpenAI unveiled a new offering: GPTs. These function as custom ChatGPT environments, allowing the author to provide additional context, giving it a specific and focused purpose. With new content emerging daily for both Node-RED and FlowFuse, the ability to update and provide essential documentation became increasingly valuable. ChatGPT is already a fantastic tool for building Node-RED flows, and if you haven't tried it yet, I highly recommend giving it a go. Now, let me introduce you to Node-RED Builder, a preconfigured environment where all the necessary prompts are already set up to ensure your success. Furthermore, the latest knowledge on Node-RED and FlowFuse is readily available within the GPT, allowing you to tap into the most up-to-date documentation for your prompts. Node-RED Builder streamlines the development of Node-RED flows, making it more accessible, especially for those new to this environment. We've even provided context to emphasize the use of default nodes over function nodes. Imagine being able to simply drag and drop elements, connect nodes, and create functional flows without delving deep into complex coding. This is precisely what Node-RED Builder makes easier, effectively opening the doors of Node-RED to a wider audience. [Access to the GPT - Node-RED builder by FlowFuse](https://chat.openai.com/g/g-V5Kyn4omE-node-red-builder-by-flowfuse-v1-0-2){rel=""nofollow""} # Community News November 2023 Welcome to the FlowFuse newsletter for November 2023, a monthly roundup of what’s been happening with FlowFuse and the wider Node-RED community. ## FlowFuse Team Summit - Barcelona by Grey The first week of November in Barcelona, the FlowFuse team embraced a blend of culture, collaboration, and creativity at our biannual summit. As a global remote-first company, these gatherings are key to nurturing the camaraderie and clear communication that fuels our work. My first summit was a vibrant tableau of wit and good-natured sarcasm, a perfect fit for my sense of humor. But it wasn't just about the fun; we delved deep into aligning our strategies to streamline communication with the vast community that recognizes the value of Node-RED. From segway tours through historic streets to culinary competitions, we bonded and built memories. During the day, we crafted go-to-market strategies, fortified our team dynamics through board games, and honed our sales approaches. The latest dashboard visualizations and product roadmaps were discussed, with our shared vision to simplify the complex for companies worldwide. **“The easy things should be easy, and the hard things should be possible.”** Our goal remains steadfast: to enhance bidirectional communication with you, our valued community, and make Node-RED's power more accessible than ever. ![](https://flowfuse.com/blog/2023/11/images/IMG_6334.jpg){width="500"} ## Announcements TLDR We're excited to share some significant enhancements that will elevate your experience with FlowFuse and Node-RED. We've announced that Dashboards 2.0 are in Beta version. Recent additions to Dashboards 2.0 include integrating Vuetify and Mermaid into Dashboard 2.0 unlocking a suite of custom UI components to enrich your dashboards. Our step-by-step guide will walk you through creating dynamic elements like countdown timers, leveraging the power of VueJS for seamless updates. We're also thrilled to introduce FlowFuse Blueprints - a game-changer in building bespoke, flexible, and resilient manufacturing applications. Our initial set of Blueprints, including ANDON Operator Terminal, Performance Overview Dashboard, and OEE Calculator, provide preconfigured Node-RED applications that streamline your development process. Dive into the world of Blueprints and discover how they can accelerate your project's journey from concept to operational reality. Join the [webinar](https://flowfuse.com/webinars/2023/blueprints/) to find out more. ## Recent Changelog Updates - [Integrate your own widgets with Dashboard 2.0](https://flowfuse.com/blog/2023/10/dashboard-integrations/) - [Blueprints](https://flowfuse.com/changelog/2023/10/blueprints/) - [Enhanced Snapshot Selection](https://flowfuse.com/changelog/2023/10/device-snapshot-selection/) - [Device Agent path bug fix](https://flowfuse.com/changelog/2023/10/path-bug-fix/) - [Resource Monitoring in Audit Log](https://flowfuse.com/changelog/2023/10/resource-alerts/) - [Certified Nodes](https://flowfuse.com/changelog/2023/10/certified-nodes/) ## Upcoming events ### FlowFuse Blueprints: Your Pathway to Enhanced Manufacturing Explore building manufacturing applications with FlowFuse Blueprints in our upcoming webinar. Blueprints make it easy to get started building applications with Node-RED. [Sign-up today](https://flowfuse.com/webinars/2023/blueprints/) to join us on November 30th. ## From our Blog - [Meet FlowFuse at SPS Nuremberg](https://flowfuse.com/blog/2023/11/meet-us-at-sps-nuremberg/) - Talk about Node-RED and how FlowFuse can help you operationalize your flows! - [Install the FlowFuse Edge Agent on the Raspberry Pi 5](https://flowfuse.com/docs/node-red/hardware/raspberry-pi-5) - Managing your Raspberry Pi 5 with Node-RED through FlowFuse is easy to set up - [Innovate from within - Why manufacturing must embrace Citizen Developers](https://flowfuse.com/blog/2023/10/citizen-development/) - Empower your Operational Technology teams as Citizen Developers - [Embracing Innovation: Build vs Buy in MES](https://flowfuse.com/blog/2023/10/mes-build-buy/) - Bridging the Gap: Uniting MES Development with Automation System Practices - [What are FlowFuse Blueprints?](https://flowfuse.com/blog/2023/10/blueprints/) - Preconfigured Node-RED Applications - [Integrate your own widgets with Dashboard 2.0](https://flowfuse.com/blog/2023/10/dashboard-integrations/) - With the 0.6.0 Release of Dashboard 2.0, we now support third-party widget integration. Read more in this deep dive. - [What are FlowFuse Blueprints?](https://flowfuse.com/blog/2023/10/blueprints/) - Preconfigured Node-RED Applications ## Join Our Team FlowFuse is expanding our team. Check out the current openings: - [Contract Front-End Engineer – Node-RED Dashboard](https://boards.greenhouse.io/flowfuse/jobs/4911532004){rel=""nofollow""} # Chart Improvements & Migrating to Dashboard 2.0 It's been a little while since we've done an update, since we last posted we've moved into the 0.7.x releases for Dashboard 2.0. With these we're making big strides in improving the UX for charting your data, as well as starting to focus on migration paths from Dashboard 1.0 to 2.0. ## Package Name Changes Firstly a bit of news regarding the `npm` package we publish. Inline with our own [company name change](https://flowfuse.com/blog/2023/08/flowforge-is-now-flowfuse), we've had to update Dashboard 2.0's npm package, and so, we've changed from `@flowforge/node-red-dashboard` to `@flowfuse/node-red-dashboard`. In the short term, we'll be keeping @flowforge available on the Node-RED Palette Manager, but it will be removed soon, and the associated NPM Package will be put into a "deprecated" mode. ### NPM Package Migration Unfortunately, this migration from `@flowforge/` to `@flowfuse/` requires a little bit of manual work, and isn't as easy as just clicking "update" in the Node-RED Editor. In any case, there is no need to update your flow in this migration, you'll just need to uninstall `@flowforge/node-red-dashboard` and install `@flowfuse/node-red-dashboard` instead. #### Running Locally If you're running Node-RED locally, or in your own infrastructure, you'll need to manually uninstall the old package: ```bash npm uninstall @flowforge/node-red-dashboard ``` and re-install the new one: ```bash npm install @flowfuse/node-red-dashboard ``` #### Running in FlowFuse Navigate to your Instance > Settings > Palette, and then change the `@flowforge/node-red-dashboard` entry to `@flowfuse/node-red-dashboard` (the latest version as of this post is `0.7.2`). Restart your instance, and the new package will automatically install. ## Dashboard 1.0 to 2.0 Migration Guide As part of our mission to ensure a smooth transition from Dashboard 1.0 to Dashboard 2.0, we have published a first draft of a [Migration Guide](https://dashboard.flowfuse.com/user/migration.html){rel=""nofollow""}. As a starting point, we have comprehesively covered the Dashboard 1.0 widgets and their associated properties. We've then detailed which properties are already supported, which have partial support, and where appropriate, we do not support and *why* (most of the time, it's just because we haven't got round to it yet!) ![Migration Guide Snippet](https://flowfuse.com/blog/2023/11/images/migration-guide-snippet.png) We fully appreciate that the migration path is not yet complete, and we that we are missing some features and properties, but please know that we are working hard to ensure that as many of the features from Dashboard 1.0 are available in Dashboard 2.0. We will be updating this guide as we progress. We will also be adding "event" and "dynamic properties" sections to the guide, to detail how you can update and control elements via runtime messages (e.g. dynamically change the label of a button), and how this differs (if at all) from Dashboard 1.0. Whilst we aren't quite there yet, this guide offers a comprehensive breakdown on our progress in backporting all of the properties from Dashboard 1.0 ### Automated Script An ambitious plan that we have is to also provide a [Migration Script](https://github.com/FlowFuse/node-red-dashboard/issues/261){rel=""nofollow""}. Any feedback, ideas or concerns are most welcome as comments on the issue. Whilst this will never provide 100% perfect migration, we hope to be able to provide a script that can be run against your flows to automatically convert as much as possible from Dashboard 1.0 to 2.0. In most cases, as you can see in the Migration guide, we match most properties 1:1, so this should do a lot of the heavy lifting for you. ## Updates to UI Chart ### Key Mapping One of the core purposes of Node-RED Dashboard has always been to provide low-code access to charting your data. With the 0.7.x releases we've made some big improvements to the UI Chart node to make it easier to use and more powerful. In Dashboard 1.0, it was common place to have to regularly re-format your own data into `{x, y}` structure to be chart-friendly. In `0.7.0` we've introduced the concept of **key mapping**, where you can specify which keys in your data object should be used for the x and y axes. This means you can now pass in data in the format you want to use, and the chart will do the rest. For example, when rendering a chart of our weekly npm downloads, we have a data structure: ```json [{ "day": "YYYY-MM-DD", "downloads": 128 }, { "day": "YYYY-MM-DD", "downloads": 256 }, { "day": "YYYY-MM-DD", "downloads": 512 }] ``` Rather than having to pipe this into a `function` node and re-map the properties to `{ x, y }`, we can now use the `ui-chart`'s key mapping properties: ![ui-chart-key-mapping](https://flowfuse.com/blog/2023/11/images/ui-chart-keymap-properties.png) Resulting in the following chart: ![ui-chart-key-mapping](https://flowfuse.com/blog/2023/11/images/ui-chart-mapping.png) ### Multiple Lines Note above, another new option has been added to define *"Series"*. In Dashboard 1.0 this was fixed as `msg.topic` at all times, and defined which line/series data points rendered too. Now, this is configurable, and can even be set as a `key:` type too, whereby each data point being treated individually, and grouped based on a given key/property. Another great new feature here is the type `JSON` for this property. We can provide a *list* of series labels, and the chart will render each value from a single data point as separate lines. For example, if we consider the data: ```json [{ "day": "2023-10-23", "temperature": 28, "humidity": 16 }, { "day": "2023-10-24", "temperature": 26, "humidity": 19 }, { "day": "2023-10-25", "temperature": 27, "humidity": 24 }] ``` We can provide a series: `["temperature", "humidity"]` like so: ![ui-chart-key-mapping](https://flowfuse.com/blog/2023/11/images/ui-chart-series-property.png) Which would result in the following plot: ![ui-chart-key-mapping](https://flowfuse.com/blog/2023/11/images/ui-chart-multipoint.png) We appreciate this offers a new way of working with data in Dashboard, but hopefully, once you've tried it, you'll find it much easier to work with, and see the value it brings when working with your own data sets. ## What else is new in 0.7.x? Whilst we focussed this article on the migration paths and new UI Chart features, we did also squeeze quite a lot more into the 0.7.x releases too with plenty other fixes and improvements: - [Re-architecture of Server-side State Management](https://github.com/FlowFuse/node-red-dashboard/pull/279){rel=""nofollow""} - [Y Axis Min/Max Options](https://github.com/FlowFuse/node-red-dashboard/pull/327){rel=""nofollow""} - ["Focus" button for widgets added to Sidebar](https://github.com/FlowFuse/node-red-dashboard/pull/320){rel=""nofollow""} - [No more "blue screen" and improved error reporting](https://github.com/FlowFuse/node-red-dashboard/pull/310){rel=""nofollow""} - [Better route handling](https://github.com/FlowFuse/node-red-dashboard/pull/301){rel=""nofollow""} You can also read the more comprehensive release notes for each release here: - [0.7.0 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.7.0){rel=""nofollow""} - [0.7.1 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.7.1){rel=""nofollow""} - [0.7.2 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.7.2){rel=""nofollow""} ## Follow our Progress As always, thanks for reading and your interested in Dashboard 2.0. If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} # Overhauling the Dashboard 2.0 Build Pipeline As a developer, sometimes you have to hold up your hands and realise something you've spent two weeks building needs to be thrown away and restarted. Having shipped the [third-party widget support for Dashboard 2.0](https://flowfuse.com/blog/2023/10/dashboard-integrations/) in line with Dashboard 1.0's approach, we then had the [feedback](https://github.com/FlowFuse/node-red-dashboard/issues/307){rel=""nofollow""} that the way Dashboard 1.0 did things really wasn't good, and asking us to consider re-building the process to make the developer experience of working with Dashboard 2.0 far more seamless. So, that's exactly what we've done with the `0.8.0` release, amongst a few other things. ::div --- style: "background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;" --- Reminder: all new releases of Dashboard are now under the `@flowfuse`{style="background-color: transparent;"} namespace, so you'll need to update to use `@flowfuse/node-red-dashboard`{style="background-color: transparent;"}, and not `@flowforge`{style="background-color: transparent;"}. :: ## Migrating our Build Pipeline Without getting *too* technical, as part of this work in supporting third-party widgets, we overhauled our build pipeline for Dashboard 2.0. This pipeline is responsible for taking our source code, and compiling it into a format that then gets deployed by Node-RED when running Dashboard. Previously, we used ***Webpack***, but now, we've switched over to ***Vite***. This is a newer build tool, and is much faster than Webpack. It's also what we've now updated out [Example Node](https://github.com/FlowFuse/node-red-dashboard-2-ui-example){rel=""nofollow""} to use too. So now, when working with a third-party widget, Vite builds up all of your code, wraps it into a single `umd.js` file, and Node-RED then serves that file up for Dashboard 2.0 to load in. ![Vite Build Process](https://flowfuse.com/blog/2023/11/images/dashboard-build.png) We've also re-written our ["Building Third Party Widgets"](https://dashboard.flowfuse.com/contributing/widgets/third-party.html){rel=""nofollow""} guide to reflect this change. ## Debugging Dashboard A new feature we've added in `0.8.0` is also for those developing Dashboard's core and third-party widgets. You can now navigate to `/dashboard/_debug` to explore the full configuration that Dashboard receives from Node-RED. This is particularly useful when you're trying to debug why a widget isn't loading, showing the correct data, or generally isn't behaving as you expect. ![Dashboard's Debug View](https://flowfuse.com/blog/2023/11/images/debug-view.png) Note in the above example, where we can see that the `ui-dropdown` has had it's options overriden by `msg.options` on injection. You can read more about the debugging view [here](https://dashboard.flowfuse.com/contributing/widgets/debugging.html){rel=""nofollow""} ## What else is new in 0.8.0? Whilst we focussed this article on the build pipeline overhaul, changes to third-party wdgets and debugging Dashboard, we did also squeeze quite a lot more into the 0.8.0 releases too with plenty other fixes and improvements: - [Dynamic setting of msg.options for UI Dropdown](https://github.com/FlowFuse/node-red-dashboard/pull/345){rel=""nofollow""} - ["Date" type for UI Text Input](https://github.com/FlowFuse/node-red-dashboard/pull/346){rel=""nofollow""} - [Finer grain controls of Text Input event emissions](https://github.com/FlowFuse/node-red-dashboard/pull/365){rel=""nofollow""} - [Control over when UI Slider emits events](https://github.com/FlowFuse/node-red-dashboard/pull/367){rel=""nofollow""} - [Improved documentation for Bar Charts](https://github.com/FlowFuse/node-red-dashboard/pull/364){rel=""nofollow""} You can also read the more comprehensive release notes for the release here: - [0.8.0 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.8.0){rel=""nofollow""} ## Follow our Progress As always, thanks for reading and your interested in Dashboard 2.0. If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} # Tracking Who Has Opened a Dashboard As we continue to add features to the Node-RED Dashboard v2 one feature request that came in was to track which users had visited a Dashboard. Multi user support for the Dashboard is on the backlog but this could be solved with the parts that are currently available. ## FlowFuse Authentication One of the features we offer on FlowFuse is the ability to protect HTTP endpoints and Dashboards using the same FlowFuse user authentication that protects access to the FlowFuse Application and the Node-RED instances. We even offer a specific RBAC 'viewer' role that just allows access to these endpoints but not the FlowFuse application. FlowFuse authentication can be enabled from the Instance Settings page on the Security tab This can be used to secure access to a Dashboard hosted in a Node-RED Instance. At the moment the Dashboard while protected by this authentication, it is not aware of which user is accessing it. But if we include an element in the Dashboard loaded via a HTTP-in/HTTP-response node we gain access to details of the authenticated user. ## Implementation First we will create a HTTP-in/HTTP-response pair to serve up a single pixel SVG image. I chose SVG as it doesn't require creating a binary image file to load. The following flow snippet includes both the HTTP-in/HTTP-response nodes and a change node to set the `msg.payload` to the SVG content and to set the HTTP headers to include the correct mime type. There is also a second change node which extracts the user information. ::render-flow ```json [{"id":"7f22dc81d8192d4d","type":"http in","z":"98c8d7ea66149291","name":"","url":"/tracker","method":"get","upload":false,"swaggerDoc":"","x":210,"y":460,"wires":[["7d36739c02cd04ec","5f4647c97917cce1"]]},{"id":"58fd30516a077e29","type":"http response","z":"98c8d7ea66149291","name":"","statusCode":"","headers":{},"x":630,"y":460,"wires":[]},{"id":"7d36739c02cd04ec","type":"change","z":"98c8d7ea66149291","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":" Sorry, your browser does not support inline SVG.","tot":"str"},{"t":"set","p":"headers","pt":"msg","to":"{\"Content-Type\":\"image/svg+xml\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":460,"wires":[["58fd30516a077e29"]]},{"id":"5f4647c97917cce1","type":"change","z":"98c8d7ea66149291","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"req.session.user","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":520,"wires":[["ddc02b4e9c30c807"]]},{"id":"ddc02b4e9c30c807","type":"debug","z":"98c8d7ea66149291","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":600,"y":520,"wires":[]}] ``` :: Next we need to add the SVG to the Dashboard, this can be done by adding a Template node with the following HTML content. ```html
``` This will load the image every time the Dashboard page loads and hence trigger the earlier flow allowing the user to be logged. ## Linking to SSO Users With the release of FlowFuse v1.14.0 the session object will also include the users email address which is the shared identifier between FlowFuse and the SSO system. This will allow the logging to use a single unified identifier. # Deploying the FlowFuse Device Agent via Balena As part of the FlowFuse Staff Summit this year in Barcelona we met up with Marc Pous from [Balena Io](https://www.balena.io/){rel=""nofollow""}. Balena is a platform for managing fleets of Edge Devices and it felt like the perfect fit for deploying the FlowFuse Device Agent. To do this you install the Balena OS on the devices, this is a stripped down Linux distribution that includes a client that connects back to Balena's platform and creates a VPN tunnel. As well as the Balena client it includes Docker and users can select containers to push to the devices. These Docker container are hosted on Balena's own container registry and are built by doing a git push to Balena's git server. A GitHub repository with all the required files [has been published](https://github.com/FlowFuse/balena-device-agent){rel=""nofollow""}, a one click deploy button to allow you to quickly try this out. ## Building FlowFuse Device Agent for Balena We already build a FlowFuse Device Agent Docker container so it was pretty simple to modify the existing `Dockerfile` for Balena. ```docker FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node RUN mkdir /opt/flowfuse-device RUN npm install -g @flowfuse/device-agent COPY entrypoint.sh /usr/src/entrypoint.sh ENTRYPOINT ["/usr/src/entrypoint.sh"] CMD ["flowfuse-device-agent"] ``` There were 2 main changes from the default Device Agent [`Dockerfile`](https://github.com/FlowFuse/device-agent/blob/main/docker/Dockerfile){rel=""nofollow""} 1. Change the base image to Balena's image, this is because the `Dockerfile` is actually a template that can be used to build images optimized for all Balena's supported hardware platforms (We currently build the FlowFuse Device Agent containers for AMD64, ARMv7 and ARM64) 2. Adding a custom `entrypoint.sh`. This is to ensure that the hostname seen in the container matches the Balena device name, making it easier to match it up with what is seen in the FlowFuse application. It also generates the configuration file from the passed in environment variable (see [below](https://flowfuse.com/#configuring-devices)) As well as the `Dockerfile` there is also a `docker-compose.yml` because Balena applications can be made up of multiple services packaged as container. In this case we just need a single container but the compose file contains all the information about what ports to expose and what volumes need creating to persist state. ## Configuring Devices The FlowFuse Device agent can be configured in 2 ways. 1. You can provide a configuration file that is provided by the FlowFuse application when you create a new Device. This file contains the unique identifiers for the Device and details of where to find the FlowFuse Application. This file can be provided to a Balena device by adding a device specific environment variable as described [below](https://flowfuse.com/#environment-variable). 2. You can provide a fleet of devices with a configuration file that contains details of where to find the FlowFuse application and a Provisioning token. Multiple Devices can all have the same Provisioning token and this will cause them to connect to the FlowFuse application on first start up and create a new Device bound to an existing team (and optionally an Application or Instance). This file can be passed to Balena devices by way of a Fleet wide environment variable as described [below](https://flowfuse.com/#environment-variable). You can create a Provisioning Token file under the Team -> Settings page on the Devices tab. ### Environment Variable Because the `device.yml` file is multi line it needs to be base64 encoded, you can do this with the following ```bash $ base64 -w 0 device.yml ``` You can then use the Balena console to create either a device specific or a fleet wide environment variable called `FF_DEVICE_YML`. ![balena-env-var](https://flowfuse.com/blog/2023/11/images/balena-env-var.png) # Meet FlowFuse at SPS Nuremberg FlowFuse is excited to be exhibiting at the SPS in Nuremberg next week. We will be located in Hall 5 Booth 145. At the SPS, we will be showcasing our latest FlowFuse platform, which is a powerful and user-friendly tool for creating and managing Node-RED flows. There's also an option to get a demonstration of how FlowFuse can be used to solve a variety of industrial automation problems. We are also keen to meet with Node-RED users and FlowFuse prospective customers at the SPS. If you would like to book a meeting with us, please visit our [contact us](https://flowfuse.com/contact-us/) page. We look forward to seeing you at the SPS! ### About FlowFuse FlowFuse is a software company that develops tools for creating and managing Node-RED flows. Node-RED is a popular open-source platform for flow-based programming. FlowFuse makes it easy to create and manage complex Node-RED flows, even for users with no prior programming experience. FlowFuse is used by a wide range of organizations, including manufacturers, utilities, and research institutions. FlowFuse is used to solve a variety of industrial automation problems, such as data acquisition, data enrichment, and process monitoring. ### About SPS Nuremberg SPS Nuremberg is the world's leading trade fair for electric automation. The fair takes place annually in Nuremberg, Germany. SPS Nuremberg is a showcase for the latest innovations in industrial automation. We hope to see you at the SPS! # Beyond Automation - AI Use Cases that are shaping the next manufacturing frontier Are we standing on the brink of a Fifth Industrial Revolution? The manufacturing industry has been in a state of flux for some time, with the rise of automation and digital transforming the way factories operate. But today, we are witnessing something even more profound: AI is pushing manufacturing to a whole new level. Some have even referred to it as “the fifth industrial revolution” due to its potential for disruption. But the question lingers for many plant managers and decision makers: in which AI-powered capabilities should one invest to bring about transformative changes in the manufacturing environment? As we navigate this question, I want to focus on three AI uses that are not only ripe for investment but also pivotal in driving manufacturing success in this competitive market. ## Empowering Citizen Developer Strategy with AI The concept of citizen development stands as one of the most significant fields in my opinion, a sentiment I've detailed in my [previous article](https://flowfuse.com/blog/2023/10/citizen-development/) about Citizen Developers. This approach is revolutionizing the manner in which applications are crafted and deployed across various industries. By empowering individuals, irrespective of their coding knowledge, to create applications, AI is dramatically hastening this process. Investing in AI capabilities that bolster your citizen developer strategy can fast-track application development, offering intuitive, template-driven platforms that employ AI to navigate users through the creation process. As we've seen over recent months and years, AI can significantly assist in code generation, thereby granting your citizen developers an even smoother initiation into application development. An excellent instance of this is the [article and Node-RED Node](https://flowfuse.com/blog/2023/09/chatgpt-for-node-red-developers/) describing the potential for integrating Node-RED with ChatGPT to assist you in building applications. This integration highlights the practical, user-friendly solutions made possible through AI, making the realm of app development accessible to a broader range of innovators. ## Refining Warehouse Management through AI-Driven Demand Forecasting In an era marked by complexities in supply chains and customer demand, AI's role in warehouse management becomes a game-changer. AI algorithms analyze historical data and market trends to predict future demand with astonishing accuracy, a step beyond traditional forecasting methods. For decision makers, investing in AI for demand forecasting means significantly minimizing overproduction or stock outs, optimizing inventory levels, and improving customer satisfaction. The advanced analytics offered by AI not only predict what products are in demand but also when and where they are needed, thereby facilitating strategic planning and resource allocation. ## Elevating Predictive Maintenance and Quality Control Unplanned downtime and quality inconsistencies are two of the biggest profit drains in manufacturing. AI's predictive capabilities are setting new standards in both maintenance and quality control protocols. By continuously monitoring equipment performance and production processes, AI can predict and identify machinery failures before they occur and detect quality deviations in real-time, allowing for immediate correction. Investing here means less downtime, reduced maintenance costs, improved product quality, and ultimately, an enhanced bottom line. ## Your Digital Infrastructure & Architecture is key While understanding where concrete Use Cases are is crucial, it’s equally important to ensure that your digital strategy and architecture can support and quickly adapt to these advanced AI implementations. See also [my article](https://flowfuse.com/blog/2023/08/isa-95-automation-pyramid-to-unified-namespace/) about the Unified Namespace. A flexible system that integrates a Unified Namespace is critical for seamless data exchange across various systems and applications. Moreover, fostering a citizen developer environment is fundamental in ensuring that these AI investments are maximally utilized, empowering your workforce to contribute actively to the company's innovation cycle. # Building a Custom Video Player in Dashboard 2.0 Dashboard 2.0 just got *a lot* more powerful with our new updates to the `ui-template` node. New features added to the node include: - Support for a full Vue component to be defined using the VueJS Options API. - Running of raw JavaScript within ` ``` Some quick gotchas to note: - `
{{ msg }}
` - is an example of how you render variables into the HTML. - `
` - lets you conditionally show/hide content based on a variable. - `
` - lets you loop over an array of items and render them into the HTML. - `
` - lets you bind a method to an event, in this case, when the user clicks on the div. - `
` - `:` is a way to define a "bound" property. In this case, the class `my-class` will be applied when `isActive` is true. - `console.log(this.myVar)` - when you're writing code inside the ` ``` With this functionality in place, we can wire the `ui-template` node to a `debug` node, and see the following when we play/pause the video: ![Example debug output when our custom build video player is played/paused](https://flowfuse.com/blog/2023/12/images/dashboard-video-2.png) #### 2. Remote control of play/pause from Node-RED We can use the built-in `$socket` variable to listen for incoming events from Node-RED. When Dashboard 2.0's nodes receive a `msg` inside Node-RED, they send a `msg-input:` event to the Dashboard client. We can listen for this event and then call the `play()` and `pause()` methods on the video element, depending on any properties of that message, in this case, the `msg.payload.event` value. ```html ``` #### 3. Seeking to a specific point in the video from within Node-RED With the `on('msg-input')` listener in place, we can now extend our handler to handle seeking to a specific point in the video. ```html ``` and with that, we now have a Dashboard 2.0 widget to display a video, that can be controlled from Node-RED, and logs details of user activity back into Node-RED. Other features available with the UI Template are detailed in the online documentation, and include: - [Loading External Dependencies](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#loading-external-dependencies){rel=""nofollow""} - [Running raw JavaScript](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#writing-raw-javascript){rel=""nofollow""} ## Follow our Progress You can also read the more comprehensive release notes for `v0.10.0` release here: - [0.10.0 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v0.10.0){rel=""nofollow""} As always, thanks for reading and your interest in Dashboard 2.0. If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} # Run Node-RED as a service on Windows FlowFuse's device agent allows you to manage and run your Node-RED instances on your own hardware such as a Raspberry Pi or Windows computer. This can be very useful where an application you've written needs to run flows with direct access to hardware sensors. In this article, we're going to explain the steps to configure our device agent to run as a service in Windows using the [nssm](https://nssm.cc/){rel=""nofollow""} utility. ## Why run the device agent as a service? The standard process for running FlowFuse's device agent is to start it on the command line using the command `flowfuse-device-agent`. This works fine for testing but for long-term installations it's useful to run the device agent as a service. Once running as a service, the device agent will continue to run even if you log off or the computer is restarted and no user is logged in. ## Summary The aim of this how-to is to install the FlowFuse device-agent as a service on a Windows computer. There will be two main parts to this: 1. Install the device-agent 2. Setup the device-agent to run as a Windows service Additionally, two user accounts will be needed for this configuration: 1. A **user** account that will be used to run the device-agent (typically, non-admin account) 2. An **admin** account that can run elevated commands and will be used to setup the service We will create a directory for the device-agent files and set the permissions on that directory so that the **user** account can read and write files in that directory. *This will be `c:\opt\flowfuse-device`* To make the device-agent run as a service, we will (in this example), use [nssm](https://nssm.cc/){rel=""nofollow""} but you are free to choose an alternative tool to run the device agent as a service. Finally, we set the service to run under the **service** account. *NOTE: The instructions in this how to were written on **Windows 11 Pro 22H2*** ### TIP: Using domain accounts If the account is a domain account, append the domain name to the **user** e.g. `user@domain` whenever the **user** name is used in the instructions below. ### TIP: Launching an elevated command prompt window (e.g. as the admin user) ```bash powershell -Command "Start-Process 'cmd' -Verb runAs ``` :cta-image{alt="Power Workplace relies on FlowFuse for scalability, reliability and security audits - book a demo" cta="demo" src="https://flowfuse.com/images/cta/power-workplace-book-demo.png"} ### TIP: Launching an elevated powershell prompt window (e.g. as the admin user) ```bash powershell -Command "Start-Process 'powershell' -Verb runAs ``` ## Pre-requisites ### Install Node.js The device-agent requires Node.js to be installed. You can download the latest version from {rel=""nofollow""}. It is recommended to install the LTS version and to check the "Automatically install the necessary tools" option. This is especially important if you intend on using any nodes that require native modules (like serialport). ### Create a New Windows User If you need to create a new **user** account follow these [instructions](https://support.microsoft.com/en-us/windows/create-a-local-user-or-administrator-account-in-windows-20de74e0-ac7f-3502-a866-32915af2a34d#:~\:text=Select%20Start%20%3E%20Settings%20%3E%20Accounts%20and,other%20user%2C%20select%20Add%20account.){rel=""nofollow""}. ## Prepare the device-agent files directory As the admin user, open an [elevated](https://flowfuse.com/#tip-launching-an-elevated-command-prompt-window-eg-as-the-admin-user) command prompt, create the files directory and setup access permissions. ```bash # In an elevated command prompt mkdir c:\opt mkdir c:\opt\flowfuse-device # grant full access to the service account that will run the device-agent icacls c:\opt\flowfuse-device /grant "user":F /T ``` *where `"user"` is the service account (not the admin account)* ## Install nssm `nssm` can simply be downloaded and executed from any path. We will download it to the `c:\opt` directory, extract the files and copy the 64 bit version to the current directory. ### `cmd` version [elevated](https://flowfuse.com/#tip-launching-an-elevated-command-prompt-window-eg-as-the-admin-user) command prompt ```bash # starting in the device-agent files directory cd c:\opt # download the nssm zip file curl -LJO https://nssm.cc/release/nssm-2.24.zip # extract the files tar -xf nssm-2.24.zip # copy the 64 bit version to the current directory copy nssm-2.24\win64\nssm.exe . # clean up del nssm-2.24.zip rmdir /s /q nssm-2.24 ``` ### `powershell` version [elevated](https://flowfuse.com/#tip-launching-an-elevated-powershell-prompt-window-eg-as-the-admin-user) powershell prompt If you don't have `cURL` installed, then powershell can be used to download the file. Here is how to do it: ```powershell # starting in the device-agent files directory cd c:\opt # download the nssm zip file Invoke-WebRequest -Uri https://nssm.cc/release/nssm-2.24.zip -OutFile nssm-2.24.zip # extract the files Expand-Archive -Path nssm-2.24.zip . # copy the 64 bit version to the current directory Copy-Item -Path .\nssm-2.24\win64\nssm.exe -Destination . # clean up Remove-Item -Path nssm-2.24.zip Remove-Item -Path nssm-2.24 -Recurse ``` ### Manual download If you prefer, you can download the nssm zip file manually from {rel=""nofollow""} and extract the files to the `c:\opt` directory. Then copy the 64 bit version to the current directory. Ultimately, you should end up with a file named `nssm.exe` in the `c:\opt\` directory. ## Install and configure the device-agent As the **service** account, to do so open a command prompt window and run the following and authenticate: ```bash runas /user:{serviceuser} cmd # e.g. runas /user:winserv cmd ``` *where `{serviceuser}` is the service account (not the admin account)* ### Check the users npm global path is set in the Users Environment Variables NOTE: The recommended flowfuse-device-agent instructions will result in the flowfuse-device-agent being installed in the NPM global directory. And the instructions to launch the device-agent expect the NPM global directory to be in your user path. This section will instruct you to a) find the NPM global path, then b) check the user’s path setting and, if necessary c) add the NPM global path to your user path. First, make a note of the path currently set for npm global. You can do this by running the following command: ```bash npm config get prefix ``` Next, ensure the `Path` Variable under the "User variables for *user*" contains the npm global path that we obtained in the previous step. Use the below command, to check the user’s `Path` setting. If it is not present, edit the path to include it. ```bash # This commands opens the environment variables editor, # look for the "Path" variable under "User variables for user", # and ensure it contains the npm global path rundll32 sysdm.cpl,EditEnvironmentVariables ``` If you did have to add the npm path to the users `Path` variable, you will need to **restart** the command prompt for the change to take effect and relogin as **user**. ### Install the device agent Note: you may have already installed the device-agent, however, **we strongly recommend** you do this step again as the service account and ensure that account has the latest version. ```bash npm i -g @flowfuse/device-agent ``` ### Link the device-agent to your flowfuse team First, we must run the device-agent and link it to our FlowFuse team. This will generate a "device configuration" details that we will use to configure the device-agent. Below is how to run the device-agent with the UI enabled. This will allow you to configure the device-agent via its web UI. ```bash flowfuse-device-agent --ui --ui-port 8080 --ui-user admin --ui-pass admin -d c:\opt\flowfuse-device -p 1880 ``` The device-agent will now be running and you can access the UI at {rel=""nofollow""} with the user and password both "admin" (you can change these in the command line if required). *NOTE: These credentials are temporary and only valid during the device setup* Proceed to configure the device-agent and link it to your flowfuse team. Full instructions can be found [here](https://flowfuse.com/docs/device-agent/register). Once you have linked the device-agent to your team, you can stop it by pressing `ctrl+c` in the command prompt window. ## Create the device-agent service As the admin user, open an elevated command prompt see [TIP](https://flowfuse.com/#tip-launching-an-elevated-command-prompt-window-eg-as-the-admin-user) above ### Install device-agent as a service ```bash cd c:\opt .\nssm.exe install flowfuse-device-agent "flowfuse-device-agent.cmd" .\nssm.exe set flowfuse-device-agent AppDirectory "c:\opt\flowfuse-device" .\nssm.exe set flowfuse-device-agent Description "FlowFuse Device Agent" # set the AppParameters (cli options) to tell the agent where its home directory is # in our case, this is c:\opt\flowfuse-device and is set with the -d option .\nssm.exe set flowfuse-device-agent AppParameters "-d c:\opt\flowfuse-device -p 1880" ``` ### Check the service is installed Run the following command to check the service is installed: ```bash services.msc ``` (look for a service named \`flowfuse-device-agent'). Alternatively, you can use the `sc` command: ```bash sc query flowfuse-device-agent ``` ### Set the user account that will run the service Some things are easier to edit in the UI, so we will edit the service via the NSSM UI. ```bash nssm edit flowfuse-device-agent ``` ![nssm editor](https://flowfuse.com/blog/2023/12/images/nssm_service_editor.png) In the UI, you can edit the service name, description, startup type, etc. The most important thing to check is the `Application` tab. This includes the path to the flowfuse-device-agent.cmd and its arguments. Select the "Log on" tab, select "This account" and enter the service account name and password that will run the device-agent. Click the "Edit Service" button to save the changes. Now you have a service that will run the device-agent as the **service** account 🎉 ### Controlling the service You can start the service with the command: ```bash sc start flowfuse-device-agent ``` You can check the current status with the command: ```bash sc query flowfuse-device-agent ``` You can stop the service with the command: ```bash sc stop flowfuse-device-agent ``` ### Further reading If you'd like to learn about windows services via the `sc` command you can access the help text by running `sc` from a command prompt. # Thank you for an incredible 2023! At the end of the year there’s always an opportunity to review how the year went, and I'm gonna take this opportunity to share the review of 2023 for FlowFuse. It's been an incredible year for FlowFuse and we've achieved a lot with our team. ### FlowFuse branding First off; in 2023 we were known as FlowForge. But, due to some trademark challenges, we went through a [rebranding phase](https://flowfuse.com/blog/2023/08/flowforge-is-now-flowfuse/) over the summer. It's been a bit of an adjustment, and you might still catch us – and even some of our customers – occasionally slipping up with the old name. But overall, we're really pleased with how smoothly everything's transitioned. ### Product adoption On the product side: Growth in adoption of the FlowFuse platform has been tremendous, in many dimensions; revenue generated, customers onboarded, and how many users are now professionalizing their usage of Node-RED. Our development platform has been used by thousands of developers to acquire data from various sources and visualize it to build rich applications for their use cases. And at every step of the way we’ve been able to improve their experience. Data acquisition was always possible without FlowFuse, however we’ve improved on the status quo through the [Certified Nodes](https://flowfuse.com/integrations/?certified=1) program. A nascent program that vets often used custom nodes from the community to ensure business readiness and validate nodes to ensure there’s no malicious code installed. Further, this year we’ve started [Dashboard 2.0](https://flowfuse.com/platform/dashboard/). The development of the successor of [node-red-dashboard](https://flows.nodered.org/node/node-red-dashboard){rel=""nofollow""}, which is built on deprecated technology and effectively on life-support. The development of the new Dashboard technology has taken massive steps and it’s very stable. Feature parity is not yet achieved, though we’re happy with the adoption of Dashboard 2.0 and the community [reporting issues and improvements](https://github.com/FlowFuse/node-red-dashboard/issues){rel=""nofollow""}. With the product improvements to FlowFuse, we’ve empowered large audiences to try and adopt the product. We’ve seen adoption in many areas: 1. Process manufacturing – Mining, oil & gas, beverages. 2. Discrete manufacturing – Ranging from automotive to logistics use-cases. 3. Digital transformation – Digital only integration, from website back-ends to workflow engines for up-skilled employees. 4. Agriculture – From environment monitoring to controlling sprinklers, water-pumps, and more. 5. Education & Research – Hundreds of students have registered for FlowFuse to learn how IIoT works, start building solutions, and prepare themselves for employing these skills in their first jobs. We’re always happy to [support you](https://flowfuse.com/contact-us/) in any of these or other industries where you may find value in how we streamline operations, manage your data acquisition logic and roll out, and remain compliant during your journey. ### Pricing changes A driving force behind the horizontal nature of adoption of the FlowFuse development platform has been the introduction of product tiers earlier this year. We’ve introduced Team and Enterprise tiers. A user can be promoted to the [Team or Enterprise tier](https://flowfuse.com/changelog/2023/09/introduction-enterprise-tier/) when they’re ready to further professionalize their adoption and have access to enhanced compliance, faster time to value for their developers, among other features. ### What 2024 will bring FlowFuse will continue to support the Node-RED community at large. In 2024 we’re looking to further grow into the main development platform for low-code developers. While our main focus will remain around Node-RED, there’s more to be done to become the defacto low-code development platform. Core of which are a few pillars: 1. Time to value for developers 2. Empowering more employees to automate the software layer of the solution 3. Enhanced compliance and enforcement We’re hoping to continue to serve our customers and grow the customer base in 2024! ### Holiday Season Support Over the next couple of weeks (22nd December - 2nd January), most of our team will be taking some well deserved time off. Don't worry, we will still be available if you need emergency support. The best way to contact us is via our website's [support page](https://flowfuse.com/support/). # Introduction to the Unified Namespace (UNS) – 2026 Updated Guide As your organization is generating more data there’s key architectural decisions to be made to ensure the full value can be unlocked and you’re leveraging not just the tip of the iceberg. The [Unified Namespace (UNS)](https://flowfuse.com/use-cases/uns/) provides a blueprint to allow data to be consumed by many data-consumers. FlowFuse helps you manage this migration and the operationalization of your data. To facilitate a many to many connection between data producers and data consumers, there are two changes to be made to your architecture: 1. Data transport through a hub-and-spokes model 2. Set structure of the Data :cta-image{alt="Get a free FlowFuse instance and connect your first data source to a Unified Namespace today" cta="sign-up" src="https://flowfuse.com/blog/2023/12/images/intro-uns-cta-1.png"} ## Hub and spokes model replaces Point to Point Traditionally, for example web servers serving web pages, the client requests a page from a server. This is a point to point connection between those two parties. ![Point to point graphic](https://flowfuse.com/blog/2023/12/images/uns-point-to-point.png "Point to Point connection") For the same data to be transmitted to a new data consumer, the consumer needs to make another request to obtain the data. This works great when you know what data you need for building your solution, and if you know where to get it. However, in manufacturing it’s not always possible to know up front who will need your data. Some machines are built and placed years before another machine would like to interact with the generated data. There might be many consumers for the same data set. Lastly; consumers might not know when to fetch new data points, and thus will try on a cycle or need another mechanism to understand if new data is available. Also, there are many challenges in point-to-point connections. For a deeper explanation, read [Why point-to-point connection is dead](https://flowfuse.com/blog/2024/11/why-point-to-point-connection-is-dead/). This is why a hub and spoke model should be employed. For each data source or data producer, a connection is made to a central hub; generally called a broker. ![Hub and spoke graphic](https://flowfuse.com/blog/2023/12/images/uns-hub.png "Unified Namespace Hub and Spokes communication") ## Structured data When many producers are connected to many data consumers, but not directly, the data producer needs to provide insight into what the events can be and will contain. It cannot, nor should even if it could, tailor the event’s data structure for a consumer so there’s decoupling on an architecture level. Structured data makes information, without structure the consumer receives mere bytes. This means that a schema for each event should be created and maintained. A schema which both the producer and consumer can read and validate each event against. As such a common Schema Definition Language (SDL) is chosen to provide clarity of how the data is structured, how it can be parsed, and in some cases also what it means for the developer. ## How Node-RED Fits In Node-RED excels in implementing a Unified Namespace with its flexible and powerful capabilities. It can act as both a central hub and a data consumer within a hub-and-spokes model, simplifying the integration of various data sources and consumers. By leveraging Node-RED's extensive library of nodes and its easy-to-use flow-based programming interface, organizations can efficiently manage data ingestion, transformation, and distribution. Node-RED also supports structured data through its support for JSON, XML, and other standard formats, allowing for clear and consistent data schemas. With its built-in nodes for MQTT, HTTP, and other protocols, Node-RED can seamlessly integrate with existing systems, enabling real-time data exchange and visualization. This makes it an ideal tool for operationalizing the Unified Namespace, ensuring that data flows efficiently and is readily available to all relevant stakeholders. Read this article to learn how you can build your Unified Namespace using Node-RED and FlowFuse: [Building a Unified Namespace with FlowFuse](https://flowfuse.com/blog/2024/11/building-uns-with-flowfuse/) You can also watch this webinar that explores the core concepts of the Unified Namespace, explains why it is essential for Industry 4.0, and demonstrates how FlowFuse and HiveMQ can be used together to build a scalable Unified Namespace. ::lite-youtube --- params: rel=0 style: "margin-top: 20px; margin-bottom: 20px; width: 100%; height: 480px;" title: YouTube video player videoid: z62O5RrOK8o --- :: ### How FlowFuse Can Help While Node-RED is highly effective for implementing UNS, managing and deploying it can be complex. FlowFuse provides a unified platform that simplifies deployment with one-click operations, secure management, and scalable Node-RED applications. It also includes features that enhance collaboration, alongside offering centralized management of all Node-RED instances to ensure streamlined operations and increased efficiency. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and experience FlowFuse's features** # Data Modeling for your Unified Namespace In the realm of industrial manufacturing, the concept of a Unified Namespace (UNS) emerges as a pivotal instrument for enhanced communication within a manufacturing network framework. Predicated on an event-driven architectural model, this approach advocates for the universal accessibility of data, irrespective of the immediate presence of a data consumer. This paradigm allows for a flexible role allocation within the network, where nodes can dynamically switch between being data producers and consumers, contingent upon the fluctuating requirements of the system at any specific juncture. For those unfamiliar with UNS, I recommend revisiting my [previous article](https://flowfuse.com/blog/2023/08/isa-95-automation-pyramid-to-unified-namespace/) on the subject. This article aims to explain the process of data modeling for your UNS, highlighting the role of tools like the FlowFuse Team Library in schema management. **Overview of Steps:** 1. [Connection to your Operational Technology (OT) equipment](https://flowfuse.com/#step-1-connection-to-your-operational-technology-ot-equipment) 2. [Structuring your payload](https://flowfuse.com/#step-2-structuring-your-payload) 3. [Building your Topic Hierarchy](https://flowfuse.com/#step-3-building-your-topic-hierarchy) 4. [Connection to your Unified Namespace](https://flowfuse.com/#step-4-connection-to-your-unified-namespace) ## Step 1 - Connection to your Operational Technology (OT) equipment The journey begins with establishing connections to OT equipment, which may include Programmable Logic Controllers (PLCs), Historian databases, and sensors. It is essential to facilitate compatibility with a diverse array of protocols. In this context, Node-RED emerges as a pivotal tool, bolstered by its expansive community-generated catalog featuring over 4500 nodes. In my example, the focus is on integration with a RevolutionPi. To achieve this, the FlowFuse Device Agent was deployed on a RevolutionPi (see our [documentation](https://flowfuse.com/docs/hardware/raspbian/)), and specific RevolutionPi nodes were installed. These nodes enable direct interaction with all interfaces of the PLC and are available through the [Node-RED library](https://flows.nodered.org/node/node-red-contrib-revpi-nodes){rel=""nofollow""}. Subsequent steps involved acquiring temperature data directly from the PLC. ![](https://flowfuse.com/blog/2023/12/images/revpi_nodes.png) For optimal data accuracy and integrity, it is recommended to timestamp data at the point of origin. In our scenario, the PLC outputs lack inherent timestamping. Consequently, I integrated a timestamp at the data acquisition stage within Node-RED, which runs on the same hardware. A general recommendation is the imperative of maintaining data integrity during transmission from OT systems to the message broker. This is particularly salient in regulated sectors such as pharmaceuticals, where standards like GxP mandate the preservation of unaltered data during transfer to the UNS. ## Step 2 - Structuring your payload The payload is the core of transmitted data. Transforming the payload for mutual intelligibility between sender and receiver, even within the same protocol, is sometimes necessary. Standardizing payload formats ensures consistent data storage and transmission. I recommend including schema type information with the data to cater to diverse use cases. Utilizing FlowFuse and Node-RED can enforce schema consistency. Node-RED's template node lets you define JSON schemas for your flows, while the FlowFuse Team Library facilitates schema sharing and consistency across your organization. In my example, I use a very simple JSON schema as a structure for measurements for `StationA`: ```json { "$schema": "Enterprise/Site/Line1/StationA/measurements/_schema", "title": "Measurement Schema for StationA", "type": "object", "properties": { "value": { "description": "The actual value being measured", "type": "number" }, "unit": { "description": "The unit of the measurement value", "type": "string" }, "timestamp": { "description": "The timestamp of the measurement in ISO 8601 format", "type": "string" } }, "required": [ "value", "unit", "timestamp" ] } ``` Example Data: ```json { "value": msg.payload, "unit": "Celsius", "timestamp": msg.timestamp } ``` ![Node-RED template node](https://flowfuse.com/blog/2023/12/images/template_node.png) The FlowFuse Team Library acts as my schema registry within my organization, allowing me to reuse my schemas and ensure consistency. ![FlowFuse Team Library](https://flowfuse.com/blog/2023/12/images/team_library.png) ## Step 3 - Building your Topic Hierarchy Your topic hierarchy should reflect your physical plant structure or align with existing asset naming systems. This approach improves data visibility and eases navigation for OT engineers. Many enterprises opt for the [ISA-95 part 2](https://www.isa.org/products/ansi-isa-95-00-02-2018-enterprise-control-system-i){rel=""nofollow""} model to structure their topics. In our example, we follow the structure of: Enterprise/Site/Line1/StationA ![MQTT Topic Tree](https://flowfuse.com/blog/2023/12/images/mqtt_topic_tree.png) ## Step 4 - Connection to your Unified Namespace Finally, transfer your data to the UNS, using protocols like MQTT or Kafka, depending on your UNS setup. While MQTT can handle up to 256 MB per payload, Kafka's default is 1MB, expandable to 10MB. These capacities suffice for most data types. In our example, we'll employ MQTT. ## Conclusion In conclusion, implementing a Unified Namespace (UNS) with efficient data modeling is a transformative step for any industrial manufacturing setup. By leveraging tools like FlowFuse Team Library, Node-RED, and protocols such as MQTT or Kafka, organizations can achieve a harmonious data ecosystem where information flows seamlessly across various nodes. As illustrated through practical examples, including the integration with a RevolutionPi, the importance of standardizing data schemas, maintaining data integrity, and structuring topic hierarchies cannot be overstated. Embracing these practices not only enhances operational efficiency but also paves the way for more advanced analytics and machine learning applications. ### The complete Node-RED flow :iframe{allow="clipboard-read; clipboard-write" height="225px" src="https://flows.nodered.org/flow/f6c783c6e9c1863145e0c63418eb5fe5/share?height=100" style="border: none;" width="100%"} # Capture Data from edge devices with Node-RED While cloud computing has revolutionized data access and analysis, not all data can be accessed from the cloud. In many scenarios, data collection from the edge – the location where data is generated – is essential for real-time decision-making or process observability. FlowFuse enables data to be collected through Node-RED. Data can be processed locally on the edge or sent on to other services. FlowFuse doesn’t rely on continuous connections to the cloud, making it a good choice for locations with unreliable internet connectivity. Use cases like real-time monitoring of critical systems, proactive maintenance, and improved operational efficiency are now possible to implement. ## Installing the FlowFuse agent To manage the capturing of data on the edge we’re going to first install the FlowFuse agent. It’s installed on your device to manage the communication between the edge device and the FlowFuse server, manage the installation of Node-RED, its execution environment, and facilitate communication between devices and the cloud. The device agent can run anywhere you can run a Docker container or Node.JS runtime (version 16.0+) can be installed. ### Registering a device on FlowFuse For the edge device to know what it’s supposed to do, it needs to listen to the FlowFuse commands. The agent's configuration is provided by a `device.yml` file from FlowFuse. Go to the team you’d like to add an edge device to, and select “Devices” on the left-hand menu, followed by the “Add Device” button. ![Setting up a FlowFuse agent](https://flowfuse.com/blog/2024/01/images/flowfuse-agent-setup.png "Setting up a FlowFuse agent") FlowFuse will prompt you to add a name (required), and a type (not required). When you’ve clicked `Add` you’ll get a new dialog to download the required file. ![Configuration file for the FlowFuse agent](https://flowfuse.com/blog/2024/01/images/device-yml-flowfuse.png "The contents of a device.yml file") ### Install the FlowFuse agent through Docker If your device supports it, the fastest way to run the FlowFuse agent is with containers. Assuming you’ve already got Docker installed, there are two steps to follow: first, move the device YAML file downloaded from FlowFuse to the edge device and save it in `/opt/flowfuse/device.yml`. Start the agent by running: ```text docker run --mount type=bind,src=/path/to/device.yml,target=/opt/flowfuse-device/device.yml -p 1880:1880 flowfuse/device-agent:latest ``` Note that for production cases, ensure the container is restarted on reboot. Docker can do this for you, [please follow their guide](https://docs.docker.com/config/containers/start-containers-automatically/){rel=""nofollow""}. ### Install the FlowFuse agent with npm To install the agent through NPM, you’ll need a Node.JS version of 18.0 or later. Open a command prompt and run: `npm install -g @flowfuse/device-agent`. This will install the FlowFuse Device Agent as a global npm module, making the flowfuse-device-agent command available in any directory on your system. Once the installation is complete, you must configure the Device Agent to connect to your FlowFuse instance. In this guide, you’ve previously downloaded the `device.yml` file that’s needed now. On Linux or Mac, move the file to `/opt/flowfuse-device/device.yml`, and for Windows-based systems, move the file to `c:\opt\flowfuse-device\device.yml`. Afterward, start the agent with: `flowfuse-device-agent`. This will launch the Device Agent and connect it to your FlowFuse instance. The Device Agent will wait for instructions on which flows to run. ### Programming flows for the edge Now the agent is running, the FlowFuse platform will show it has contacted back to the platform and is ready to do some work. First, add it to the application and start the developer mode. That enables the device editor and provides you secure access to the editor anywhere in the world for everyone in the FlowFuse team with the right access role. When the development is done, be sure to create a snapshot of the developed flows to create a point-in-time backup, or to roll the snapshot out to many other devices later. # Node-RED Dashboard 2.0 is Generally Available! Back in [June 2023](https://flowfuse.com/blog/2023/06/dashboard-announcement/) we announced that FlowFuse would be investing into building out the next generation of Node-RED Dashboard, the most popular UI framework for Node-RED. We followed this up with the [first release](https://flowfuse.com/blog/2023/07/dashboard-0-1-release/) (`0.0.1`) in July, just one month later, and today, we are pleased to announce that we have reached a major milestone in this journey, with the release of our first major version (`1.0.0`) of Node-RED Dashboard 2.0. ![Dashboard 2.0 Example showing weather data](https://flowfuse.com/blog/2024/01/images/dashboard-ga-example.png) With our `1.0.0` release, you can now build your dashboards on a reliable and stable package, and we invite you to to start contributing your own third-party [widgets](https://dashboard.flowfuse.com/contributing/widgets/third-party.html){rel=""nofollow""} and [plugins](https://dashboard.flowfuse.com/contributing/plugins/){rel=""nofollow""}. We're excited to see what the community can contribute and build on top of this new Dashboard 2.0 framework, and we'll be continuing development to the core collection of widgets too. With Node-RED Dashboard 2.0, we have re-built the original Node-RED Dashboard from the ground up. It is now extensible due to it being VueJS-based, completely responsive down to mobile, and we've made many quality of life improvements across the board to the existing widget collection, as well as adding a few new ones too. ## What's new in Dashboard 2.0? We've shared plenty of updates since we started, detailing the feature parity with the original Node-RED Dashboard, as well as some of the new widgets and features we've added to the new Dashboard, such as Markdown, Mermaid Charts and new Layout Options, you can read more about those here: - [Dynamic Markdown, Tables & Notebooks](https://flowfuse.com/blog/2023/09/dashboard-notebook-layout/) - [UI Chart Improvements](https://flowfuse.com/blog/2023/11/dashboard-0-7/) - [Building a Custom Video Player](https://flowfuse.com/blog/2023/12/dashboard-0-10-0/) Furthermore, the most requested feature for the legacy dashboard has been implemented in Dashboard 2.0, the ability to hide charts and forms based on the user that's viewing the dashboard. ![Dashboard 2.0 Example showing personalised dashboard](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-user2.png) Read more about it here: - [Personalised Multi User Dashboards](https://flowfuse.com/blog/2024/01/dashboard-2-multi-user/) If that wasn't enough, we also have [rich documentation](https://dashboard.flowfuse.com/){rel=""nofollow""} for Dashboard 2.0 too, detailing all of the available nodes, details on how Dashboard 2.0 is built and how to contribute to the project too if you're that way inclined. ## Upcoming Webinar If you're interested in learning more about Dashboard 2.0 and in particular, personalised multi-user dashboards, we're hosting a webinar on Thursday, 29th February. You can find out more information [here](https://flowfuse.com/webinars/2024/node-red-dashboard-multi-user/) ## Follow our Progress We aren't stopping here, we'll continnue to push Dashboard 2.0 forward with future development, with a [new UI Gauge](https://github.com/FlowFuse/node-red-dashboard/issues/12){rel=""nofollow""} next on the list. You can track that progress of that particular issue, and the rest of the work we have lined up on our GitHub Projects: - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} - [Dashboard 1.0 Feature Parity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/5){rel=""nofollow""} If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. # Personalised Multi-user Dashboards with Node-RED Dashboard 2.0! This week has seen the release of the [first major version of Node-RED Dashboard 2.0](https://flowfuse.com/blog/2024/01/dashboard-2-ga), with it, we've made available a new FlowFuse-exclusive feature, personalised multi-user dashboards. This new feature will allow you to build applications that provide unique data to each user, build admin-only views, and track user activity, to name but a few. We're really excited to see what the Node-RED Community and our FlowFuse customers can do with such a powerful and flexible framework. ## Personalised Multi User Dashboards The original Node-RED Dashboard was built with a "single source of truth", no matter how many users interacted with the dashboard, each user would always see the same data. This is great for prototyping, or hobby projects, but as you scale up your Node-RED usage, you'll want to be able to have unique dashboard experiences for each user. ### Getting Started To enable personalised multi-user dashboards, you'll need to be using FlowFuse, and complete two steps: #### Step 1: Enable "FlowFuse User Authentication" All instances on FlowFuse can be configured with *"FlowFuse User Authentication"* in the "Security" Settings. This option requires any user that wants access to your Editor or dashboard to be authorized by FlowFuse first. !["Screenshot of the 'Security' settings available for any Instances running in FlowFuse"](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-ffauth.png "Screenshot of the 'Security' settings available for any Instances running in FlowFuse"){dataZoomable=""} **"Screenshot of the 'Security' settings available for any Instances running in FlowFuse"** #### Step 2: Install FlowFuse's User Addon ##### FlowFuse Cloud :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} *Note: Every instance created from today onwards automatically comes with the necessary configuration. Already created instances need to be manually restarted.* The Personalised Multi-User Dashboard plugin, `@flowfuse/node-red-dashboard-2-user-addon`, is available in our [Certified Nodes](https://flowfuse.com/integrations/?certified=1) catalogue, accessible to our Teams and Enterprise customers. Once the "FlowFuse User Authentication" option has been enabled on your instance, you can then install our plugin, `@flowfuse/node-red-dashboard-2-user-addon`, through the "Manage Palette" option in the Node-RED Editor. For your devices, we provide the necessary configuration and access token upon request, so that your Node-RED devices can also benefit from a Personalised Multi-user Dashboard. ##### FlowFuse Self-Hosted For all our Teams and Enterprise Self-Hosted customers who also want to use the Certified Nodes and the Multi-User Dashboard, we provide all necessary configurations upon request to get started. Alternatively, if you're looking to elevate your Node-RED infrastructure, [book in a chat with us](https://flowfuse.com/contact-us) to talk about how FlowFuse can help. ### Using the Plugin Once enabled, any messages emitted by a Dashboard 2.0 node will contain a new `msg._client.user` object, e.g: ```js { "userId": "", // unique identifier for the user "username": "", // FlowFuse Username "email": "", // E-Mail Address connected to their FlowFuse account "name": "", // Full Name "image": "" // User Avatar from FlowFuse } ``` Then, when running Node-RED Dashboard 2.0 on FlowFuse, you'll have a new sidebar option in the Node-RED Editor, which allows you to control which node types allow for "client constraints". ![The new 'FF Auth' options available in Node-RED to allow for client constraints](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-ff-settings.png "The new 'FF Auth' options available in Node-RED to allow for client constraints"){dataZoomable=""} **A screenshot of the new 'FF Auth' options available in Node-RED to allow for client constraints on different node types.** In the original Node-RED Dashboard, this was *always* enabled for the `ui-notification` and `ui-control` nodes, whereby you could include `msg.socket` data and it would only then send that message to the specified client. For Dashboard 2.0 we've extended this concept so that as a Node-RED Developer, you can now include `msg._client.user` data in any message sent to a Dashboard 2.0 node. Under the covers, our FlowFuse-exclusive plugin will then automatically filter messages to only send to the relevant user's connection. Utilising this feature, below you can see an example where we send data to a `ui-template` to render a custom table for each user. Under the covers this is a `ui-event` node (triggered on a page view), which then uses the `msg._client.user` object to make a REST API call to retrieve a list of todo items for that specific user. We then wire the response into the `ui-template`, which has been configured to "Accept Client Constraints", and so only sends this data to User 2's dashboard. ![Showing Admin Task View](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-admin-tasks.png "Showing Admin Task View"){dataZoomable=""} **Example of a dashboard that displays user-specific content.** Note too that we're also utilising the new [Teleport](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#teleports){rel=""nofollow""} option available in a `ui-template` which allows us to define content to show in the top-right of the dashboard, in this case, a little *"Hi {username}"* message. ### Examples #### Rendering Logged In User Data In the previous example, you may have noticed that we're also displaying a welcome to the authenticated user on our dashboard, this means that we have access to the full User object within any `ui-template` that we render too. ![Showing User Unique Data](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-user2.png "Showing User Unique Data"){dataZoomable=""} **The "Admin" view that is only made available to users registered as an "admin".** Under the covers, we're appending our `user` object to the `msg` object, via the SocketIO `auth` option. We make the `socketio` object available via a computed `setup` variable, this means that we can access this data in any `ui-template` node, and render like so: ```html ``` To enable custom user-by-user content in a `ui-template` though, we must allow it to "Accept Client Constraints". This means that if a `.msg._client.user` value is included in any messages sent to a `ui-template` node, then the underlying SocketIO message will be filtered to only send to the relevant user's connection, and no others. #### Admin Only Views With this new functionality we can also now show/hide content based on the authenticated user. We recently introduced the option to [set default visiblity & interaction states](https://github.com/FlowFuse/node-red-dashboard/pull/484){rel=""nofollow""}. This was partly introduced because it's a good practice to set the default "Visibility" option for any admin-only pages to "Hidden", and then use a `ui-control` node to show the content only to the relevant admins. :iframe{allow="clipboard-read; clipboard-write" height="100%" src="https://flows.nodered.org/flow/2fe8e6f1e7002f1ff6a9195ad1a153b6/share" style="border: none; margin-bottom: 12px;" width="100%"} Let's breakdown the above flow: 1. We wire a `ui-event` node (which emits each time a user views a page) into a switch node 2. Our switch node checks the `user.username` against a known list of admin users and branches "admin:" and "non-admin" users 3. For admin users, the `change` node defines a message for our `ui-control` node to dynamically show content, in this case an "Admin" page, when appropriate. ```json { "pages":{ "show": ["Admin View"] } } ``` All events going into `ui-control` are automatically filtered based on the `msg._client.user` object, so only the Admin users will receive the message to show the "Admin View" page, resulting in: ![Showing Admin Only View](https://flowfuse.com/blog/2024/01/images/multi-user-dashboard-admin.png "Showing Admin Only View"){dataZoomable=""} **The "Admin" view that is only made available to users registered as an "admin".** Further extensions of this could also check `ui-event` in case a non-admin user tries to access the `/admin` page directly, in which case we can utilise `ui-control` to navigate the user away from the page immediately. See the [ui-control documentation](https://dashboard.flowfuse.com/nodes/widgets/ui-control.html#navigation){rel=""nofollow""} for more details on this. ## Upcoming Webinar If you're interested in learning more about Dashboard 2.0 and in particular multi-user Dashboards, we're hosting a webinar on Thursday, 29th February. You can find out more information [here](https://flowfuse.com/webinars/2024/node-red-dashboard-multi-user/) ## Follow our Progress We aren't stopping here, we'll continue to push Dashboard 2.0 forward with future development, and you can track that progress on our GitHub Projects: - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} - [Dashboard 1.0 Feature Parity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/5){rel=""nofollow""} If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. # FlowFuse 2.0 Release Following the release of FlowFuse 1.0 end of 2022, we're excited to release FlowFuse 2.0, marking a significant step in managing Node-RED remote instances, which we call Devices. FlowFuse already was the best place to operate Node-RED at scale in the cloud or on-premise, now it's able to manage Node-RED where ever it's run. Many organizations position Node-RED instances on remote servers like edge or industrial devices. This way they can meet network requirement, interact with analog protocols, and overcome other infrastructure requirements. Management of remote instances is crucial for the overall success of closing the gap between IT and OT. A key enhancement was the introduction of Device Groups (from version 1.15) and the new feature to assign target snapshots. This allows for direct and streamlined management of Node-RED Device fleets, setting the stage for future advancements in device management capabilities. For our FlowFuse users, this means it is no longer necessary or recommended to assign devices to an instance. Node-RED devices can be managed independently, and snapshots can be assigned via DevOps pipelines. ## Enterprise-Readiness FlowFuse is committed to augmenting the enterprise-readiness of Node-RED with introductions like [Single Sign-On (SSO)](https://flowfuse.com/docs/admin/sso/), [Multi-Factor Authentication (MFA)](https://flowfuse.com/docs/user/user-settings/#two-factor-authentication), and [High Availability](https://flowfuse.com/docs/user/high-availability/) since version 1.0. Furthermore, we recently achieved [SOC2 Type 1 compliance](https://flowfuse.com/blog/2024/01/soc2/). With these advancements, Node-RED, in combination with FlowFuse, is genuinely ready for enterprise and production use. ## Enhanced Integration Capabilities The Node-RED Flow Library has always been a cornerstone, offering over 4800 connectors (nodes) for various OT and IT protocols. Thanks to the community and the Node-RED library. Building on this foundation, FlowFuse introduced "Certified Nodes" and "Blueprints". These [Blueprints](https://flowfuse.com/blog/2023/10/blueprints/) are designed to provide an easier start with Node-RED, showcasing its full potential, while Certified Nodes ensure the security of the nodes used. Learn more about our new Certified Nodes [here](https://flowfuse.com/blog/2023/10/certified-nodes/). ## Developer Velocity Node-RED team development is made possible with FlowFuse. Different development team members are able to share and collaborate on the same Node-RED instance. This makes for much easier collaboration between Node-RED developers. We've worked hard on maturing our [snapshot capabilties](https://flowfuse.com/docs/user/snapshots/) and introduced [DevOps Pipelines](https://flowfuse.com/docs/user/devops-pipelines/) that can be set up to stage Node-RED instances that have different development stages, e.g. test, development and production. ## Looking Ahead At FlowFuse, our mission is to empower bottom-up innovation and enable organizations to transform their workflows into business-critical applications with unprecedented efficiency. As we move forward, we are excited to invite our users to actively engage with our future developments. Our [Roadmap](https://flowfuse.com/changelog/) lays out the advancements we're targeting, offering a glimpse into the features and enhancements that are on the horizon. We also encourage our users to stay informed and involved by checking out our latest updates in our detailed [changelog](https://flowfuse.com/changelog/). Your insights and feedback are crucial to us; they fuel our commitment to continuous improvement and innovation. We warmly invite you to [share your thoughts and suggestions](https://flowfuse.com/contact-us/), as your input is a vital part of our journey in shaping the next steps for FlowFuse. ## How to get started You can install FlowFuse yourself via a variety of install options. You can find out more details [here](https://flowfuse.com/docs/install/introduction/). If you'd rather use our hosted offering: [Get started for free](https://app.flowfuse.com/account/create){rel=""nofollow""} on FlowFuse Cloud. ## Upgrading FlowFuse \[FlowFuse Cloud]\({{ site.appURL }}) is already running version 2.0. If you installed a previous version of FlowFuse and want to upgrade, our documentation provides a guide for [upgrading your FlowFuse instance](https://flowfuse.com/docs/upgrade/). The version 2.0 release of the FlowFuse Helm Chart includes a breaking change for deployments making use of the `forge.localPostgresql` setting when upgrading. This is where the helm chart installs a dedicated PostgreSQL database instance. With version 2.0 we have updated the version of the Bitnami PostgreSQL Helm sub-chart we bundle and the upgrade process will require some manual intervention to ensure things work correctly. A fresh install should not require any extra steps. The steps are documented on the [Upgrade instructions](https://flowfuse.com/docs/install/kubernetes/#upgrade) page, please read them carefully before upgrading ## Getting help Please check FlowFuse's [documentation](https://flowfuse.com/docs/) as the answers to many questions are covered there. Additionally you can go to the [community forum](https://discourse.nodered.org/c/vendors/flowfuse/24){rel=""nofollow""} if you have any feedback or feature requests. # Step-by-Step Guide to Deploying Node-RED with FlowFuse in balenaCloud In a [recent webinar with balena](https://flowfuse.com/webinars/2024/balena/), we explored the dynamic capabilities of deploying FlowFuse to a fleet of devices using [balenaCloud](https://www.balena.io/cloud){rel=""nofollow""}. This blog post serves as a practical guide to replicate that process, specifically tailored for those aiming to streamline their deployment of FlowFuse in an efficient and user-friendly manner. ## How to Implement FlowFuse with balenaCloud on a Fleet of devices [![Deploying Node-RED with FlowFuse in balenaCloud](https://i.ytimg.com/vi/cKFu1ljUlKE/hqdefault.jpg)](https://www.youtube.com/watch?v=cKFu1ljUlKE "Deploying Node-RED with FlowFuse in balenaCloud"){rel=""nofollow""} ### Preparation Steps Before diving into the deployment process, it's crucial to familiarize yourself with key resources. We recommend reviewing our previous [blog post](https://flowfuse.com/blog/2023/11/device-agent-balena/) on deploying the FlowFuse Device Agent via balena. This post contains a vital link to the GitHub repository, essential for deploying FlowFuse with balena, laying the groundwork for the steps ahead. ### Creating a New Fleet in balenaCloud 1. Navigate to the [FlowFuse git](https://github.com/FlowFuse/balena-device-agent){rel=""nofollow""} repository. Click on the **Deploy with balena** button. 2. Name your fleet. 3. Select your default device. 4. Click **Create and Deploy**. ### Adding Devices to the Fleet Once your fleet is created, the next step is to add devices. To add a device to your fleet, follow these [instructions](https://docs.balena.io/learn/getting-started/var-som-mx6/rust/#add-a-device-and-download-os){rel=""nofollow""}. ### Setting Up FlowFuse Setting up FlowFuse correctly is essential for seamless operation: 1. Create a new instance within FlowFuse or use an existing one if you prefer. Follow these [instructions](https://flowfuse.com/docs/user/introduction/#creating-a-node-red-instance) to create a new instance. 2. Create a **Device Provisioning Token** by following these [instructions](https://flowfuse.com/docs/device-agent/register/#bulk-registration). 3. Ensure you add the FlowFuse Node-RED application you want the devices to provision. If left at default, devices will need to be manually added to applications. ### Using the Device Provisioning Token 1. First, convert the contents of the Device Provisioning Token to base64. Follow these [instructions](https://flowfuse.com/blog/2023/11/device-agent-balena/#environment-variable) to convert the file to base64. 2. Once converted, import this string into balena as a **Fleet** level variable, not a device level variable. Follow these [instructions](https://docs.balena.io/learn/manage/variables/#fleet-wide-variables){rel=""nofollow""} to import the Fleet level variable with the Name `FF_DEVICE_YML`. 3. This action will provision any new device added to the fleet with the yaml file configuration, automatically adding the device to a FlowFuse instance. ### Deploying and Testing the FlowFuse Instance Deploying the FlowFuse instance brings everything together: 1. Navigate to your FlowFuse application created earlier. 2. Go to your devices and you should now see your newly provisioned devices from balena. 3. If this is your first time setting up your fleet, the device will not have a snapshot. You will need to deploy a snapshot. Follow these [instructions](https://flowfuse.com/docs/user/snapshots/#create-a-snapshot) to do so. Ensure that you select **Set Target Snapshot**. 4. Once complete, the FlowFuse instance will deploy to your device(s). ## Integrating InfluxDB (Optional) Integrating InfluxDB enables effective data storage and management: 1. Similar to the previous steps, navigate to this [Github repository](https://github.com/mpous/flowfuse-agent-influx-balena/tree/main?tab=readme-ov-file){rel=""nofollow""} and click **Deploy with balena**. 2. This time, instead of creating a new fleet, select **Use an existing fleet instead**. 3. Choose your fleet for deployment and select **Deploy to fleet**. ### Data Generation and Management For testing, we have created a flow to get you started. Follow this [link](https://flows.nodered.org/flow/66f37bb739b6cdb0c7ad3a4e2edd68ef){rel=""nofollow""} and import it. There are four sets of flows for you to begin with. The first is for data generation. The second is a manual data generation flow. The third is key as it initiates the creation of a database, in this case, **mydb**. The last flow is a simple query that pulls data from InfluxDB. 1. Import the flows into your FlowFuse instance of Node-RED and deploy. Follow these [instructions](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/#_2-import-helpful-example-flows-provided-with-custom-nodes) for importing and exporting. 2. Return to Flowfuse, go to your instance, and create another [snapshot](https://flowfuse.com/docs/user/snapshots/#create-a-snapshot). 3. Ensure that you **Set Target Snapshot**. ### Finalizing and Testing the Setup The final steps ensure that your setup is fully operational: 1. Once deployed, navigate to the device. 2. [Enable Developer Mode](https://flowfuse.com/docs/device-agent/deploy/#editing-the-node-red-flows-on-a-remote-instance-that-is-assigned-to-an-application). 3. Next, click the newly revealed button, **Open Editor**, to access the deployed Flow. ### Conclusion Implementing FlowFuse with balenaCloud significantly enhances your device management and data processing capabilities. This guide provides a foundational approach, but don't hesitate to delve deeper into each step to tailor the setup to your specific needs. # Import a File into Node-RED with Dashboard 2.0 Need to get a file into Node-RED, but don't want to over complicate things. This article outlines how you can leverage Dashboard 2.0 to import a file directly into Node-RED via a Dashboard. ## Why would you need to import a file to Node-RED? Often times it is necessary to update lookup tables in a SQL database, but you don't necessarily want to give access to everyone to edit the database, nor do you want to have to do it all yourself. This can often be seen when new products are introduced into a manufacturing facility. It may not be often, but enough that it warrants its own application. This process will guide you in a way that will enable your teammates to upload the files to the system themselves. Furthermore, on the management layer of most companies, Excel and Google Sheets are the go-to tools to perform data collection tasks. Getting management involved in processes might require you to build an import feature for them. Asking your manager to "Save as" CSV is much easier than teaching them SQL! ### Node-RED Dashboard (FlowFuse) ![csv dashboard](https://flowfuse.com/blog/2024/01/images/csv-dashboard.png) This simple flow allows the user to visualize data from a CSV in the [Node-RED Dashboard](https://flowfuse.com/platform/dashboard/). The button then allows the user to initiate a request to send the data to the next step. This next step could be anything from loading into a SQL database to saving it. ### Instructions 1. Install Node-RED Dashboard 2.0. Follow these [instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to install. 2. Import Flow - to import the flow into your Node-RED instance follow these [instructions](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/#_1-copy-and-share-your-flows-using-export-and-import). 3. Access Dashboard - To access the dashboard, navigate to the `https://.flowfuse.cloud/dashboard`. This dashboard is currently configured to take in CSV files and transform them into a single message that is sent to the table for visualization. Simultaneously the data from the import is stored locally in the flow context. From there, the button can be used to trigger the sending of the data from the flow context to the next destination. In this case, it is a simple debug node. :iframe{allow="clipboard-read; clipboard-write" height="225px" src="https://flows.nodered.org/flow/8c505039ac1b8dbed2bee1e22ee2975a/share?height=100" style="border: none;" width="100%"} ## Need to Send a File to Node-RED from another application or source? Check out this [blog](https://flowfuse.com/blog/2024/01/send-a-file) on how to send a file from either a stand alone web application or use the sample python script to imbed it into your current application. # AI and ChatGPT - Revolutionizing the Manufacturing Industry The application of artificial intelligence (AI) in various industries, particularly in manufacturing, is a topic of growing interest. The evolution of technologies like ChatGPT is driving significant changes in this sector. For a more nuanced understanding, we reference four informative blog posts from our team members. The first post, ["AI Use Cases that are shaping the next manufacturing frontier"](https://flowfuse.com/blog/2023/12/ai-use-cases/), offers an insightful overview of AI's role in diverse areas. Following this, ["ChatGPT AI Assistants with Node-RED"](https://flowfuse.com/blog/2023/11/ai-assistant/) examines the specific impact of AI assistants. The third article, ["Node-RED Builder a ChatGPT GPT"](https://flowfuse.com/blog/2023/11/chatgpt-gpt/), discusses the capabilities of generative pre-trained transformers like ChatGPT. Lastly, ["How ChatGPT improves Node-RED Developer Experience"](https://flowfuse.com/blog/2023/09/chatgpt-for-node-red-developers/) explores ChatGPT's application in Node-RED development, an important aspect for many in manufacturing. ## How AI and ChatGPT are Impacting Manufacturing AI and ChatGPT's integration into manufacturing indicates a shift in production process management. Here are some key impacts: ### Boosting Efficiency and Productivity AI, especially ChatGPT, enhances manufacturing efficiency by analyzing data to improve production lines, predict maintenance, and aid in design and development. ### Automating Routine Tasks AI is adept at handling repetitive tasks, speeding up manufacturing and allowing human workers to engage in more complex production aspects. ### Elevating Quality Control AI algorithms consistently ensure high-quality standards, quickly identifying and fixing product defects or deviations. ### Enabling Customization and Flexibility AI's learning and adaptability make it suitable for customizing production, allowing manufacturers to meet specific customer demands more efficiently. ### Transforming the Workforce AI in manufacturing necessitates skilled workers to operate and maintain these systems, altering job roles and responsibilities. ## Pros and Cons of AI and ChatGPT in Manufacturing ### Pros 1. Boosted Productivity: AI-driven automation increases production efficiency. 2. Consistent Quality Assurance: AI ensures ongoing product quality. 3. Reduced Costs: AI optimizes resource use and minimizes waste. 4. Encouraging Innovation: AI facilitates new manufacturing methods and products. 5. Enhancing Safety: AI reduces human exposure to hazardous manufacturing conditions. ### Cons 1. Initial Investment Costs: AI technology implementation can be expensive. 2. Need for Skilled Labor: Demand for workers proficient in AI technologies is growing. 3. Job Role Changes: Automation might decrease the need for certain labor roles. 4. Security Concerns: AI systems can be susceptible to cyber threats. 5. Technological Dependence: Excessive reliance on AI could limit problem-solving abilities in workers. ## Frequently Asked Questions (FAQs) 1\. How is AI changing manufacturing? :br**AI is altering manufacturing through automation, optimizing efficiency, and fostering production innovations.** 2\. What is ChatGPT's role in manufacturing? :br**ChatGPT aids in data analysis, automates processes, and improves communication and documentation in manufacturing.** 3\. Are manufacturing jobs at risk due to AI? :br**While AI may automate some repetitive jobs, it also creates opportunities for skilled labor in technology management and development.** 4\. Can AI enhance manufacturing product quality? :br**Yes, AI's continuous monitoring and analysis significantly boost quality control.** 5\. What are the main challenges of integrating AI in manufacturing? :br**Challenges include high implementation costs, the need for skilled labor, and transitioning to automated processes.** 6\. Is AI cost-effective in manufacturing? :br**Despite high initial costs, AI can lead to long-term savings through improved efficiency and waste reduction.** 7\. How does AI affect manufacturing worker safety? :br**AI reduces risk by taking over hazardous tasks, improving overall workplace safety.** 8\. Can small manufacturers benefit from AI? :br**Yes, AI solutions are increasingly accessible for small-scale manufacturers.** 9\. What training is required for AI-enabled manufacturing workers? :br**Training in AI system operation, data analysis, and potentially programming skills is needed.** 10\. What does the future hold for AI in manufacturing? :br**The future suggests more integrated, intelligent, and adaptable manufacturing processes driven by AI advancements.** # Send a File to Node-RED Have you ever needed to send a CSV file to your Node-RED instance? This file can go on to populate a shift schedule, product specifications, or some other configuration file that is used. In this guide, we provide a couple of options to upload the data to your Node-RED for further processing and to organize the data to be sent on or used. ## Why would you need to send a file to Node-RED? Often times it is necessary to update lookup tables in a SQL database, but you don't necessarily want to give access to everyone to edit the database, nor do you want to have to do it all yourself. This can often be seen when new products are introduced into a manufacturing facility. It may not be often, but enough that it warrants its own application. This process will guide you in a way that will enable your teammates to upload the files to the system themselves. Furthermore, on the management layer of most companies, Excel and Google Sheets are the go-to tools to perform data collection tasks. Getting management involved in processes might require you to build an import feature for them. Asking your manager to "Save as" CSV is much easier than teaching them SQL! ## 2 Ways to send a file to Node-RED There are many approaches that can be taken when solving this. We are going to go over 2 here. 1. [Simple Python Script](https://flowfuse.com/#simple-python-script) - Simple script that will be shared below. It is a simple Python application that allows the user to send a file with a simple command, but this might require a little more technical skills that the end user may not feel comfortable with. 2. [Stand Alone Web Application](https://flowfuse.com/#stand-alone-web-application) - A web-based application that allows the user to upload files to a browser with a selectable endpoint. ### Simple Python Script This simple Python script sends a file to a Node-RED flow. The flow that will work with this script can be seen [here](https://flowfuse.com/#node-red-ingress). The script requires **requests** and **Python 3.x**. Install requests: ```bash pip install requests ``` Create a file called run.py and paste the contents into the file. ```python import requests def send_file(nodered_url, file_path): # Open the file in binary mode with open(file_path, 'rb') as file: files = {'file': (file.name, file, 'multipart/form-data')} response = requests.post(nodered_url, files=files) return response # Update the ip address and port of your Node-RED instance nodered_url = 'http://localhost:1880/fileupload' # Update the location of your file file_path = 'C:/Users/myUser/Downloads/shiftSchedule.csv' response = send_file(nodered_url, file_path) print(f"Response Status Code: {response.status_code}") print(f"Response Body: {response.text}") ``` Update the **nodered\_url** to the location of the Node-RED instance. Be sure to adjust the port if the default port of 1880 isn't being used. Update the **file\_path** with the path to where the file to be uploaded will be located. **Save** To run: ```python python run.py ``` ### Stand Alone Web Application ![csv upload application](https://flowfuse.com/blog/2024/01/images/csv_upload_app.png) This stand-alone web application can be run on either Windows or Linux, .bat for Windows, and .sh for Linux. #### Installation Clone the repository and navigate to the directory: ```bash git clone https://github.com/gdziuba/FF_Send-File-to-NR.git && cd FF_Send-File-to-NR ``` #### Configuration Edit the lines in the body of [index.html](https://github.com/gdziuba/FF_Send-File-to-NR/blob/21214f88c6c4536f49efb88cf5f84bf52071a88b/templates/index.html#L69){rel=""nofollow""} to include the endpoints to which you would like to send the files. ```text ``` ### Operating Systems #### Windows Run the script: ```bash .\start_app.bat ``` This will install if necessary, start the Flask Application, and take you to localhost:5000 on the browser. #### Linux Make the script executable by running running: ```bash chmod +x setup_and_run.sh ``` Then run the application with: ```bash ./setup_and_run.sh ``` To access the application, open a browser to the **\:5000** of the running application. #### Node-RED Ingress :iframe{allow="clipboard-read; clipboard-write" height="225px" src="https://flows.nodered.org/flow/effb53752e5d6f767b3c7e5d41a4a6e8/share?height=100" style="border: none;" width="100%"} Once we have a file ready to be sent, we now need to configure the receiving side in Node-RED. In this example, we are leveraging a CSV formatted file and then converting it to be used at a later time. A link to the flow can be found [here](https://flows.nodered.org/flow/effb53752e5d6f767b3c7e5d41a4a6e8){rel=""nofollow""}. To import the flow, follow these [instructions](https://flowfuse.com/blog/2023/03/3-quick-node-red-tips-5/#_1-copy-and-share-your-flows-using-export-and-import). A Simple HTTP In node can be used in the form of a Post, ensuring the configuration allows for a file. ## Wanna import it directly into your Node-RED instance via a Dashboard? Check out this [blog](https://flowfuse.com/blog/2024/01/import-a-file) on how to directly import a file into a Node-RED instance via Dashboard 2.0. # Sentiment Analysis with Node-RED Have you ever built a sentiment analysis system to extract insights from text content? If yes then I don’t think you'll need an explanation of how complex it is to build. In this guide, we will build a sentiment analysis system with Node-RED using Dashboard 2.0 in a few easy steps. ## What exactly is sentiment analysis? Sentiment analysis is a context-mining technique used to understand emotions and opinions expressed in text, often classifying them as positive, neutral, or negative. There are many real-world applications of this technique. - **Analysing Feedback:** Customers, or other stakeholders like employees, are periodically requested to fill out a feedback form. Analysis of such feedback is the most widespread application of sentiment analysis. - **Campaign Monitoring:** Another use case of sentiment analysis is a measure of influence which is crucial in any marketing campaign. - **Brand Monitoring:** Brand monitoring is another great use case for sentiment analysis. Companies can use sentiment analysis to check the social media sentiments around their brand from their audience. ## Building a Form in Dashboard 2.0 In this system, we will analyse the sentiment of text content obtained from the user. For this we are going to build a user interface using Dashboard 2.0 and Node-RED. 1. Install Node-RED Dashboard 2.0. Follow these [instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to install. 2. Drag a ui form widget to the canvas and select the created group. 3. Add an element in the form widget and give it a name and label, select the type as multiline, and set the number of rows according to your need. !["Taking user input for Sentiment analysis using form"](https://flowfuse.com/blog/2024/01/images/sentiment-analysis-form.png "Taking user input for Sentiment analysis using form") ## Normalizing the data We need to normalize the payload before sending it to the next node because the form widget always returns an object containing the property of values of form elements. 1. Drag a change node to canvas. 2. Set `msg.payload.$FORM_ELEMENT_NAME` to `msg.payload`, replace the `$FORM_ELEMENT_NAME` with the name of the form element that you have added to the form to obtain user input. 3. Connect the UI form nodes output to the change node’s input. !["Normalizing the payload using change node"](https://flowfuse.com/blog/2024/01/images/sentiment-anlaysis-change-node\(1\).png "Normalizing the payload using change node") ## Installing custom node Now it’s time to install a custom node that can perform sentiment analysis for us. In this guide, we will use the `node-red-node-sentiment` which is a Node-RED node that uses the AFINN-165 wordlists for sentiment analysis of words. It returns a sentiment object containing a score and other properties but we will only use the score property. Score property typically ranges from -5 to 5. 1. Install the `node-red-node-sentiment` package by the Node-RED palette manager. 2. Drag a sentiment node to canvas. 3. Connect the change nodes output to sentiment node input. ## Calculating percentage Why do we need to calculate the percentage? We will show the final result with the help of a circular progress bar and three different emojis. Ideally we should show the progress bar based on a percentage of score instead of negative values. 1. Drag another change node to canvas. 2. set `msg.payload` to `((msg.sentiment.score - (-5)) / (5 - (-5))) * 100` as a JSONata expression, it will calculate the percentage of the score. !["Calculating the percentage based on the score using the change node"](https://flowfuse.com/blog/2024/01/images/sentiment-analysis-change-node\(2\).png "Calculating the percentage based on the score using the change node") ## Displaying result on Dashboard 2.0 Finally, we are going to display the result on Dashboard 2.0 with the help of the Vuetify circular progress bar and emojis. To do that we will build a Vue component by using our ui template widget. 1. Drag a ui template widget to canvas and create another group for it. 2. Paste the below Vue component snippet into the template widget. We're aware that not everyone coming into Dashboard 2.0 will be familiar with VueJS. We have a more detailed guide [here](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#building-full-vue-components){rel=""nofollow""}, but we'll also give a quick overview of the component that we'll use to display the result: ```html ``` - v-progress-circular is a Vuetify component to display a circular progress bar, for a detailed guide refer to our blog on [Custom Vuetify components for Dashboard 2.0](https://flowfuse.com/blog/2023/10/custom-vuetify-components-dashboard/). - `rotate` is an attribute that lets you specify the rotation angle of the progress bar. - `size` and `width` allow you to set the size of the circular progress bar, and another `width` attribute allows you to set the stroke width of the circular progress bar. - v-if, v-else-if, and v-else, allow dynamic rendering of elements based on specified conditions, in this component we are rendering emojis based on percentages calculated by score. Your final flow should look like this: !["Node-RED flow to do sentiment analysis"](https://flowfuse.com/blog/2024/01/images/sentiment-anlaysis-flow.png "Node-RED flow to do sentiment analysis") ## Deploying the Flow !["Deploying Sentiment analysis Node-RED flow"](https://flowfuse.com/blog/2024/01/images/sentiement-analysis-flowfuse-editor.png "Deploying Sentiment analysis Node-RED flow") Finally, we have successfully built our sentiment analysis system. Now it's time to deploy the flow, to do that click on the red deploy button which you can find in the top right corner. After that go to `https://.flowfuse.cloud/dashboard` :video{ariaLabel="Sentiment analysis on Text using Node-RED Dashboard 2.0" autoPlay="true" height="306" loop="true" muted="true" playsInline="true" preload="none" width="600"} ## Conclusion In this post, a sentiment analysis system is built with Node-RED in which the user has a form field to paste text content. After submitting the form, it calculates the percentage based on the output score, which ranges from -5 to 5. The output will be displayed on dashboard 2.0 by a circular progress bar and three different emojis based on percentage. # FlowFuse is now SOC 2 Type 1 Compliant FlowFuse achieved SOC 2 type 1 compliance! SOC 2, governed by the American Institute of Certified Public Accountants (AICPA), is a crucial framework for organizations handling customer data. An independent audit assessed that FlowFuse's controls are effectively designed and operationally applied. Achieving SOC 2 Type 1 compliance validates our practises as an business and provides our customers assurances we apply the highest standards to ensure their data is protected. ## Improving Our Security Posture At FlowFuse, we understand that professionalizing Node-RED deployments for our clients means adhering to the highest standards, including SOC 2 requirements. This commitment is at the core of our security philosophy. In a world rife with cybersecurity threats and data breaches, taking information security seriously isn't just an option, it's a critical necessity. Our SOC 2 audit was far more than just a procedural step. It represented a comprehensive, independent third-party validation of our robust controls and processes. We believe in transparency and accountability, which is why we document our policies in our open handbook, inviting scrutiny from vendors and reinforcing trust with our customers. Providing this level of independent audit not only serves our customers better and more efficiently but also offers FlowFuse valuable insights into enhancing our security measures and identifying any gaps in our policies. This proactive approach ensures we continue keeping your data safe and secure at all times. As we continue to grow and evolve, ensuring the security of our systems and data becomes ever more critical. The next step on FlowFuse's journey to provide independant proof we're on the right track: We're currently in the observation phase of the SOC2 type 2. SOC 2 Type 1 assesses the design of an organization's security controls at a specific point in time, while SOC 2 Type 2 evaluates the effectiveness of those controls over a period of time, typically three to twelve months. ## FlowFuse's Journey to SOC 2 Compliance ### Compliance Partners The independent audit was performed by Advantage Partners. Their expertise played a large role in our successful attainment of this certification. Before the audit was performed the company went through an extensive process to uncover what policies were missing, required updating, or were already in place. Further, lots of tribal knowledge has been written down and is now enforced by internal policies. For example 1. [Data Management Policy](https://flowfuse.com/handbook/company/security/data-management/#data-management-policy) 2. [Access Control Policy](https://flowfuse.com/handbook/company/security/access-control/#access-control-policy) 3. [Incident Response Policy](https://flowfuse.com/handbook/company/security/incident-response/#incident-response-plan) 4. [Human Resources Security Policy](https://flowfuse.com/handbook/company/security/human-resources/#human-resources-security-policy) It's been a team effort from engineering to updated HR polices! # Speech-Driven Chatbot System with Node-RED Have you ever wanted to integrate speech recognition and synthesis into your Node-RED project and thought it was too complex? Often it has required external services or APIs. However, in this guide, we show you how you can use speech recognition and synthesis in your Node-RED projects without needing an external service or API. In addition, we make things more interesting by building a system that can listen to us and respond like humans using the Chat-GPT API. Let's get started! ## What exactly is speech recognition and synthesis? Speech recognition is a technology where a device captures spoken words through a microphone, checks against grammar rules and vocabulary, and returns recognized words as text. On the other hand, speech synthesis converts app text into speech and plays it through a device's speaker or audio output. There are many benefits and real-world applications of this technology. - **Hands-Free Operation:** Using speech recognition technology is often used today to perform tasks such as making calls, sending messages, or controlling smart home devices without the need for physical interaction. - **Accessibility:** It allows individuals with visual impairments to access digital content through spoken words and as discussed above, to control devices without physical interaction. - **Efficient Content Consumption:** It allows us to listen to information instead of reading. For example, in the audiobook industry by using speech synthesis technology they create audio versions of books which helps users to be more productive. ## Installing Dashboard 2.0 Install Dashboard 2.0. Follow these [instructions](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to get up and running. ## Building Speech-to-Text Vue component In this section, we will build a Vue component that will perform a speech-to-text conversion operation using Web speech API, and display results on the dashboard. While we did say previously that we won't need any external API for speech recognition, this [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API){rel=""nofollow""} is not an external API. This will process your speech locally as it is a JavaScript API that allows us to use speech-related functionalities, such as speech recognition and synthesis, in a web browser directly. It is widely present in modern browsers (except Firefox) which eliminates the need for external APIs to implement these features. Let's now start to build that component. 1. Drag a ui template widget to canvas and select the created group. 2. Paste the below Vue snippets into the template widget step by step. If you are unfamiliar with Vue, we have added comments that will help you to understand the code better. We are going to start by pasting a user interface’s snippet which will allow us to interact with our system. This snippet adds a button that triggers our system to listen, an Icon, and a paragraph to display speech recognition results on the dashboard. ```html ``` Now paste the below script right after the HTML in the template widget, This script adds functionality of speech recognition in our system. ```html ``` ## Adding an Environment variable Why do we need to add an environment variable? In this guide, we will build a speech-driven chatbot that involves integrating the Chat-GPT AI model. For this we need openAi’s API key. An API key is a form of private data that needs to be protected from being exposed. That is why we need the environment variables. It provides a secure way to store and access the API key without revealing it directly in the flow. For more details see [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/) 1. Navigate to the instance's setting and then go to the environment section. 2. Click on the `add variable` button and add a variable for Chat-gpt API. !["Setting environment variable for Chat-gpt token"](https://flowfuse.com/blog/2024/01/images/speech-driven-chatbot-environment-section.png "Setting environment variable for chat-gpt token") ## Setting msg property Now let’s set that added environment variables as msg's property. 1. Add a change node to canvas. 2. Set environment variable to ms.token property. 3. Connect the change node’s input to the template widget’s output. !["Setting msg's property for Chat-gpt token"](https://flowfuse.com/blog/2024/01/images/speech-driven-chatbot-change-node.png "Setting msg's property for Chat-gpt token") ## Installing and configuring custom node In this section, we will install a custom node that will allow us to interact with the Chat-gpt AI model. 1. Install `@sumit_shinde_84/node-red-contrib-node-gpt` by pallet manager, you can use other nodes according to your preference. 2. Drag a ChatGPT node to canvas. 3. Connect the ChatGPT node’s input to the change node’s output. ## Building Text-to-Speech Vue component We will build a Vue component that converts text received from ChatGPT into speech. 1. Drag another template widget to canvas and select the added group, alternatively, you can create a separate group for this component according to your preference. 2. Paste the below Vue snippets into the template widget. 3. Connect the template widget’s input to the ChatGPT node’s output. Paste the below snippet in the template widget which displays chat-gpt response on the dashboard ```html ``` Paste the below snippet right after the HTML, This snippet adds the functionality of text-to-speech into our system, which triggers when msg received by the previous node. ```html ``` Your final flow should look like this: !["Speech Driven Chatbot system flow"](https://flowfuse.com/blog/2024/01/images/speech-driven-chatbot-flow.png "Speech Driven Chatbot system flow") ## Deploying the Flow !["Deploying Sentiment analysis Node-RED flow"](https://flowfuse.com/blog/2024/01/images/speech-driven-chatbot-flowfue-editor.png "Deploying Sentiment analysis Node-RED flow") We have successfully built our Speech-Driven Chatbot System. Now it's time to deploy the flow, to do that click on the red deploy button which you can find in the top right corner. After that go to `https://.flowfuse.cloud/dashboard` :video{ariaLabel="Speech Driven Chatbot using Node-RED Dashboard 2.0" autoPlay="true" height="400" loop="true" muted="true" playsInline="true" preload="none" width="648"} ## Conclusion In this guide, we have built a Speech-Driven Chatbot System which allows us to understand how we can add speech recognition and synthesis features into our project without any external API or custom node. It also provides a brief overview of how we can integrate chat-gpt into our system. # Selecting a broker for your Unified Namespace When starting to roll out a new data distribution architecture for the unified namespace (UNS), one of the first questions you'll ask is, "What broker should I select for my UNS? The broker must implement a publish-subscribe (pub-sub) pattern, though that leaves plenty of options. ## Technology selection ### Two protocols frontrunners Currently, there are two protocols that are front runners for becoming the de facto data transfer choice in (industrial) IoT: [MQTT](https://flowfuse.com/blog/2024/06/how-to-use-mqtt-in-node-red/) or [Kafka](https://flowfuse.com/blog/2024/03/using-kafka-with-node-red/). They’ve been designed for different use cases and have different properties. At this time, MQTT is more often deployed as a broker in the unified namespace and is generally the best choice when starting to implement a unified namespace, it also features better support from hardware vendors. First and foremost, MQTT has been designed to enable IoT use cases. The main design objectives were to be lightweight to enable low-bandwidth communication, enable low-power devices, and handle unreliable networks. MQTT enables a large number of data producers and consumers to collaborate. Kafka is designed as an event streaming platform. Its initial adoption was mostly for data brokers between microservices all part of the same web backend for large sites like LinkedIn. When communicating data between servers or just a few data centers around the world, there’s less of a concern around the reliability of the connection or to enable constrained devices to participate in the shift towards a unified namespace. Generally, MQTT is more often seen deployed in practice as data distribution architecture. ### The Cloud route Cloud message queue brokers like AWS Kinesis and GCP Pub/Sub offer a high level of convenience. Scaling the infrastructure for real-time data processing and communication is their concern, the customer is mostly concerned about paying the bill. These brokers are fully managed, meaning they are maintained and updated by the cloud provider, reducing the burden on developers. However, this convenience comes with the tradeoff of vendor lock-in. When selecting these brokers, the cloud vendor has usually adapted their technology to support many protocols, and these offerings are usually jack-of-all-trades solutions – master of none. It creates a situation where the unified namespace implementation will change in subtle ways to accommodate the vendor instead of the other way around. An organization might become so reliant on a particular vendor's products or services that they are unable to easily switch to another vendor or protocol that serves their business objectives better. The cost of changing is exacerbated by having to train personnel on new and open-source protocols. In addition to vendor lock-in, cloud message queue brokers also introduce reliance on the network to the cloud providers. Network reliability for (industrial) IoT is a major concern due to the physically distributed nature, adding external dependencies creates more variability. ### Exotic options RabbitMQ is a widely used open-source message broker that’s mostly used as an event message bus for web applications. It can also function as a hub in a unified namespace. The broker primarily supports the [AMQP](https://flowfuse.com/docs/node-red/protocol/amqp/) (Advanced Message Queuing Protocol), considered the industry standard for high-performance messaging systems. It also supports STOMP (Streaming Text Oriented Messaging Protocol) and MQTT (MQ Telemetry Transport), catering to various messaging needs. NATS, short for Network Agnostic Messaging System, is another open-source message broker that is designed for simplicity and reliability. NATS implements its own protocols, making it harder to be interoperable with hardware and software previously purchased. NATS has requirements on message structure too, which creates another barrier to adoption for IoT use cases. ## How Node-RED Helps Node-RED provides a powerful and flexible way to integrate with various brokers, supporting protocols such as [MQTT](https://flowfuse.com/blog/2024/06/how-to-use-mqtt-in-node-red/), [Kafka](https://flowfuse.com/blog/2024/03/using-kafka-with-node-red/), and [AMQP](https://flowfuse.com/docs/node-red/protocol/amqp/). It allows you to build and manage workflows that interact with your chosen broker, seamlessly connecting different data sources and systems. However, using Node-RED alone in production environments requires additional considerations, such as server deployment, instance management, security implementation, and scalability. This is where FlowFuse enhances Node-RED's capabilities by adding production-ready features. FlowFuse simplifies managing and deploying Node-RED applications, providing essential functionalities like scalability, robust security, and efficient collaboration tools. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and experience how FlowFuse can streamline your Node-RED deployments and management.** ## Conclusion An MQTT broker is currently recommended as a broker solution for your unified namespace. There are many different implementations of the protocol available. At FlowFuse, we’re using [Mosquitto](https://mosquitto.org/){rel=""nofollow""}, due to its efficiency on resources and flexible authentication layer. Further, our customers are reporting to be happy with [EMqX](https://www.emqx.io/){rel=""nofollow""}, which is written in Erlang – itself a messaging-oriented programming language – and has been put through its paces in practice. If you’re dipping your toes into the unified namespace, either of those or another MQTT broker is currently recommended. Note that it’s recommended to allow yourself flexibility in the broker, and treat it as a message-passing system, and your organization will be able to easily swap it out later if any other broker is a better fit later on. # Unified Namespace: When to Use It, and When to Choose Something Else At FlowFuse, we're convinced of the Unified Namespace (UNS) architecture for IoT cases. It's a powerful tool that can make information much more readily available and easy to consume. However, as with any architecture, there are times when it's not the best choice. In this blog post, we'll discuss when to use the UNS and when to consider other options. ### Latency sensitivity When automating strictly digital tasks, there's generally no or very lenient requirements on latency. Requesting APIs from another server, normalizing data, and sending it towards another service takes very little time, though it hardly matters if you’re 100ms later than usual. In industrial automation or other cases where physical safety is safeguarded, there’s a requirement for low latency to ensure that data is transmitted and processed quickly enough to maintain real-time control of processes. Extending this beyond the UNS – For controlled use cases, use specialized software and do not rely on third parties to broker the information correctly. However, while control was used as an example here, the general case here is not the UNS for real-time, low-latency communication. ### Large files or binary data Sending large blobs of binary data, such as pictures and archives, through UNS is not recommended due to several drawbacks. First, UNS is optimized for small, mostly text-based communication, and sending binary data through it can significantly increase message size and processing overhead. As the sender of the data, there’s also generally no control over the number of receivers of the data, so if you send a large file to the broker once, it might need to be copied many times for every receiver. To overcome these limitations, it is more efficient and practical to store binary data elsewhere, such as on a shared storage location or a cloud service. Once the data is stored externally, a reference to its location can be sent through UNS. ### Data security and data access The UNS becomes more useful the more hubs are connected, and it stands to reason that better business outcomes are achieved when everything is connected. Not just the sensor data (Level 1 of the [automation pyramid](https://flowfuse.com/blog/2023/08/isa-95-automation-pyramid-to-unified-namespace/#automation-pyramid-visualization) *except actuators*), but also the customer-facing systems like your ERP (Level 4 or the automation pyramid), and even your CRM, Customer Relationship Management. This would allow better insight and communication with the customer when, for example, their car is done with production and will be shipped to them. The CRM contains personal details about the customer. Publishing what a customer ordered might thus disclose personally identifiable information (PII). Connecting a CRM is certainly possible, and could streamline the supply chain, but PII shouldn’t be published. Consider if there’s a unique ID for the customer instead to use. Does your CRM, for example, keep a customer ID? If not, mask the information instead. Use strong hashing algorithms like SHA-512 to hash their email addresses. This way, other systems can cross reference messages received in the UNS, without ever knowing the customer PII. Another venue to explore is topic-based authentication for data receivers, which most data brokers provide. However, stripping and masking PII is still highly recommended either way. ### In Conclusion The Unified Namespace is a powerful architecture that can make IoT data more accessible and easier to consume. However, it's not always the best choice for all applications. When considering whether to use the UNS, be sure to weigh the benefits against the potential risks. If you need low latency, strong data security, or fine-grained control over data access, you may need to adjust your architecture pattern or instantiate point-to-point connections through REST or other interfaces. # Connect Node-RED to KepserverEX OPC server. KepserverEX, often referred to as Kepware, is an OPC server that has been the important tool many manufacturing companies have used on their digital transformation journey. It plays an important role for many to extract data from PLCs, Programmable Logic Controllers, without having to directly interact with them. ## PTC's KepserverEX PTC's [KEPServerEX](https://www.ptc.com/en/products/kepware/kepserverex-ppc){rel=""nofollow""} is a versatile connectivity platform designed to securely manage, monitor, and control diverse automation devices and software applications. Central to its functionality is the OPC standard, which enables universal communication across industrial hardware and software, facilitating data exchange. This makes KEPServerEX particularly valuable in a variety of use cases, such as real-time data monitoring, machine-to-machine (M2M) communication, and industrial Internet of Things (IIoT) applications. It serves as a critical bridge in the automation and controls engineering space, offering a robust solution for integrating disparate systems, thereby enhancing operational efficiency and enabling data-driven decision-making. Integrating KEPServerEX with Node-RED extends this functionality, by allowing bidirectional communication for sending, storing, and or manipulating data. ### Scope The goal of this blog is for a quick start guide on the configuration for collecting data from a KepserverEX OPC server. We are going to be leveraging the [node-red-contrib-opcua](https://flows.nodered.org/node/node-red-contrib-opcua){rel=""nofollow""} node. We will assume that you already have [KepserverEX](https://www.ptc.com/en/products/kepware/kepserverex-ppc){rel=""nofollow""} install and ready for the integration. We will be using Basic256Sha256 security in this guide with anonymous authentication. Assumptions of the installation include allowing Default configuration of the installation of KepserverEX 6.15 and allowing dynamic tag addressing. ### Configure Connection from Node-RED to Kepserver #### Step 1: KepserverEX The first thing we need to do is check our **OPC UA Configuration Manager** for the security requirements for our environment. In the tray at the bottom, click on the KepserverEX symbol and select **OPC UA Configuration** ![kepware tray](https://flowfuse.com/blog/2024/02/images/kepserverex-tray.png) If your Node-RED instance lives on the same server that your KepserverEX is on, pick accordingly or click add if you need to define by ip address. This is for setting different credential requirements for localhost vs remote host access. Also note, that if you have multiple network adapters, make sure to select the adapter that is in use. :cta-image{alt="Aperia Technologies stopped reprogramming controllers station by station with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/aperia-book-demo.png"} ![kepware endpoint definition](https://flowfuse.com/blog/2024/02/images/kep-endpoint-definition.png) We are testing locally on the server, so we will use the one selected for loopback addressing. We will be leaving the OPC server port as default and select **Basic256Sha256** with **Sign and Encrypt**. Click **OK**. #### Step 2: Node-RED Next, navigate to your Node-RED instance and install the [node-red-contrib-opcua](https://flows.nodered.org/node/node-red-contrib-opcua){rel=""nofollow""} node if you haven't already done so. Import the flow below into your Node-RED environment. :iframe{allow="clipboard-read; clipboard-write" height="225px" src="https://flows.nodered.org/flow/04a84fe5b0db7cda9e74ba811e7b0ca5/share?height=250" style="border: none;" width="100%"} Next, let's configure the **OPC UA Client**. Click the **pencil** to add a new OPCUA-Endpoint. ![kepware node-red encrypted opc ua node](https://flowfuse.com/blog/2024/02/images/opcua-endpoint-node-red-encrypted.png) For the endpoint, **copy** the endpoint definition from the KepserverEX OPC UA Configuration Manager. In our example, it is `opc.tcp://127.0.0.1:49320`, and paste it into the Endpoint. For SecurityPolicy select **Basic256Sha256**. For SecurityMode, select **Sign\&Encrypt**. Lastly, we will be selecting **Anonymous**. Click **Update**, then **Deploy**. Trigger the flow by **clicking** on the inject node. The server may not connect at this time, and it is expected. ![kepware node-red invalid endpoint](https://flowfuse.com/blog/2024/02/images/node-red-opc-ua-invalid-endpoint.png) #### Step 3: KepserverEX Moving back over to KepserverEX, Click on the tray again in the bottom of the screen and select **Configuration**, then select **Edit** from the file menu, then **Properties**. Next, Select **OPC UA** and ensure that **Allow Anonymous login** is set to **Yes**. Click **OK**. ![kepware node-red anonymous login](https://flowfuse.com/blog/2024/02/images/node-red-kepware-anonymous-login.png) Select the tray at the bottom of the screen again, and select **OPC UA Configuration**. Select the **Trusted Clients** tab. ![kepware node-red trusted client before](https://flowfuse.com/blog/2024/02/images/kepserverex-trusted-client-before.png) Now select the **NodeOPCUA-Client** and then **click** Trust. ***If you don't have the client option, trigger the inject node again from the Node-RED flow and check the logs*** ![kepware node-red trusted client after](https://flowfuse.com/blog/2024/02/images/kepserverex-trusted-client-after.png) #### Step 4: Node-RED Lastly, Navigate back to Node-RED and **trigger** the inject node. This node will now browse the project from the KepserverEX and display all of the existing tags. ![kepware node-red browsed tags](https://flowfuse.com/blog/2024/02/images/kepware-opc-browsed-tags-node-red.png) ### Read Tags We will be leveraging the default Simulated Examples for reading tags from KepserverEX. Let's move on to the next set of flows. ![kepware node-red read tags](https://flowfuse.com/blog/2024/02/images/node-red-kepware-read-tag.png) Edit the OPCUa-Item node and note the item. ```text ns=2;s=Simulation Examples.Functions.Ramp1 ``` Let's break down the syntax, ns stands for namespace that will coincide with the project. In this case, it is namespace 2. Once the namespace has been selected, we are use **Dynamic Addressing** to select the tag through the variable **s**, which stands for string type of NodeId. **Click** Done. Now let's **trigger** the Read inject node and view the debug output. The debug node is set to show a complete msg object. Note the payload as the value of the variable. ![kepware node-red read tags output](https://flowfuse.com/blog/2024/02/images/node-red-debug-output-opc.png) ### Write Tag Writing a tag is a similar process. The only difference is that a variable is set in the **OPCUa-Item** node and the **OPCUa-Client** action is set to Write. In this example, we created a new variable in KepserverEX under **Simulation Examples > Functions** called myInt of Date Type Long. ![kepware node-red write tags](https://flowfuse.com/blog/2024/02/images/node-red-kepware-write-tag.png) View the OPCUa-Item node and note the item. ```text ns=2;s=Simulation Examples.Functions.myInt ``` **Click** Done and **Deploy** Open up the **Quick Client** within KepserverEX and navigate to the address of **Simulation Examples.Functions** and look for myInt. It should by default, be 0. **Trigger** the inject node within Node-RED to see the Value change within the Quick Client. ![kepware node-red write tags output](https://flowfuse.com/blog/2024/02/images/node-red-debug-output-write-opc.png)![kepware node-red quick client](https://flowfuse.com/blog/2024/02/images/kepware-quick-client.png) ### Conclusion This guide was designed to help you easily connect your Node-RED instance to KepserverEX with security. For more examples of how to do more advanced configuration, please watch the past [webinar](https://flowfuse.com/webinars/2023/getting-started-opcua-node-red/) going over these [examples](https://github.com/mikakaraila/node-red-contrib-opcua/tree/master/examples){rel=""nofollow""} in detail. KepServerEX is one option for an OPC UA server; see [FlowFuse's OPC UA client and server capabilities](https://flowfuse.com/integrations/opcua/#opc-ua-client-and-server-capabilities) for its own certified OPC UA node, including hosting a server directly instead of going through a gateway. # History of Node-RED In January 2013, I could have never foreseen that my fun little proof-of-concept project would become Node-RED, an open source low-code environment with millions of deployments in IoT and automation. Long before IoT became the ubiquitous term it is today, I was working in IBM’s Emerging Technology Group playing around with capturing data from devices and doing interesting things with it. The team focused on very fast-paced, short, proof-of-concept projects and was afforded time to learn new skills, innovate and work on side projects. My background working with the MQTT protocol space before it was known outside of IBM led me to a side project: I wanted some way to visualize mapping messages on an MQTT infrastructure to see how they come in on one topic and get sent out on another. Using it as an excuse to also start playing the relatively new Node.js runtime, I spent a day or two putting together a little demo of an application that would connect to an MQTT broker and visualize the topic mappings in a web browser. Showing it to my colleague, Dave Conway-Jones, I mentioned it wouldn't take much to make it more interactive; to let you draw the mappings and apply them. He sent me on my way to do just that and, 24 hours later, I had a simple browser-based application that could define and apply mappings between MQTT topics. It very quickly became useful. ![](https://flowfuse.com/blog/2024/02/images/history-nr-screenshot.png) An early screenshot of Node-RED The projects Dave and I were working on became rich sources of requirements; each project needing to access data from some other source such as a device plugged into a serial port, or being able to modify the data with a bit of JavaScript code. I spent a few days redesigning the code to make it easier to write in new nodes, unlocking the ability to quickly add in the function node, change node, and switch node, which became the basic building blocks of the tool. The utility of the application was clear, and more colleagues started making use of it - all grounded in real client projects. But it was still a tool largely known only to our team. ### Going Open Source As we considered how best to move the project forward, we essentially had two choices; keep it to ourselves and see if we could get an IBM product group to back turning it into a fully-fledged product, or to go the open source route. In my mind, the OSS route was the natural fit and that is what we ultimately chose to do, getting it published in late 2013. I demoed Node-RED in late 2013 at a London IoT meetup and word spread among my peers in that community. A week later, I was at an open source hardware conference and attended a workshop on home automation. I was surprised to see Node-RED on everyone’s screens! The facilitator had seen Node-RED and reworked his workshop so that people didn’t have to worry about writing lines of code and were able to do useful things much more quickly. ![](https://flowfuse.com/blog/2024/02/images/history-ibm-lab.jpg) The IBM Emerging Technology Demo Lab - many of the demos were built on Node-RED ### Building an audience A key step forward was when IBM was preparing to launch its new IBM Cloud service. In the months before the launch, they were looking for innovative ideas that could help expand the offering. We put forward a proposal to use this tool we'd created as a way to visualise the mapping of web services within the cloud. This generated some great interest from a wider audience within the company, and whilst that concept didn't ultimately come to anything, we had gotten our project noticed. Over time, we started seeing Node-RED being picked up by more than just the OSS community. Companies started using it with their own hardware devices and online services. By this time, IBM Cloud had launched, and from our previous conversations with them, we got Node-RED included as one of the 'starter applications' in the catalogue that gave users a one-click option for getting Node-RED running in the cloud. ![](https://flowfuse.com/blog/2024/02/images/history-cloud-catalog.png) Node-RED in the original IBM Cloud catalog ### Moving to a Foundation As we saw the project grow, discussions were had around the longer-term future of the project. Some companies voiced a concern about it being a single-vendor open source project. This also came at a time when IBM was actively working with the Node.js project to help relaunch the Node Foundation (which has since become the OpenJS Foundation). This culminated in Node-RED joining the foundation as one of its founding projects, alongside other well-established projects such as Node.js itself, jQuery and many others. Having an independent governance structure around the project gave companies more confidence to get involved, knowing they had an equal voice in its development. Hitachi became big supporters of the project and had a team dedicated to working on it. For software developers, time spent writing boilerplate code is not time adding value to the application they’re building. With low-code, Node-RED abstracts all that boilerplate so they can focus on the business problem. Device manufacturers paid attention when Node-RED was installed on the Raspberry Pi image, with its low-code accessibility attracting a broad range of people from systems engineers building automations to IoT hobbyists. Now with millions of deployments, Node-RED continues to collect, transform, and integrate data through visualized dashboards. And, as this open source community grows it remains rooted in the two pillars of its low-code user experience, and its extensibility. # Node-RED: The perfect adapter and middleware for your UNS Digitalization is at the inflection point where it’s been adopted enough that the additional investments provided better and better ROIs for organizations. The next bump in ROI will be achieved through the UNS. A torrent of information is more useful when structured and adapted for new use-cases. Either a [performance dashboard](https://flowfuse.com/blueprints/manufacturing/performance-overview/), Artificial Intelligence, or station metrics – each is built faster when the data is readily available and well structured? As a company started around Node-RED, we’ve not spoken a lot where FlowFuse fits into the picture, which is what this post is about. ## Adapting legacy machines to the UNS The digitalization effort in traditional industries like manufacturing, agriculture, and beyond, has additional challenges due to the high capex assets that need to join in the effort. As these assets will not be replaced, the only way is to adapt them. Adaptation will require tooling that can interact with sensor data regardless of the protocol, data format, and data structures. Node-RED bridges the gap between analog and digital data acquisition by seamlessly integrating with a vast array of protocols including serial bus support, Modbus, MQTT, and OPC-UA. Its format agnostic nature allows it to handle diverse data formats, from parsing binary data, to JSON, Protobuf (Sparkplug B), making it a versatile tool for extracting and manipulating data from various sources. With its widespread adoption, Node-RED ensures compatibility with almost every protocol, enabling users to connect and process data from a wide range of devices and applications. ## Contextualisation of the data When data is captured and parsed, it needs to be contextualized. For example; in a UNS the topic hierarchy on which to publish and subscribe to is based on location – that is; context. Furthermore, a raw sensor reading might miss details like the unit of measurement, what message version is required, or doesn’t supply the information in a proper type. All these minor niggles are actual blocking issues for adopting all sensors to the UNS. In some cases makes and models of the sensor might influence the tolerances of readings, it’s a good idea in those cases to include that information in the message. ## Filtering and preprocessing Not all messages are created equal, which was discussed in [an earlier post](https://flowfuse.com/blog/2024/01/unified-namespace-when-not-to-use/.). In two of the three examples provided in that post, Node-RED can preprocess or filter information before sending it to the UNS. In the case of big files or binary data, Node-RED can store it in S3 or a network attached storage layer, or even store it locally through a REST interface. The distribution of the event that created the binary data is still published through the UNS though. Filtering of data is also a great use-case for Node-RED, generally just a `change` node and the data is ready to be published. The flexibility to do virtually anything with captured data is what makes Node-RED such a strong partner for your UNS. ## Continuous improvement While it would be ideal if data schemas were stable, changes are frequent and unpredictable. It’s a non-obvious requirement for your UNS edge to be adaptable. Message structures can change, to add or remove data from them. Though also the format, from JSON to Sparkplug B, or maybe to XML. Not to say that standardization of messages will continuously require updates to leverage the UNS for higher business value. A swiss-army knife as both data sender and receiver is not just a nice-to-have, it’s a requirement. ## Scalable operations While there are other tools available that can adapt to a few protocols, or parse a handful of data formats, there’s no alternative for Node-REDs breadth and depth of integration level. This is why many organizations have already adopted Node-RED for their edge cases, which their current standard solution doesn’t handle. There’s no situation where a vendor provided, off-the-shelf solution handles protocols and formats across vendors, modern and legacy OT, that also satisfies the IT requirements unless the extensibility is handled through an Open-Source community, with compliance and security controls from a professional entity surrounding the open source project. ### How FlowFuse Enhances Node-RED While Node-RED is powerful for implementing UNS, its management and deployment can be complex. FlowFuse simplifies this process with a unified platform that offers one-click deployment, secure management, and scalability for Node-RED applications. It enhances collaboration through centralized management of all Node-RED instances, ensuring streamlined operations and increased efficiency. **[Sign up](https://app.flowfuse.com/account/create){rel=""nofollow""} now for a free trial and explore how FlowFuse can transform your Node-RED experience.** # Node-RED in a Unified Namespace Architecture As we embark on the journey toward a more interconnected industrial environment, the emergence of a Unified Namespace (UNS) as the fundamental framework for facilitating communication among various systems and devices has become a focal point of discussion. I've often been queried about the role of Node-RED in a UNS architecture. To illuminate this, let's delve into an exemplary UNS architecture, underlining the classic use cases for Node-RED within this framework. In this Article: [Node-RED: The perfect adapter and middleware for your UNS](https://flowfuse.com/blog/2024/02/node-red-perfect-adapter-middleware-uns/), where classic use cases for Node-RED are delineated. Building upon that foundation, this discussion aims to present a tangible architectural example, showcasing the practical implementation of these use cases. ## Simplifying Complexity: The Two-Layered Approach For ease of understanding, consider the architecture split into two principal layers: the Shopfloor layer and the Service layer. **The Shopfloor Layer** This is where the physical assets dwell, requiring connectivity through a network layer, which in our example, is the UNS. It acts as the foundation for data flow from the operational technology (OT) on the shop floor to the information technology (IT) in the Service layer. **The Service Layer** Here resides the applications and software that analyze data, transforming raw metrics into actionable insights. It's where the data becomes meaningful through analytics, dashboards, and decision-support tools. ## The Roles in UNS Architecture Within this bifurcated architecture, we have two general categories of actors: Indirect Consumer/Producers and Direct Consumer/Producers. **Indirect Consumer/Producers**: These actors cannot natively communicate with our UNS broker. The communication barrier could be due to protocol differences, such as not using MQTT, or payload structures incompatible with your UNS's schema. This is a common challenge in manufacturing, especially in "brownfield" scenarios where legacy machines and equipment from various eras must be integrated. In such cases, Node-RED shines as a middleware for protocol conversion and data contextualization. Take, for example, the topic hierarchy in UNS based on location for context. A raw sensor reading might lack necessary details like measurement units or message versions. Node-RED steps in to enrich this data, ensuring compatibility with the UNS. In our [concrete architecture example](https://flowfuse.com/blog/2023/12/unified-namespace-data-modelling/), we have an indirect PLC producer. With Node-RED, we can convert and contextualize the data from this PLC for the UNS, ensuring smooth communication and effective integration. **Direct Consumer/Producers**: Contrastingly, direct actors can interact with the UNS out of the box. Modern industrial equipment usually falls into this category, equipped to speak the language of the UNS directly. However, the challenge remains not just in protocol communication but also in data contextualization. Merely speaking the same language is not enough; the data must also carry the correct context to be fully understood and utilized. ![Example Architecture](https://flowfuse.com/blog/2024/02/images/unified-namespace-architecture.png) ## Harnessing Node-RED for Actionable Insights Node-RED's prowess extends beyond middleware capabilities; it can also derive actionable insights. Our example architecture includes Dashboards for both the Human Machine Interface on the Shopfloor and an OEE Dashboard in the Service Layer. These dashboards engage with the UNS, calculating KPIs directly within Node-RED. For manufacturing applications, our [Blueprint Library](https://flowfuse.com/blueprints/) serves as a robust starting point, offering one-click deployment to your Node-RED managed instance. Node-RED emerges not just as a translator between machines and UNS but as an interpreter and analyst, generating real-time insights that drive decision-making and operational efficiency. The role of Node-RED in UNS architecture, therefore, is not ancillary; it is central to realizing the vision of a connected, intelligent industrial ecosystem. ![Andon Live Dashboard](https://flowfuse.com/img/ANDON-Screenshot-D4DBvWieJZ-650.avif) # Should You Invest in Professional Services for Your Node-RED Development? Professional services come in many different forms. Anything from setup and configuration, flow development, to node development. In this article, we will cover a few examples of how you can leverage professional services for your Node-RED applications. ## Should You Invest in Professional Services for Your Node-RED Development? Node-RED is multifaceted, and Professional Services (PS) can apply to many use cases. We will break it down into three categories that will help identify whether PS (Professional Services) is right for you. Those categories will be setup, flow development, and node development. Each takes a different skill set and has different levels of complexity. Before we jump into things, we need to understand the value proposition of Node-RED. Why Node-RED? Node-RED is a tool that makes Citizen Development a reality. It should be treated that way throughout the development journey regardless of Professional Services or not. Following standard procedures that minimize complexity, for example: reducing the use of function nodes when other nodes would be more ideal. Don’t overcomplicate things that don’t need to be or better said by this quote. > Easy things should be easy, and hard things should be possible. > -Larry Wall ### Setup It is often recommended to use PS for the setup and installation of products, and Node-RED and FlowFuse aren't different. This becomes especially true if you desire to professionalize your Node-RED instance. The reason for this is that PS teams focus on minimizing the risk of application failure, security compromise, and data/ip loss. This is an area of PS that can easily be overlooked, mainly there aren't any negative consequences in the short term. The work these teams focus on prevents catastrophic issues in the future. With Node-RED and FlowFuse, these teams will focus on installing FlowFuse based on system requirements and prerequisites, migrating existing Node-RED instances, securing your environment, and integrating with your existing ecosystem of applications. ### Flow development Oftentimes, many think of Flow development when they think of PS for Node-RED. There will be a process flow that needs to be accomplished, but it is leveraging standard nodes built within the Node-RED platform. Occasionally, there will be a need to install a custom node from the pallet manager, but the complexities in the development are adhering to a process and or visualization. There may be complex data translation, but for the most part, access to the command line of the Node-RED instance is unnecessary. Professional services may be used in situations like this where knowledge of specific systems that Node-RED is integrating into is complex. For example, a complex backend database for specific industry-wide used applications. ### Node Development At the core of some integration projects lies a critical requirement: the need for specific integrations that align precisely with your application's demands. This is where Node-RED's open-source nature becomes its most significant asset. Node-RED allows for the development of custom nodes, catering to specialized protocols unique to your organization or to serve niche applications. Such node development isn't just another feature; it represents the foundational element of Node-RED, embodying the platform's core value proposition. Whether addressing the need for custom integration with internal systems or expanding functionality to include less common applications, node development stands as the pivotal mechanism for enabling these capabilities. This critical development can be undertaken in-house, leveraging your team's expertise, or through PS specialized in node development. #### Conflating Flow and Node Development It is important when scoping out a project to identify all of the potential nodes needed and identify any potential missing ones. The skills for developing Nodes and Flow development, while similar, are often different skill sets. Having these identified will help prevent scope creep and setting expectations on timelines. ## Conclusion As we've explored throughout this article, Professional Services for Node-RED can significantly enhance the efficiency, security, and scalability of your automation and integration projects. From the initial setup and configuration to the intricacies of flow and node development, the expertise PS teams offer can be invaluable. The decision to engage with Professional Services should be informed by your project's specific needs, internal capabilities, and strategic priorities. Whether it's leveraging PS for the foundational setup of your Node-RED instance, outsourcing flow development to expedite project timelines, or seeking expert assistance for node development, the goal is the same: create a coding environment that is conducive to citizen development. If you are interested in professional services or consultation, [please reach out](https://flowfuse.com/professional-services/). # Bringing Software Development practices to Node-RED I'm always thinking about how we can continue to improve the Node-RED experience. One area I like to explore is to make sure we learn the right lessons from the Software Development world. In this post, I'm going to look at some of the common practices in modern Software Development and show how they translate to the Node-RED world. ### Linting Linting is a way of examining some code and automatically spotting things that need attention. This can range from stylistic errors ("Use tabs, not spaces") to real bugs that will prevent the code from working as intended. This is all about spotting problems *before* the code runs. It also helps ensure consistency when you have multiple people contributing to the code. Having code that is consistently formatted and free of syntactic mistakes makes it much easier to maintain. Applying this concept to Node-RED, we have the [`nrlint` tool](https://github.com/node-red/nrlint){rel=""nofollow""}. This is a linting tool that can run either on the command-line or within the editor directly to spot potential problems with the flows. On the stylistic side, for example, it can highlight nodes that aren't properly aligned to the grid. Whilst this doesn't have any bearing on the runtime operation of the flow, it encourages keeping the flows tidy and orderly. It can help identify potential infinite loops in flows, and highlight Debug nodes without a name set. ![](https://flowfuse.com/blog/2024/02/images/node-red-linter.png) ### Debugging Whether you are writing lines of code or not, eventually you will need to figure out why your application isn't doing what you think it should. In Software Engineering there are two typical approaches. One is to add debug statements through the code to print out bits of information as the program runs. Then, depending on what output you got, you'd move the debug statements around, add some more, print out different bits of information - all until you'd nailed down the problem. This is the Debug node approach in Node-RED; adding nodes at different points of your flow to capture some piece of information and then iterating as you go. This is probably how most Node-RED users go about it today. The downside is you end up leaving the Debug nodes in place, capturing information long after it is needed. The alternative approach is Step-by-Step debugging. This is why you are able to pause the program and then step it forward one statement at a time - examining the state at each point. But what's the equivalent for low-code? Pretty much exactly that when you have the [Node-RED Debugger plugin](https://flows.nodered.org/node/node-red-debugger){rel=""nofollow""} installed. This allows you to set 'breakpoints' on any node input or output that are triggered when a message arrives at that point of the flow. The Debugger will then pause the whole runtime and shows you all of the queued up messages in the flow. You can then examine those messages and tell the Debugger to 'release' them one at a time - seeing how the flow progresses. ![](https://flowfuse.com/blog/2024/02/images/node-red-debugger.png) ### Testing Testing code is a critical part of developing software. You want to make sure it does what you want. But it's more than just manually testing it once and then letting it go; you want to have tests you can run regularly, whenever you make changes, to ensure you don't break something that was working previously. In the Software Development world, there are all sorts of testing methodologies and techniques; unit testing individual components, system testing larger sections, stubbing out components to simulate different conditions, integrating test suites into the whole development process. They each have their own place in the process of software development. The question is, how does this apply to Node-RED? Most Node-RED users today will of course be testing their flows whilst developing them - iterating until the flow does what is needed. It's far less common to have a set of repeatable tests including the flows. That is certainly achievable with Node-RED today, albeit with some limitations. For example, a typical test will be to verify that, given a set of particular inputs, the outputs look correct. This can be done using Inject nodes to quickly trigger messages with different values, and use Debug nodes to examine the results. That isn't ideal as you end up littering your flows with these extra Inject nodes, and it still requires manually verifying the results. It also doesn't work well if your flows need to interact with external systems - such as saving values to a database. You don't want to pollute your system with test data. So what would be the ideal workflow? This is something the project has spent some time exploring in the past, and the start of a design was put together. The concept would be to introduce a Testing sidebar to the editor. Within that, you can define a set of test cases. Then for each test case, you can customise the behaviours of individual nodes. For example, a test case may disable an MQTT node at the start of a flow, and tell the runtime to inject a message in its place. For each subsequent node in the flow, the test case would then be able to either to bypass it (to avoid interacting with external systems), or to add checks on what the node outputs. Each test case would then define some criteria for what it means to pass the test. As with the Debugger plugin, the Test Runner would be disabled by default - so the 'production' flows aren't modified in anyway. When the Test Runner is enabled, it would then take care of running each test in return and reporting back the results. The main challenge is providing a user experience that makes it easy to create these tests in a way that is consistent with the low-code nature of Node-RED. Whilst this is very much a future roadmap item for Node-RED, it is one I hope we can start moving forward soon. Having a good, repeatable, testing strategy in Node-RED will make it stand-out from many of the other low-code tools and platforms available today. ### Low-Code vs Lines-of-Code The low-code nature of Node-RED means it is easily accessible to a wide range of users. You don't need to be a seasoned software engineer to get started. If you have a task to solve, and understand it well enough to break it into the right set of steps, translating that into a Node-RED flow can be much easier than having to write all of the corresponding code from scratch. Just because you aren't writing code in Node-RED, it doesn't mean you shouldn't be able to benefit from ways of working that are proven to improve the end result - whilst keeping true to the low-code nature of the project. ### FlowFuse Cloud Both `nrlint` and Node-RED Debugger are already pre-installed in all [FlowFuse Cloud](https://app.flowfuse.com){rel=""nofollow""} hosted instances. You can start using them today via the sidebar menu. ![](https://flowfuse.com/blog/2024/02/images/node-red-sidebar.png) # Storing Data: Getting Started with Node-RED It's quite straightforward to pass plenty of useful data with each message (msg) in your flows. Not only can you store information in msg.payload, but you can also place information in any other named object, for instance, msg.store. In this article, we will explore some of the better solutions for storing and retrieving transactional information in your flows. #### Storing data outside of msg.payload In this example, we have data in msg.payload as well as in msg.later. :video{ariaLabel="Storing data outside of msg.payload" autoPlay="true" height="362" loop="true" muted="true" playsInline="true" preload="none" width="1040"} If you want your debug to display the full content of the message, change the output to 'complete message object' as shown above. You can import the flow using this code. ::render-flow ```json [{"id":"aaa1e17e5f158004","type":"inject","z":"67746003c844dbc4","name":"Inject the message","props":[{"p":"payload"},{"p":"later","v":"A string I want to be able to use later in my flow","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello World","payloadType":"str","x":690,"y":140,"wires":[["bce1bf09736125b7"]]},{"id":"bce1bf09736125b7","type":"debug","z":"67746003c844dbc4","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":880,"y":140,"wires":[]}] ``` :: Storing data outside of msg.payload can be very useful when you need access to that data later in your flow. You may notice that many nodes overwrite the content of msg.payload, so putting your data elsewhere is essential otherwise, it will be overwritten and lost. #### Tidying your messages You may also want to remove data you don't need from your messages to optimize the speed of your flows. It's easy enough to remove data you don't need, wherever it sits within your messages using the Change Node. In this example, we are going to delete the content of msg.other while leaving the rest of the message to be passed to the next Node. :video{ariaLabel="Deleting data from msg.other" autoPlay="true" height="344" loop="true" muted="true" playsInline="true" preload="none" width="1136"} The Change Node is configured as follows. ![Change Node configuration](https://flowfuse.com/blog/2024/02/images/delete.png "Change Node configuration") You can import the flow using this code. ::render-flow ```json [{"id":"1ac51e71153f7c1f","type":"inject","z":"67746003c844dbc4","name":"Inject the message","props":[{"p":"payload"},{"p":"other","v":"We don't need this string anymore","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello World","payloadType":"str","x":530,"y":100,"wires":[["5d3a978ad9eab443","cd7609101328caa2"]]},{"id":"5d3a978ad9eab443","type":"debug","z":"67746003c844dbc4","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":700,"y":60,"wires":[]},{"id":"cd7609101328caa2","type":"change","z":"67746003c844dbc4","name":"","rules":[{"t":"delete","p":"other","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":100,"wires":[["be2f7f68dee570be"]]},{"id":"be2f7f68dee570be","type":"debug","z":"67746003c844dbc4","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":900,"y":100,"wires":[]}] ``` :: #### Storing data outside of msg.payload so you can access it later in your flows Storing data outside of msg.payload allows you to access it later in your flows. In this example, we inject geographical coordinates and use an API to get the sunset time for each location. We can then output the result as a sentence. As the HTTP Node, which we are using to interact with the weather API, overwrites msg.payload with the response, we will store the submitted city name and coordinates in msg.store for later use. You can see the flow working below. :video{ariaLabel="Example flow which gets the sunset time for a given location" autoPlay="true" height="448" loop="true" muted="true" playsInline="true" preload="none" width="1142"} You can import the flow using this code. ::render-flow ```json [{"id":"809cc8f4678767b7","type":"inject","z":"67746003c844dbc4","name":"London","props":[{"p":"payload.city","v":"London","vt":"str"},{"p":"payload.lat","v":"51.5072","vt":"str"},{"p":"payload.lng","v":"0.1276","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":80,"wires":[["6b5a6a2ef7a64f1a"]]},{"id":"b86d2d558eebbd7e","type":"inject","z":"67746003c844dbc4","name":"Washington DC","props":[{"p":"payload.city","v":"Washington DC","vt":"str"},{"p":"payload.lat","v":"38.9072","vt":"str"},{"p":"payload.lng","v":"77.0369","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":160,"wires":[["6b5a6a2ef7a64f1a"]]},{"id":"aaecc81a2de233d6","type":"debug","z":"67746003c844dbc4","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":820,"y":160,"wires":[]},{"id":"6b5a6a2ef7a64f1a","type":"change","z":"67746003c844dbc4","name":"","rules":[{"t":"set","p":"store","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":120,"wires":[["273ee743f8d1a5f1","af43cea5866e11f5"]]},{"id":"273ee743f8d1a5f1","type":"template","z":"67746003c844dbc4","name":"create the URL","field":"url","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"https://api.sunrisesunset.io/json?lat={{store.lat}}&lng={{store.lng}}","output":"str","x":500,"y":120,"wires":[["52e8913233f379eb","1d94053c790791ee"]]},{"id":"af43cea5866e11f5","type":"debug","z":"67746003c844dbc4","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":480,"y":160,"wires":[]},{"id":"52e8913233f379eb","type":"http request","z":"67746003c844dbc4","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":670,"y":120,"wires":[["aaecc81a2de233d6","a245b322ade3a7e9"]]},{"id":"1d94053c790791ee","type":"debug","z":"67746003c844dbc4","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":660,"y":80,"wires":[]},{"id":"a245b322ade3a7e9","type":"template","z":"67746003c844dbc4","name":"Create the sentence","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"The sun will set at {{payload.results.sunset}} in {{store.city}}.","output":"str","x":860,"y":120,"wires":[["3e4b2d3644845f91"]]},{"id":"3e4b2d3644845f91","type":"debug","z":"67746003c844dbc4","name":"debug 7","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":120,"wires":[]}] ``` :: Using this technique, we can build the content of message.store (or any other name you'd like to use) and then output the content at the end of a flow, even if you use msg.payload to interact with APIs and other custom nodes. #### Why not store the values in context? Where possible, it's more robust to store all the information related to a particular message within the message rather than saving it to context and retrieving it later. Using context risks a [race condition](https://en.wikipedia.org/wiki/Race_condition#:~\:text=A%20race%20condition%20or%20race,to%20unexpected%20or%20inconsistent%20results){rel=""nofollow""} within your flow that could result in data corruption. In this example, we simulate how a race condition can make context a bad choice for transactional data storage. The flow passes in the name and age of two people then moves the age to context. The flow then adds a random delay for each message so that, in some cases, the messages do not reach the debug in the order they were created. After the delay, the age is pulled back from context and added to each msg.payload. :video{ariaLabel="Example of how a race condition can make context a bad place to cache data" autoPlay="true" height="356" loop="true" muted="true" playsInline="true" preload="none" width="1042"} If there is a race condition in play, we should intermittently see Rob and John's stored ages being assigned to the wrong person. We can see in the image above that Rob is showing the incorrect age. You can import the flow using this code. ::render-flow ```json [{"id":"7ac4e7165d99f041","type":"inject","z":"67746003c844dbc4","name":"Rob","props":[{"p":"payload.name","v":"Rob","vt":"str"},{"p":"payload.age","v":"46","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":840,"wires":[["1e0575bd662f9277"]]},{"id":"e9c0baba4f50b0b2","type":"inject","z":"67746003c844dbc4","name":"John","props":[{"p":"payload.name","v":"John","vt":"str"},{"p":"payload.age","v":"29","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":570,"y":880,"wires":[["1e0575bd662f9277"]]},{"id":"d5f688682206d316","type":"change","z":"67746003c844dbc4","name":"","rules":[{"t":"set","p":"age","pt":"flow","to":"payload.age","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":860,"wires":[["d56225e59bde93cf"]]},{"id":"d56225e59bde93cf","type":"change","z":"67746003c844dbc4","name":"","rules":[{"t":"delete","p":"payload.age","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":860,"wires":[["d5a119b311bf8377"]]},{"id":"e3ec11b4e038e564","type":"debug","z":"67746003c844dbc4","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1080,"y":940,"wires":[]},{"id":"d5a119b311bf8377","type":"delay","z":"67746003c844dbc4","name":"","pauseType":"random","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"0","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":720,"y":940,"wires":[["b329d7f2685d0ed6"]]},{"id":"b329d7f2685d0ed6","type":"change","z":"67746003c844dbc4","name":"","rules":[{"t":"set","p":"payload.age","pt":"msg","to":"age","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":940,"wires":[["e3ec11b4e038e564"]]},{"id":"1e0575bd662f9277","type":"delay","z":"67746003c844dbc4","name":"","pauseType":"random","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"0","randomLast":"1","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":700,"y":860,"wires":[["d5f688682206d316"]]}] ``` :: #### Conclusion Storing transactional data in msg rather than context in Node-RED offers advantages in modularity, scalability, simplicity, and also helps prevent race conditions. By using msg, data transfer between nodes becomes more seamless, allowing for independent node reuse across flows without additional configuration. This approach ensures scalability by avoiding the imposition of large datasets on the global context. Moreover, storing data outside of msg.payload within the msg object enhances flexibility. It separates metadata and other relevant information from the main payload, promoting a cleaner and more organized structure. This practice not only aligns with Node-RED's visual programming paradigm but also improves code readability. Steering clear of context for transactional data, while also organizing it within the msg object, provides a comprehensive and reliable solution within the Node-RED framework. Thanks to [SunriseSunset](https://sunrisesunset.io/){rel=""nofollow""} for creating the useful API we've used in this article. # Citizen Development: Unleashing Domain Experts Citizen development has taken the spotlight recently, but the concept itself isn't entirely new. Remember the era of "hackers"? They weren't malicious actors, but individuals who tinkered with technology, pushing boundaries and creating solutions. Today, these same problem-solving personas have evolved into "[citizen developers](https://www.gartner.com/en/information-technology/glossary/citizen-developer){rel=""nofollow""}" – business users empowered to build applications without relying solely on professional coders. ## Why Now? While the drive for user-friendliness has always been present in programming languages, several factors have converged to push citizen development to the forefront: - Democratization of Technology: Cloud computing and advancements in low-code/no-code platforms have lowered the barrier to entry, making development tools accessible to individuals with minimal technical expertise. - Business Agility: The need for rapid innovation necessitates solutions that bypass lengthy IT backlogs. Citizen developers can bridge the gap, creating applications and internal tools quickly and efficiently. - Domain Expertise: Business users possess deep insights into specific processes and challenges. By giving them the tools to build solutions, organizations can tap into a new knowledge source by putting domain experts into the driver's seat. ### Beyond Spreadsheets: Platforms of Empowerment Reports and Dashboards are often created by a BI teams that have to take information from specialists to create custom visualizations that may or may not come out to be exactly what they asked for. Queue old faiful, spreadsheets, where a domain expert can create their own reports that suit their needs. Spreadsheets are powerful for analysis, but they often lack the real-time secure connectivity needed for enterprise applications. Transform the knowledge created in a spreadsheet into a personalized real-time visualization. To do that, you must first select a citizen development platform and those platforms should offer: - Visual Interfaces: Drag-and-drop functionality and pre-built components eliminate the need for complex coding, allowing users to focus on logic and functionality. - Integration Capabilities: Seamless connection with existing data sources and systems ensures that citizen-built applications integrate seamlessly into the overall workflow. Plus, the ability to create custom integrations. - Governance and Security: IT governance establishes guardrails while empowering users, ensuring data security and application stability through features like Role Based Access Control and SSO integration. - Collaboration Tools: Built-in collaboration features enable teams to share ideas, iterate on solutions, and ensure knowledge transfer. - Reseliency: Backup management. ### Citizen Developers: Not Rube Goldberg Machines, But Value Creators The key to successful citizen development lies in empowerment, not abdication. By providing the right tools, training, and governance, organizations can avoid creating complex, fragile solutions. Instead, citizen developers become powerful problem-solvers, building value-driven applications that address specific needs and contribute to overall business goals. ## Lean In Remember, citizen development isn't about replacing professional developers. It's about creating a collaborative environment where everyone can contribute their unique skills and perspectives. By removing the coding barrier, we unleash a wider pool of innovators, accelerating progress and driving organizational success. # Getting Started with Node-RED Dashboard 2.0 With our latest release of Node-RED Dashboard 2.0, we've made some big improvements to the onboarding experience. We're seeing over 2,000 people download Dashboard 2.0 per week, and are seeing a great buzz in the community of brand new Node-RED users, experienced Node-RED users that haven't explored a UI solution previously and existing users migrating from Dashboard 1.0. So, with that in mind, we wanted to offer a new "Getting Started" guide that will help you get up and running with building custom user interfaces and data visualizations in Node-RED. ## How to Install Node-RED Dashboard 2.0 ### Step 1: "Manage Palette" ![Screenshot to show where to find the "Manage Palette" option in Node-RED](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-manage-palette.png){dataZoomable=""} Screenshot to show where to find the "Manage Palette" option in Node-RED 1. Click the Node-RED Settings (top-right) 2. Click "Manage Palette" ### Step 2: Search & "Install" ![Screenshot to show where to find the "Install" tab, and how to find @flowfuse/node-red-dashboard](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-search-install.png){dataZoomable=""} Screenshot to show where to find the "Install" tab, and how to find @flowfuse/node-red-dashboard 1. Switch to the "Install" tab 2. Search for *"@flowfuse/node-red-dashboard"* 3. Click "Install" ## Adding your first widgets :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} With the nodes installed, getting started is as easy as choosing a node from the Palette (the left-hand side list of nodes) in Node-RED, and dropping it onto your canvas. :video{ariaLabel="Screen recording to show how easy it is to deploy your first Dashboard 2.0 application." autoPlay="true" height="460" loop="true" muted="true" playsInline="true" preload="none" width="800"} Screen recording to show how easy it is to deploy your first Dashboard 2.0 application. In this case, we drop in a `ui-button`, click "Deploy" and then can see the button running live in our user interface. Notice too that Dashboard will automatically setup some underlying configurations for you (visible in the right-side menu): - `ui-base`: Each instance of Node-RED that uses Dashboard 2.0 must have a single `ui-base` element (we're hoping to add support for multiple in the future). This element contains all of the global settings for your Dashboard instance. - `ui-page`: A single Dashboard (`ui-base`) can consist of multiple pages, and can be navigated to using the left-side sidebar. Each page is then responsible for displaying a collection of `ui-group` elements. - `ui-group`: Each group contains a collection of widgets, and can be used to organize your Dashboard into logical sections. - `ui-theme`: Each `ui-page` can be assigned a given theme. Your "Themes" provide control over the aesthetic of your Dashboard like color, padding and margins. ## Configuring your layout Dashboard 2.0 adds a dedicated sidebar to Node-RED to provide a centralized view of your pages, groups and widgets. From here you can add new pages and groups, modify existing settings, and re-order content to your liking. ![Screenshot showing the Dashboard 2.0 sidebar in the Node-RED Editor.](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-sidebar.png){dataZoomable=""} Screenshot showing the Dashboard 2.0 sidebar in the Node-RED Editor. When defining your layout options, we break the choice into two sections: - **Page Layout:** Controls how the `ui-groups`'s are presented on a given page in your application. - **Navigation Sidebar:** Defines the left-side navigation style, defined at the `ui-base` level. ![Example of a "Grid" page layout, with "Collapsing" sidebar navigation.](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-layout.png){dataZoomable=""} Example of the "Grid" page layout, with "Collapsing" sidebar navigation. ### Page Layout Currently, we have three different options for page layout: - **Grid:** ([docs](https://dashboard.flowfuse.com/layouts/types/grid.html){rel=""nofollow""}) This is the default layout for a page, and uses a 12-column grid system to layout your `ui-groups`. Widths of groups and widgets define the number of columns they will render in. So, a "width" of 6" would render to 50% of the screen. Grid layouts are entirely responsive, and will adjust to the size of the screen. - **Fixed:** ([docs](https://dashboard.flowfuse.com/layouts/types/fixed.html){rel=""nofollow""}) Each component will render at a *fixed* width, no matter what the screen size is. The "width" property is converted a fixed pixel value (multiples of 48px by default). - **Notebook:** ([docs](https://dashboard.flowfuse.com/layouts/types/notebook.html){rel=""nofollow""}) This layout will stretch to 100% width, up to a maximum width of 1024px, and will centrally align. It's particularly useful for storytelling (e.g. articles/blogs) or analysis type user interfaces (e.g. Jupyter Notebooks), where you want the user to digest content in a particular order through scrolling. ### Navigation Sidebar Dashboard 2.0 offers various options on the appearance of the navigation sidebar: - **Collapsing:** When the sidebar is opened the page content will adjust with the width of the sidebar. - **Fixed:** The full sidebar will always be visible, and the page content will adjust to the width of the sidebar. - **Collapse to Icons:** When minimized, users can still navigate between pages by clicking on the icons representing each page in the sidebar. - **Appear over Content:** When the sidebar is opened, the page is given an overlay, and the sidebar sits on top. - **Always Hide:** The sidebar will never show, and navigation between pages can instead be driven by [`ui-control`](https://dashboard.flowfuse.com/nodes/widgets/ui-control.html){rel=""nofollow""}. ### Define Your Layout In our example, we're going to switch to a "Notebook" layout, with a "Collapse to Icons" sidebar: ![Example of the "Notebook" layout and "Collapse to icons" sidebar](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-example.png){dataZoomable=""} Example of the "Notebook" layout, with "Collapse to Icons" sidebar navigation. ## Adding More Widgets Now, we're going to build a quick example to demonstrate how we can wire nodes together, and visualize the output from a `ui-slider` onto a `ui-chart`. ### Adding a Group In the Node-RED Editor's Dashboard 2.0 sidebar, we're going to then do the following things: 1. Edit "My Group" and rename it to "Controls" 2. Create a new "Group" in your existing page called "Data Visualization" You'll now see the two groups listed under "Page 1". "Controls" with a single `ui-button` and "Data Visualization" with no widgets. ![Screenshot of the modified and newly added groups](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-new-group.png){dataZoomable=""} Screenshot of the modified and newly added groups ### Connecting New Nodes Then, we're going to add two new widgets: - UI Chart - UI Slider Which we can do by dropping them from the left-side Palette and onto our canvas. We'll need to double-click each new node and confirm which "Group" we want to add this node to. In this case, we'll add the `ui-slider` to the "Controls" group, and the `ui-chart` to the "Data Visualization" group. We're also going to connect the output from both the `ui-slider` and `ui-button` to the input of the `ui-chart`: ![Screenshot of the Node-RED Editor, showing the ui-slider and ui-button connected to our ui-chart](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-flow.png){dataZoomable=""} Screenshot of the Node-RED Editor, showing the ui-slider and ui-button connected to our ui-chart Now, when we view our Dashboard, we can see the `ui-slider` output is def straight into our `ui-chart`: ![Screenshot of the Dashboard with all three widgets rendered](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-final.png){dataZoomable=""} Screenshot of the Dashboard with all three widgets rendered The final step we're going to make is to modify our `ui-button`. We're going to rename it to "Clear", and configure it's "Payload" option to send a JSON payload of `[]`, which, when sent to the `ui-chart` will clear the chart of all data. ![The ui-button configuration after setting it's payload and label](https://flowfuse.com/blog/2024/03/images/dashboard-getting-started-btn-config.png){dataZoomable=""} The ui-button configuration after setting it's payload and label With all of this together, we have the following functional Dashboard: :video{ariaLabel="Short animation showing the final functional dashboard" autoPlay="true" height="450" loop="true" muted="true" playsInline="true" preload="none" width="800"} Short animation showing the final functional dashboard. ## Next Steps Whilst this is just a simple introduction of Node-RED Dashboard 2.0, we do have many other articles and documentation that can help you get started with more advanced features. - [FlowFuse Dashboard Articles](https://flowfuse.com/blog/dashboard/) - Collection of examples and guides written by FlowFuse. - [Node-RED Dashboard 2.0 Documentation](https://dashboard.flowfuse.com){rel=""nofollow""} - Detailed information for each of the nodes available in Dashboard 2.0, as well as useful guides on building custom nodes and widgets of your own. - [Node-RED Forums - Dashboard 2.0](https://discourse.nodered.org/tag/dashboard-2){rel=""nofollow""} - The Node-RED forums is a great place to ask questions, share your projects and get help from the community. - [Beginner Guide to a Professional Node-RED](https://flowfuse.com/ebooks/beginner-guide-to-a-professional-nodered/) - A free guide to an enterprise-ready Node-RED. Learn all about Node-RED history, securing your flows and dashboard data visualization. - [FlowFuse - Book a Demo](https://flowfuse.com/contact-us) - FlowFuse provides a complete platform to scale your production Node-RED applications, increase developer velocity, and enhance security in order to accelerate innovation. ## Follow our Progress New features and improvements are coming to Node-RED Dashboard 2.0 every week, if you're interested in what we have lined up, or want to contribute yourself, then you can track the work we have lined up on our GitHub Projects: - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} - [Dashboard 1.0 Feature Parity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/5){rel=""nofollow""} If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. # FlowFuse and Gallarus Announce Strategic Partnership to Accelerate Industry 4.0 Adoption FlowFuse, a leading provider of the low-code end-to-end development platform for industrial applications, and Gallarus Industry Solutions Limited, the leading industry 4.0 integrator in Europe dedicated to digital transformation through the deployment of the Unified Namespace (UNS) digital architecture, today announced an exciting strategic partnership. This collaboration aims to empower businesses with advanced solutions for Industry 4.0 transformation and optimized operational efficiency. Powered by Node-RED, FlowFuse enables teams of all sizes to harness low-code capabilities to build robust applications with unparalleled scalability, and security. FlowFuse’s collaborative environment and scalable architecture ensure that teams can build securely and together for continuous development and operational success. Gallarus, with its extensive experience in digital transformation projects and industrial technology integration, will leverage its expertise in high-quality project implementation, support, and maintenance. Together, they offer a comprehensive solution for businesses seeking to digitally transform their operations. “We are pleased to announce this strategic alliance with Gallarus,” said Zeger-Jan van de Weg, CEO at FlowFuse. “This partnership signifies a significant step forward in providing businesses with the tools they need to excel in the current digital landscape. By combining our expertise, we are confident in delivering exceptional value to our customers.” “This partnership with FlowFuse perfectly aligns with our mission to empower businesses with transformative industry 4.0 solutions,” said Patrick Mc Carthy, COO at Gallarus Industry Solutions Limited. “FlowFuse's low-code development platform, combined with our expertise in UNS architecture and integration, will provide a powerful solution for organizations of all sizes to move away from an Industry 3.0 mentality and embrace Industry 4.0, streamlining operations to unlock new levels of efficiency not seen before.” This combined expertise will address the complexities of digital transformation for businesses by: - Facilitating data access, transformation, and visualization across any protocol. - Breakdown data silos enabling organization-wide data availability via the UNS. - Enable citizen developers to build extremely useful industrial applications. For more information about FlowFuse and Gallarus Industry Solutions Limited, please visit their respective websites at [flowfuse.com](https://flowfuse.com) and [gis.ie.](http://gis.ie){rel=""nofollow""} # FlowFuse Open Source Tier Resource Limits Today, with the latest FlowFuse release, an important change was made that updates the resource limits for our open-source, self-managed FlowFuse server tier. This change does not affect FlowFuse Cloud users in any way. ## New Open Source Tier Resource Limits The new limit for the number of Node-RED runtimes on the Open Source tier is 5. These 5 instances can be distributed across a maximum of 5 teams. This revised structure allows for the development of your initial FlowFuse and Node-RED solutions, providing a clear understanding of how FlowFuse can enhance your organization's workflows. Upgrading to a Team or Enterprise tier license unlocks the full potential of FlowFuse and grants access to our dedicated support team. ## Why the Change? Here at FlowFuse, we're committed to providing a great experience for all our users, including those utilizing our free, open-source tier. We initially designed this tier to offer a platform for small organizations and larger teams to explore FlowFuse's capabilities without needing our direct involvement. However, we've observed instances where the Open Source tier's free limits were being used to run very large deployments and even entire sites. While support is a key benefit we offer to paying customers, we believe everyone deserves a positive FlowFuse experience. As such, we're adjusting the resource limits to better align with the Open Source tier's intended purpose and allow the company to continue to invest in the Open Source tier. ## Transitioning Users If you're currently impacted by these resource limit changes, we want to ensure a smooth transition. We're offering a complimentary 1-year Enterprise license to affected users. To claim your license, simply email with the IP address your server has used to send telemetry data (if available) and a screenshot of your admin panel so we can match the currently usage with the new license. We appreciate your understanding and continued support. If you have any questions regarding this update, please don't hesitate to reach out to our team at . # Securing HTTP Traffic for Node-RED with FlowFuse Citizen development empowers employees to create digital solutions. However, it requires guardrails to ensure data security, operational stability, and compliance. These guardrails are what FlowFuse provides to the Node-RED community to level up their deployments. FlowFuse offers many different security measures for authentication and authorization, which all apply to different scenarios. In this post we’ll take a look at most of them, specifically for HTTP traffic. We’ll discuss the trade-offs for auditabliltiy, convenience to use as either machine or human, among other factors. ## HTTP Basic Authentication: A Simple Approach HTTP Basic Authentication is widely supported and straightforward to implement, making it a popular choice for securing APIs. It requires users to provide a username and password before accessing the Node-RED instance. While this method is easy to use, it's important to note that the username and password are shared and transmitted in plain text, making it vulnerable to interception if the connection doesn't leverage SSL/TLS. FlowFuse by default ensures SSL/TLS is deployed. ## Personal access tokens: Knowing who accessed the Node-RED Personal access tokens (PATs) are an essential component of FlowFuse, allowing users to securely access their accounts without sharing their passwords. These tokens are generated by the user and can be used to authenticate to the SaaS product's API or other services. PATs provide a more secure alternative to traditional username/password authentication, as they can be revoked or regenerated at any time, limiting the potential impact of a compromised token. ## FlowFuse Authentication: Seamless Integration FlowFuse authentication offers a seamless and secure way for users to access dashboards and other resources that are typically accessed through a browser. It leverages single sign-on (SSO) and SAML 2.0, reducing the management burden for organizations. For users this is convenient as they can access multiple applications and resources using a single set of credentials, eliminating the need to remember and manage multiple passwords. For organizations, SSO enhances security by centralizing authentication and authorization, reducing the risk of unauthorized access. By leveraging SSO and SAML 2.0, FlowFuse takes care of user management, freeing up customers from the administrative burden of managing user accounts and passwords. FlowFuse authentication adheres to industry-standard security protocols, ensuring compliance with regulatory requirements. This method of authentication is however impractical for API access by other services, to programmatically transfer data between them. ## Bearer Authentication: A Token-Based Approach Bearer Authentication offers a more secure and flexible alternative to traditional username/password authentication. Users will generate the token through the FlowFuse platform with each instance generating its own token. These tokens can be designed to have limited lifespans, reducing the risk if compromised. In the case the token then becomes compromised only the instance in which the token is generated can become subject to malicious behaviors. In the case that this does occur, simply deleting the token will elevate any unwanted access. Compared to FlowFuse Authentication, this method is very well suited for API access and programmatic access to FlowFuse. ## Choosing the Right Authentication Strategy FlowFuse provides multiple authentication mechanisms to cater to various aspects of security and user experience. When designing your Node-RED applications, consider the specific requirements of your project and the patterns of user interaction to select the most appropriate authentication strategy. By doing so, you can ensure both a high level of security and an optimal user experience for your web development projects with API calls through Node-RED. In conclusion, understanding and utilizing these different types of authentication in FlowFuse empowers citizen developers like you to create more secure and efficient applications for diverse use cases. # Installing and operating Node-RED behind a firewall Practitioners using Node-RED often find themselves in a situation where a firewall is deployed in their organization. This network configuration is a fact of life and is generally not controlled by the same people using Node-RED. Given security reigns supreme in Industrial IoT (IIoT), and a firewall offers a lot of benefits, we anticipate it will be deployed more often in the future, and as such it’s good to understand how you can get the most out of Node-RED when deployed behind a firewall. ## Node-RED installation with a firewall Generally, the standard install procedure for Node-RED requires a connection to the NPM servers that host the package. Due to NPM’s unaudited nature, IT is unlikely to agree to a permanent exception to the firewall to allow access to it. However, there are a couple of actions one can take to install Node-RED anyway. First, ask for a temporary exception. Node-RED is installed in a few minutes, so if there’s a set time schedule an exception can be made there’s a regular method available again through collaboration with the network administrator. The second option is leveraging vendor specific package managers. As these are vetted repositories, it’s not uncommon that these gates in the firewall have been created and opened to you. Some vendors supply repositories for major package managers like `apt-get` on Debian/Ubuntu-based systems, or there’s a marketplace approach to install Node-RED like for example the [Rexroth CtrlX with Node-RED in it](https://developer.community.boschrexroth.com/t5/Store-and-How-to/FlowFuse-Node-RED/ba-p/82135){rel=""nofollow""}. Lastly, you could consider downloading the NPM package beforehand and transferring it to your machine within the network. NPM allows the installation of local packages, which in turn allows you to create applications with Node-RED. This is generally a shadow IT action, and not recommended unless it’s approved. ## FlowFuse and your firewall As FlowFuse can be installed in a VPN behind a firewall, there’s no requirement to open up a ‘gate’ in your firewall to FlowFuse servers. The safe perimeter provided remains to the outside world. The security aspect remains, though as a Node-RED developer, there are still everyday tasks you’ll need access to the outside world. Consider installing third party nodes to connect your Node-RED instance to virtually any protocol or digital service. There are over 5000 of these nodes available, and as an organization it’s challenging to keep on top of. Your firewall provides one layer of security so that the data you’re accessing remains safe. FlowFuse provides a second layer of protection; we’ve introduced a [“Certified Nodes” catalog](https://flowfuse.com/integrations/?certified=1). These nodes have gone through automated and manual inspection to prevent malicious code from making it onto your production systems. Installing these packages would typically still require you to obtain files from NPM. With FlowFuse however, a cache can be built with only vetted nodes – All other nodes remain unavailable. Once Node-RED is installed and the initial development has been completed, FlowFuse aims to reduce the maintenance burden on both IT and OT teams too. In the same package cache aforementioned, Node-RED versions can be added. Updating Node-RED to the latest version becomes a job of just a few clicks. Updating your Node-RED instances, even behind a firewall, is imperative, as virtually all breaches are the result of daisy-chaining multiple security vulnerabilities into a high to critical event. ## Wrap up FlowFuse founding engineers have decades of experience running Node-RED wherever it is valuable. Our product is the culmination of that, and we’re excited to help you become successful in your digitalization efforts – even when a firewall is in play. # Looking towards Node-RED 4.0 and beyond With Node-RED 4.0 coming soon, I wanted to take a look at what users can expect to see with the new release, as well as some of the new features we're working on. The Node-RED project [schedules its releases](https://nodered.org/about/releases/){rel=""nofollow""} around a yearly major release that coincides with when a Node.js version reaches its end-of-support. This lets us drop support for that node.js version and update the default version of node used in the docker containers we publish. We treat this as a major change because it might require actions on the end-users part to update any additional modules they have installed. With Node-RED 4.0, we will be dropping support for anything earlier than Node 18 - with Node 20 the currently recommended version to use. That will give users almost 2 full years before needing to consider another Node.js upgrade. Now, talking about Node.js versions is not that exciting. What is more exciting is to look at what new features are coming to Node-RED. There are a few things already merged and ready to be released in the first beta release, and more landing each week. ### More auto-complete Node-RED already has simple auto-complete on `msg` fields in the editor. We've now extended that to also work with `flow`/`global` context inputs as well as the `env` type for accessing environment variables. ![Node-RED editor autocompleting properties](https://flowfuse.com/blog/2024/03/images/nr4-auto-complete.png "Node-RED editor autocompleting properties") This makes it so much easier to work with these types of properties - being sure you're using something that exists rather than having to switch between different views in the editor to get the names right. In the case of env vars, it also shows you where the value was set - useful when you have nested groups and subflows which might be overriding a particular value. The `msg` auto-complete is still based on a built-in list of common message properties used by the core nodes. There is interest in enabling this to pull completions from 'live' messages seen by the node in question - but that's not currently in the plan for 4.0. ### Timestamp formatting The Inject node has provided the ability to inject a timestamp since the very early days of Node-RED. The value it actually sets is the number of milliseconds since epoch (aka January 1st, 1970). If you're used to working with JavaScript, then this is a perfectly normal way to pass times around. However, it isn't always what is needed and flows end up using a Function node to reformat it in some way. With 4.0 we've added options to pick what format the timestamp is generated in at the start. Now, formatting times and dates can be a big can of worms of options. So, for this initial release, we've kept it simple by offering three options: !["Format options for Node-RED timestamp"](https://flowfuse.com/blog/2024/03/images/nr4-timestamp-formatting.png "Format options for Node-RED timestamp") - *milliseconds since epoch* - the existing option, just more explicitly labelled for what it is - *YYYY-MM-DDTHH\:mm\:ss.sssZ* - also known as ISO 8601 - *JavaScript Date Object* - the standard Date object There is scope to allow custom format strings to be set in the node - but we'll see what the feedback is on these new options first. ### A better CSV node The CSV node has had a big overhaul to make it more standards compliant. It turns out CSV has a whole bunch of tricky edge cases that most users don't hit - but if you did hit them you would be stuck. The new node follows the [RFC4180](https://www.ietf.org/rfc/rfc4180.txt){rel=""nofollow""} standard and is also faster - wins all around. For those flows that rely on some of the non-standard edge case behaviour of the existing node, we've kept a legacy mode in place to keep those flows working. ### Customising config nodes in Subflows This one needs a bit of explaining. Subflows are a way Node-RED lets you create a flow and add multiple reusable instances of it within your flows. For example, a subflow may connect to an MQTT broker and do some standard processing on the messages it received before sending them on. The Subflow can then expose a set of properties that can be customised for each instance. In our example, that could be the topic the MQTT node subscribes to. However, in that example, the MQTT node's broker configuration would be locked to the same broker config node in every instance - and that's something we're solving in Node-RED 4.0. We're making it possible to expose the choice of a configuration node in the Subflow properties - so each instance can be customised even further. Another common use for this will be with [Node-RED Dashboard](https://flowfuse.com/platform/dashboard/) - which uses config nodes to set the location of a widget. With Node-RED today, you cannot really use dashboard nodes inside subflows as you end up with multiple copies of the widgets all packed into the same group. With this update, you'll be able to configure the subflow instance with exactly what dashboard group to place its contents into. ### Updated JSONata The JSONata library is used to provide the `expression` types in Node-RED - a really powerful way of working with JSON objects. With this release we've updated to the new major release of JSONata that comes with a bunch of performance improvements. ### And many more minor changes I'll hold off listing them all out here, but there are plenty of other smaller changes scattered through the editor and the core nodes. Be sure to check the beta release notes when it arrives to see what else has been done. ## Looking further ahead Whilst all of these are great incremental improvements to Node-RED, there are some bigger items we're looking at that will really improve the overall Node-RED experience. I wrote recently about improving how users can [test their flows](https://flowfuse.com/blog/2024/02/software-development-in-node-red/#testing). This remains something I think we really help make Node-RED stand apart from other low-code solutions. It won't be in the imminent 4.0 release, but it is definitely still on the roadmap for a future release. ### Concurrent editing Another area we want to improve is the collaboration experience within Node-RED. Working on flows as a team is a key feature of FlowFuse, and we want to make it even easier to do. One of the common complaints is how Node-RED currently handles multiple users editing flows at the same time. Whilst it's better than it used to be, it still makes for a very jarring experience when you have to keep merging other users' changes into your own. Our goal is to make collaboration as simple and natural as possible. There are a wide range of approaches we could take here. For example, a small improvement would be to merge other users' changes in the background without interrupting what you're doing. But I think we do better than that. What if the editing experience was more like Google Docs - knowing that other users have the editor open, and being able to see their changes in real time. This would make for a truely collaborative editing experience. There are some difficult problems to solve before we can get there, but I think this will be one of the more transformational changes to Node-RED we've had for some time. ## Beta releases The [release plan](https://nodered.org/about/releases/){rel=""nofollow""} has Node-RED 4.0 coming around the end of April. As mentioned, we'll be doing a series of beta releases between now and then to start getting early feedback from the community. Keep an eye on the [community forum](https://discourse.nodered.org/c/news/9){rel=""nofollow""} for release announcements as they come. # Why Low-Code is Better There are two common reasons why new languages come about. They provide a feature missing in the existing programming languages, or it is a tool that is easier to learn and use. The latter often functions like a swiss army knife with each iteration including more and more tools. The journey of low-code is like a swiss army knife, the perfect tool for the Citizen Developer. ## A Typical Coding Evolution Every decade seems to introduce something that simplifies coding. For me, in college, it was Python. A classmate was excited about this new programming language that was all about simplicity and readability, especially with its indentation-based syntax. It seemed too simple at first. Yet, over time, Python became a staple in my programming toolbox. During this time in our education, our coursework was filled with languages like C++, which felt distant from the future of programming we imagined. We joked about "outdated" languages, not yet realizing the breadth of what programming could encompass. After finishing college in 2010, I started working during the tail end of the housing crisis in the U.S. My first role was as a Controls System Integrator at Logical System Inc., where I was introduced to programming PLCs with Ladder Logic. Despite my initial reservations, viewing it as barely a programming language, this experience was my first step towards appreciating the diversity and utility of programming languages beyond the conventional. Later on in my career, I worked as a Corporate Automation and Controls Engineer where I worked on and enforced standards for partner System Integrators and OEMs. One of these important standards was making sure applications written in the control process were written in Ladder Logic within the PLCs. There were exceptions, of course, but the rule was to apply a visually appealing coding language, Ladder Logic, over a text-like language often called Structured Text. ## Programming Languages as Specialized Tools Programming languages are typically optimized for certain tasks. For instance, Matlab and R excel in complex numerical computations thanks to their extensive libraries designed specifically for mathematical operations. While Python might not replace Matlab or R for their core functionalities, it can broaden the applicability of numerical analyses into various other contexts. In another example, VB.net is the go-to for standalone applications in Windows environments. However, for applications that need to run across different operating systems (excluding web applications), Java might be a better choice due to its platform independence. Each programming language has its niche, along with inherent complexities and constraints. There are situations, however, where the specific strengths of a programming language become less critical. In cases where the application is straightforward, the choice of language might simply come down to personal preference or familiarity. But an important consideration arises when thinking about the future of the project: "Will someone else need to edit or view this code later?" If the answer is yes, and especially if the project aims to involve citizen developers, opting for a low-code solution becomes highly advantageous. Low-code platforms are designed with accessibility in mind, making them ideal for projects that benefit from collaboration and ease of maintenance. ## The Purpose of Citizen Development The idea behind Citizen Development is simple, make programming accessible to more people. This is what low-code platforms aim to do. They lower the entry barrier, making programming more inclusive. Insisting on complex, traditional programming languages when there are simpler, equally powerful alternatives seems counterproductive. We should be looking towards making programming more accessible to everyone. If we want to drive meaningful change within our organizations, embracing tools that broaden participation is key. The aim of adopting new standards and tools is to simplify, not complicate. It's about finding better, more accessible ways to work that can accommodate a wider range of skill sets. ## Simplification This discussion isn't about the mechanics of coding, it's about opening up the field to more diverse contributions. Low-code platforms represent a step towards a more inclusive, collaborative future in technology. By lowering barriers to entry, we're not just simplifying coding, we're inviting a broader community to engage, innovate, and drive progress. I look forward to seeing how we can all contribute to this evolving landscape. ## How FlowFuse Helps Our goal here at FlowFuse is to keep expanding on the Swiss army knife, Node-RED. We strive to elevate Node-RED for professionals by providing the tools needed to deploy Node-RED in a safe and secure way. For example by default, the editor for Node-RED is protected using your FlowFuse user credentials. You can also use SSO to further protect your user accounts and give access to Node-RED to your team members. All traffic to FlowFuse and your Node-RED instances is protected by HTTPS. FlowFuse has set up the domain name and manages the certificates so you can spend time on your flows rather than configuring security. We believe that Low-Code is the future and strive to make Citizen Development a reality. To learn more [schedule a call with one of our experts.](https://flowfuse.com/contact-us/) # Scaling Node-RED with FlowFuse: Differences between a FlowFuse Instance and a Device Instance FlowFuse is a Software as a Service (SaaS) platform designed to enhance the experience and capabilities of Node-RED for its users. By focusing on scalability, security, and Dev Ops, FlowFuse aims to remove some of the technical barriers associated with using Node-RED, making it easier for citizen developers to automate tasks, process data, and create applications. In this blog post, we will discuss the differences between a FlowFuse instance and a FlowFuse device instance while highlighting how FlowFuse addresses scalability challenges in Node-RED deployments. ## Scalability Challenges with Traditional Node-RED Deployments While deploying Node-RED is quite simple, managing multiple instances across different environments can become complex and time-consuming. As the number of devices and use cases grow, users face difficulties in scaling their Node-RED applications efficiently to handle increased load without compromising performance or security. This is where FlowFuse comes into play. ## The Role of FlowFuse as an Orchestration Tool FlowFuse functions as an orchestration tool that allows the deployment and management of all your Node-RED instances at scale, addressing scalability challenges head-on. By leveraging its platform, users can quickly deploy and manage multiple Node-RED instances while ensuring optimal performance and security. This enables them to connect with a wide range of devices, from PLCs and sensors to legacy software, without worrying about the complexities of managing their Node-RED deployment. ## Deploying Node-RED Next to Devices One common issue in IoT deployments is that device instances of Node-RED often communicate with unsecure devices or networks. To mitigate security risks and ensure data protection, it's common to deploy Node-RED in close proximity to these devices. The FlowFuse platform uses [device agents](https://flowfuse.com/platform/device-agent/) that communicate back to the platform via a reverse tunnel over port 443. This setup requires only one firewall rule: allowing outbound connections from the [device agent](https://flowfuse.com/platform/device-agent/) running Node-RED to the FlowFuse platform, significantly minimizing security risks while enabling remote monitoring, flow editing, and configuration deployment at scale. ## Deploying Node-RED Instances Within the FlowFuse Platform Not all instances of Node-RED need to be deployed at the edge and can be deployed anywhere. FlowFuse offers this flexibility in cases where users prefer or require deploying their Node-RED instances within the platform itself. This capability allows users to focus on developing and managing their applications without worrying about the underlying infrastructure. ## Conclusion FlowFuse addresses scalability challenges in Node-RED deployments by providing an easy-to-use platform that enables users to manage multiple instances at scale while maintaining security and performance. By understanding the differences between a FlowFuse instance and a device instance, you can make informed decisions about your deployment strategy and leverage the full potential of Node-RED for your applications. Stay tuned for our upcoming blogs where we will dive deeper into the areas of security, dev ops, and backup solutions provided by FlowFuse. [Walter took this exact path, growing from a single Node-RED instance to more than 130 across its global production network without adding headcount to its IT team](https://flowfuse.com/customer-stories/scaling-industrial-iot-operations-while-maintaining-competitive-edge/), using FlowFuse device agents to manage that scale. # How Kafka is applied in manufacturing Have you ever wondered how manufacturing and automotive industries can effectively manage the vast amount of real-time data generated by sensors and systems throughout the production process? A few years back, these industries faced major obstacles in handling the large volume of real-time data produced by sensors placed across the production line. Even today many industries continue to grapple with similar challenges. Traditional data management systems struggle to process and analyze this data in real-time, leading to inefficiencies in operational activities and decision-making. To address these challenges, various manufacturing and automobile plants have embraced technologies like Apache Kafka. Kafka provides a distributed streaming platform that enables the efficient handling of real-time data streams. By leveraging Kafka, we can aggregate, process, and analyze data in real-time seamlessly. This guide provides a high-level overview of Kafka, covering its definition, components, functionality, applications, and limitations. ## What is Kafka? Apache Kafka is a platform for distributed data streaming that allows for the publishing, subscribing, storing, and processing of streams of records in real-time. It is intended to handle data streams from multiple sources and to deliver them to multiple consumers. In essence, it can move large quantities of data in real-time from any source to any destination, simultaneously. Kafka is also a very good [broker for UNS architecture](https://flowfuse.com/blog/2024/01/unified-namespace-what-broker/). ## Understanding Kafka's Architecture Kafka architecture is designed to provide a scalable and fault-tolerant platform for handling real-time data streams. The architecture consists of several key components, each component serves a specific purpose in the data processing pipeline. In this section, we will take an overview of Kafka's architecture and its key components. !["Architecture of Kafka"](https://flowfuse.com/blog/2024/03/images/using-kafka-in-manufacturing-kafka-architecture.png "Architecture of Kafka"){dataZoomable=""} **1. Topics and Partitions** - Topics: Imagine topics as folders for organizing data – they act as distinct categories. Kafka arranges information into these topics for systematic storage. - Partitions: Think of partitions as subdivisions within topics. They enable parallel processing across multiple servers, enhancing fault tolerance and throughput. **2. Producers:** - Producers: Producers are like architects of data flow. They decide where to send records within a topic. This decision can be balanced using a round-robin or directed by a record key for specific purposes, such as maintaining order. **3. Brokers:** - Definition: Brokers are the backbone servers in a Kafka cluster. - Tasks: Brokers store data, handle requests from both producers and consumers, and maintain the integrity and persistence of data. They also manage the critical task of tracking offsets, which determine the position of consumers within partitions. **4. Consumers and Consumer Groups:** - Consumers: These entities read data from brokers. They subscribe to one or more topics and pull data from the specific partitions they are interested in. - Consumer Groups: Consumers collaborate in groups to scale data processing. Kafka dynamically assigns each consumer in a group a set of partitions from the subscribed topics, ensuring that each partition is processed by only one consumer within the group. **5. Offsets** - Definition: Offsets act as unique identifiers for records within a partition. They denote the position of a consumer in the partition. - Function: As consumers read records, they increment their offset. This allows them to resume processing from where they left off, which is crucial for handling failures or restarts. Kafka stores offset information in a specialized topic for easy recovery. **6. Replication** - Mechanism: Kafka ensures data durability by replicating partitions across multiple brokers. - Replication Factor: This configurable setting determines the number of copies of a partition in the cluster. If one broker fails, another can seamlessly take over, guaranteeing high availability. ## Features of Kafka Now that we've gained a foundational understanding of Kafka, let's explore the key features that make it a preferred choice for many organizations. These features highlight why Kafka transcends being just another data processing tool and why it merits consideration for various use cases. - **High Throughput and Scalability:** Kafka can handle thousands of messages per second and can scale horizontally and vertically to meet growing data demands without compromising performance. - **Fault Tolerance and Reliability:** Built to ensure reliability, Kafka guarantees fault tolerance through replication, safeguarding data against loss in the event of a broker failure. Data redundancy ensures data safety even during hardware failures. - **Real-Time Processing and Low Latency:** Kafka's real-time processing ensures low latency for instant data analysis, which is critical for real-time decision-making. ## Applications of Kafka As we explore the capabilities of Kafka, we realize that it goes beyond being just a regular data processing tool. Kafka is a strategic powerhouse that influences decision-making, operational efficiency, and overall effectiveness in various industries. In this section, we will discuss specific, practical applications of Kafka in different industries, demonstrating how its adaptability can solve unique challenges. **1. Manufacturing Operations Optimization:** - Real-time Production Monitoring: Kafka is used in manufacturing for continuous monitoring of production lines, equipment status, and inventory levels. This real-time visibility aids in optimizing production efficiency, reducing downtime, and enhancing overall supply chain management. - Quality Assurance and Yield Management: Companies utilize Kafka to monitor quality control metrics in real-time, enabling proactive measures to maintain product quality standards, minimize defects, and optimize production yield. **2. Predictive Maintenance:** Organizations use Kafka to collect and analyze sensor data from machinery and equipment to predict potential failures. This helps them optimize scheduled maintenance tasks to prevent costly downtime and disruptions **3. Supply Chain Management:** Kafka provides real-time visibility into supply chain operations. This enables companies to track shipments, monitor inventory levels, and coordinate with suppliers and distributors for efficient supply chain management. **4. Logistics and Transportation:** Companies use Kafka to track vehicle and shipment locations in real-time, optimizing routes through the processing of streams of GPS data. **5. Telecommunications:** Telecom operators utilize Kafka to monitor network performance metrics in real-time. This allows swift responses to outages or service degradations, ensuring a seamless communication network. **6. Financial Services:** Banks leverage Kafka to process transactions in real-time, enabling immediate fraud detection by analyzing patterns in transaction data as they occur. This enhances overall security and compliance in financial operations. ## Challenges and Considerations As beneficial as Kafka is in various industries, it also presents certain limitations and challenges that must be considered before deciding to use Kafka for your applications. 1. Performance: Kafka both receives and transmits data. When the flow of data is compressed or decompressed, the performance is affected. For example, if the data is decompressed it will eventually drain the node memory. As a result, it affects both throughput and performance. 2. Complexity: As we all know Kafka is an excellent platform for streamlining messages. However, in the case of migration projects that transform data, Apache Kafka gets more complex. Hence, to interact with both data producers and consumers you need to create data pipelines. 3. Tool Support: There is always a concern for startup companies to use Kafka over other options. Especially, if it remains in the long run. This is because a full set of management and monitoring tools are absent in Kafka. 4. Message Tweaking: Kafka uses system calls before delivering a message. Therefore, the messages are sensitive to modifications. Tweaking messages reduces the performance of Kafka to a greater extent. The performance is not impacted only under the condition of not changing the message. 5. Data Storage: Apache Kafka is not a recommended option for storing large sets of data. If the data is stored for a long period, the redundant copies of it are also stored. When this happens, the app must be ready to compromise its performance. For this reason, only use Kafka if there is a need to store data for a short period. Additionally, if you are interested in learning more about Kafka and its practical implementation, refer to our guide on Using [Kafka with Node-RED](https://flowfuse.com/blog/2024/03/using-kafka-with-node-red/). ## Conclusion This guide provides a high-level overview of Apache Kafka, including its definition, architecture, features, and applications in various industries and the challenges or limitations of using Kafka. Kafka's versatility in real-time data processing, decision-making, and operational efficiency is highlighted, with applications ranging from manufacturing to finance. The overview aims to provide a clear understanding of Kafka's role in handling data challenges and fostering innovation across sectors. # Using Kafka with Node-RED Kafka is one of the most powerful technologies enabling seamless data communication. Many individuals are utilizing it alongside Node-RED for real-time data exchange in their IoT and IIoT applications. However, some users are encountering difficulties in obtaining assistance with Kafka-related queries. During my recent visit to the Node-RED Forum, I noticed that while some Kafka-related queries have been answered nicely, others remain unanswered or have not been satisfactorily addressed, leaving users feeling stuck. To address this issue, we've created a comprehensive Kafka guide covering everything you need to know about Kafka, from installation and connection to data transmission. For newcomers to Kafka, we recommend reading our previous blog on [how Kafka is applied in manufacturing](https://flowfuse.com/blog/2024/03/using-kafka-in-manufacturing/), where we've covered the basics and practical applications extensively. ## Discussing problem and potential solution Let's start by discussing a problem: imagine a temperature sensor network across a city. We need to centralize and analyze this data in real time for effective monitoring and visualization. To resolve this problem we will use Kafka, Temperature sensors will feed data into Kafka through the Kafka producer. To retrieve real-time data for visualization and monitoring, we’ll be using Kafka Consumer. We will organize the data by region. The temperature data for each region will be managed in a specific Kafka topic partition. While in this guide, we will generate simulated data using random number expression and run both producers and consumers on the same system, practical scenarios often involve distributed setups across different devices or systems. ## Installing and running Kafka locally In this part, we’ll be installing Kafka locally using Docker to simplify the installation process, so make sure you’ve got Docker installed before you dive in. 1. Pull the zookeeper image if it is not already, and run the zookeeper container. ```text docker run -p 2181:2181 zookeeper ``` 2. Pull the Kafka image if it is not already, and run Kafka Container, expose PORT 9092. ```text docker run -p 9092:9092 ` -e KAFKA_ZOOKEEPER_CONNECT=.1:2181 ` -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://:9092 ` -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 ` confluentinc/cp-kafka ``` :cta-image{alt="Power Workplace relies on FlowFuse for scalability, reliability and security audits - book a demo" cta="demo" src="https://flowfuse.com/images/cta/power-workplace-book-demo.png"} ## Running Kafka on the cloud To run Kafka on the cloud, you can consider utilizing any cloud service according to your preferences. For a guide on running Kafka on a cloud platform, the procedures may differ. You can refer to the documentation provided by your preferred cloud service for detailed instructions. During the writing of this tutorial, I utilized [Aiven’s cloud data platform](https://aiven.io/kafka-connect){rel=""nofollow""} which offers the option to use Kafka in the free trial. However, you are free to choose any cloud service that suits your requirements and preferences. ## Installing Dashboard 2.0 We will be installing Dashboard 2.0 to display real-time temperature data of various regions on a chart. If you are new to Dashboard 2.0, we recommend referring to [Getting started with Dashboard 2.0](https://flowfuse.com/blog/2024/03/dashboard-getting-started/), which covers everything from basic concepts to installation and creating your first dashboard seamlessly. ## Installing and configuring Kafka custom node 1. Install `node-red-kafka-manager` by the palette manager. 2. Before connecting to Kafka, ensure you have the following information ready and environment variables set up as discussed below in the Adding environment variable section. - Host: The IP address or hostname of your Kafka broker server. - Port: Kafka typically uses port 9092 by default. Ensure this aligns with your Kafka broker's configuration. - SSL Configuration (if applicable): CA Certificate: The Certificate Authority (CA) certificate for validating the SSL connection. - SASL (Simple Authentication and Security Layer) Mechanism: Most of the Kafka broker servers use SASL for authentication such as 'PLAIN', 'SCRAM-SHA-256,' or 'SCRAM-SHA-512.' - Username: SASL username of Kafka broker server for authentication. - Password: SASL password of Kafka broker server for authentication. 3. Drag the Kafka Producer node onto the Canvas, click on that node, and click on the edit icon next to the broker input field to configure it. !["Screenshot showing configuration of kafka"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-kafka-configuration.png "Screenshot showing configuration of kafka") 4. Enable the TLS option if your Kafka broker server is using it for secure communication. 5. After enabling the TLS option click on the edit icon next to `add new tls-config` and upload the CA Certificate in PEM format. !["Screenshot showing TLS configuration for kafka"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-tls-configuration.png "Configuring tls for kafka") ## Adding Environment variables In this section, we will be setting up environment variables for Kafka configuration. If you have read our previous blog post, you may already know why we highly suggest using environment variables for every configuration. If not, please refer to our blog post on [Using Environment Variables in Node-RED](https://flowfuse.com/blog/2023/01/environment-variables-in-node-red/) for more information. !["Screenshot showing adding environment variable in Node-RED"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-setting-environment-variables.png "Screenshot showing adding environment variable in Node-RED") 1. Navigate to the instance's setting and then go to the environment section. 2. Click on the `add variable` button and add variables ( host, port, username, and password) for the configuration data that we discussed in the above section. To leverage the ease of configuration provided by the Kafka custom node that we are utilizing, ensure to set only one variable for both host and port in the following format: ```text [{"host":,"port":}] ``` 3. Click on the save button and restart the instance by clicking on the top right Action button and selecting the restart option. ## Creating a new Kafka topic In this section, we'll guide you through the process of creating a Kafka topic to handle temperature data from different city regions. To ensure the segregation of data for different zones within the city, we will configure the topic to create three partitions. 1. Drag an inject node onto Canvas. 2. Set `msg.topic` to `createTopics` string. 3. Set `msg.payload` to `[{"topic": "Temperature","partitions": 3,"replicationFactor":1}]`, you can create as many topics as you want at a time. 4. Drag the Kafka admin node onto Canvas. 5. Connect the inject node’s output to the Kafka admin node’s input. 6. Deploy the flow by clicking on the top-right red deploy button. 7. After the Deploy, click on the inject button to create a topic. ![Screenshot showing set payload in inject node for creating new Kafka topic"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-inject-node.png "Screenshot showing set payload in inject node for creating new Kafka topic") !["Screenshot showing Node-RED flow for creating new kafka topic"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-creating-topic.png "Screenshot showing Node-RED flow for creating new kafka topic") ## Sending Data to Kafka topic In this part, we’ll be setting up a producer to send simulated city region temperature data to Kafka. The producer will populate one of the available partitions in our temperature topic. We’ve already set up the temperature topic with 3 partitions. We’ll use these partitions to send data from each city region across the city (downtown region, suburban region, and industrial region). The partitioning process keeps the data separated, making it easier and faster to work with and analyze. 1. Drag a Kafka producer node onto Canvas. 2. Selected added Kafka configuration. 3. Click on that node and add the topic that we have created, set the key as `downtown`, and set the partition number as 0. (The partition index starts from zero) 4. Drag an inject node onto Canvas. 5. Set `msg.payload` to `$floor($random() * 100)` as a JSON expression and set the inject node to send the payload automatically after a specific interval of time. !["Screenshot showing kafka producer configuration"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-kafka-producer.png "Screenshot showing kafka producer configuration") ## Receiving data from Kafka topic In this section, we will be creating consumers who will subscribe to listen to downtown region temperature data. 1. Drag the Kafka consumer node onto Canvas. 2. Selected added Kafka configuration. 3. Click on that node, add the topic that we have created, and enter partition `0` from which it will read temperature data. 4. Add the Change node onto the Canvas and set `msg.topic` to `msg._kafka.key` and `msg.payload` to `$number(msg.payload)` because the Kafka custom node we are using is converting number data into a string. (consumer returns a Kafka object containing information related topic’s partition from data received, a key which we have set in the producer section to recognize data, and other information) 5. Add the ui-chart node on to Canvas and select the created group in which the chart will render. 6. Connect the Kafka consumer node’s output to change the node’s input and change the node’s output to the chart node’s input. !["Screenshot showing kafka consumer configuration"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-kafka-consumer.png "Screenshot showing kafka consumer configuration") !["Changing payload received from Kafka consumer"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-change-node.png "Changing payload received from Kafka consumer") Repeat the same steps to create producer and consumer for the rest of the two regions suburban and industrial, ensuring to set partitions 1 and 2 for, as we have already assigned partition 0 to the first producer created. ## Deploying the flow Our temperature monitoring system is now complete and ready for deployment. To initiate the deployment process, locate the red 'Deploy' button positioned in the top right corner and navigate to `https://.flowfuse.cloud/dashboard` !["Screenshot showing Node-RED flow of Real-time temperature monitoring system"](https://flowfuse.com/blog/2024/03/images/using-kafka-with-node-red-temperature-monitoring-system-flow.png "Screenshot showing Node-RED flow of Real-time temperature monitoring system") :video{ariaLabel="Video showing Dashboard 2.0 view of Real-time temperature monitoring system" autoPlay="true" height="338" loop="true" muted="true" playsInline="true" preload="none" width="600"} ## Conclusion In this guide, we’ve gone over everything you need to know about how to get started with Kafka and Node-RED. additionally, in this article, we’re going to focus on solving a problem where the sensor data across the city need to be centrally stored for efficient monitoring and visualization. By solving this problem step-by-step, you’ll understand how to integrate Kafka into your Node-RED applications. # How to Build an Admin Dashboard with Node-RED Dashboard 2.0 (2026) Managing and analyzing increasing amounts of data becomes crucial for organizations. Dashboard 2.0 and Node-RED help organizations access the data, normalize it, and visualize it. But what about controlling who can access what data? That's where an admin-only page comes in. Now With [Node-RED Dashboard 2.0](https://flowfuse.com/platform/dashboard/), we can also create robust and secure admin-only pages easily. In this guide, we'll provide you with step-by-step instructions to Build an Admin-only page with Node-RED Dashboard 2.0. If you're new to Dashboard 2.0, refer to our blog post [Getting Started with Dashboard 2.0](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) to install and get things started. ## Enabling FlowFuse User Authentication Before proceeding further, let’s enable FlowFuse user authentication. This step adds an extra layer of protection to our dashboard by adding a login page that restricts access exclusively to registered FlowFuse users. Additionally, it further simplifies the process for the FlowFuse Multiuser addon to track and access logged-in user's data on the dashboard. For more information, refer to the [documentation](https://flowfuse.com/docs/user/instance-settings/#flowfuse-user-authentication) and ensure that it is enabled. !["Screenshot displaying the configuration settings within the FlowFuse instance, enabling user authentication for enhanced security. "](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-flowfuse-instance-setting.png "Screenshot displaying the configuration settings within the FlowFuse instance, enabling user authentication for enhanced security. "){dataZoomable=""} ## Exploring FlowFuse Multiuser Addon The FlowFuse Multiuser Addon is a plugin developed for Dashboard 2.0 to access logged-in user data on the dashboard. To install and understand how the FlowFuse Multiuser Addon works, refer to [Exploring the FlowFuse User Addon](https://flowfuse.com/blog/2024/04/displaying-logged-in-users-on-dashboard/#enabling-flowfuse-user-authentication) ## Storing a list of Admin users Before we start building the admin-only page We need to store a list of admin users somewhere so that we can later display the admin-only page to those users only, For this guide we will store the admin list in the global context. 1. Drag an inject node onto the canvas. 2. Drag the 'change' node onto the canvas and set `global.admins` to a JSON array containing the usernames of admin users. This will store the created admin list in our Node-RED global context. !["Screenshot displaying the change node which which stores list of admins username in global context"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-change-node-for-storing-adminlist-to-global-contenxt.png "Screenshot displaying the change node which which stores list of admins username in global context"){dataZoomable=""} 3. Connect the inject node’s output to the change node’s input. 4. To store the list in a global context, click the inject node’s button once you've deployed the flow. ## Building an Admin-only page Now, let's proceed with the practical steps to implement the admin-only page: 1. Create a new page in Dashboard 2.0, where we will display sensitive data that we want to hide from regular users, this page will be our admin page. 2. Drag an event node on the canvas, then click on it, and select the UI base that contains your all pages including the admin page 3. Drag a switch node on the canvas, and add two conditions, one to check whether the user’s username is contained in the admin list or a second for otherwise, see the below image. !["Screenshot displaying the switch node which checks whether the logged-in user's username is contained in the admin list or not"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-switch-node-checking-page-viewer-isadmin.png "Screenshot displaying the switch node which checks whether the logged-in user's username is contained in the admin list or not"){dataZoomable=""} 4. Drag two change nodes onto the canvas, Configure the first change node to show the admin page by setting `msg.payload` as `{"pages":{"show":["Admin View"]}}`, and the second change node to hide the admin page by setting the payload as: `{"pages":{"hide":["Admin View"]}}`. !["Screenshot displaying the change node which contains payload to show admin page"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-change-node-for-showing-page.png "Screenshot displaying the change node which contains payload to show admin page"){dataZoomable=""} !["Screenshot displaying the change node which contains payload to hide admin page"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-change-node-for-hidding-page.png "Screenshot displaying the change node which contains payload to display admin page"){dataZoomable=""} 5. Connect the first change node's input to the switch node's first output and the second change node's input to the switch node's second output. 6. Drag a ui-control widget onto the canvas, then click on it and select ui-base which includes all your pages including the admin page. 7. Finally, connect both change node’s outputs to the ui-control’s input. ## Hidding Admin only page by default To hide an admin-only page by default to ensure regular users don't accidentally land on the admin-only page the following steps are needed. 1. Go to the Dashboard 2.0 sidebar, and select the layout tab. 2. Locate the admin-only page and click on the edit icon next to it. 3. Set visibility as "hidden". !["Screenshot displaying admin-only page configuration"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-admin-only-page-configuration.png "Screenshot displaying admin-only page configuration"){dataZoomable=""} ## Deploying the flow !["Screenshot displaying the FlowFuse Editor with flow of admin-only page"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-flowfuse-editior.png "Screenshot displaying the FlowFuse Editor with flow of admin-only page"){dataZoomable=""} 1. With your flow updated to include the above, click the "Deploy" button in the top-right of the Node-RED Editor. 2. Navigate to `https://.flowfuse.cloud/dashboard`. 3. When you visit the page for the first time, you'll need to log in with your FlowFuse username and password or through Single-Sign on. Now, if your username is added to the list of admin usernames stored in the global context, you will be able to see the admin-only page. !["Screenshot displaying the Dashboard view of normal users"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-dashboard-view-for-normal-users.png "Screenshot displaying the Dashboard view of normal users"){dataZoomable=""} !["Screenshot displaying the Dashboard view of admin users"](https://flowfuse.com/blog/2024/04/images/building-admin-panel-node-red-dashboard-2-dashboard-view-for-admin-users.png "Screenshot displaying the Dashboard view of admin users"){dataZoomable=""} ## Next step If you want to learn more about FlowFuse multiuser addon and personalize the multiuser dashboard. we do have many other resources, please refer to them to learn more. - [Webinar](https://flowfuse.com/webinars/2024/node-red-dashboard-multi-user/) - This webinar provides an in-depth discussion of the Personalised Multi-User Dashboards feature and offers guidance on how to get started with it. - [Personalised Multi-user Dashboards with Node-RED Dashboard 2.0](https://flowfuse.com/blog/2024/01/dashboard-2-multi-user/) - This article explores the process of building multi-user Dashboards secured with FlowFuse Cloud. - [Displaying logged-in users on Dashboard 2.0](https://flowfuse.com/blog/2024/04/displaying-logged-in-users-on-dashboard/) - This detailed guide demonstrates how to display logged-in users on Dashboard 2.0 which using the FlowFuse Multiuser addon and FlowFuse. - [Multi-User Dashboard for Ticket/Task Management](https://flowfuse.com/blueprints/flowfuse-dashboard/multi-user-dashboard/) blueprint, which allows you to utilize templates to develop Personalize multi-user dashboard quickly. # Dashboard 2.0: Milestones, PWA and New Components With a new release of Node-RED Dashboard 2.0 we have plenty of new fixes and improvements being added to the project. In this post, we'll deep dive into community contributions, PWA support, new Vuetify components, and the rest of the great work published in this latest release. ## Community Contributions We firstly wanted to take this opportunity to point out a big milestone that we're very proud to see in Node-RED Dashboard 2.0. This release marks the first time we've had more contributions in a single release from the community, than from FlowFuse employees. I think this is a testament to the community, and a big milestone in validating the success, and popularity, of Dashboard 2.0 in the wider Node-RED community. So thank you very much [@BartButenaers](https://github.com/bartbutenaers){rel=""nofollow""}, [@Ek1nox](https://github.com/Ek1nox){rel=""nofollow""}, [@fullmetal-fred](https://github.com/fullmetal-fred){rel=""nofollow""} and [@cgjgh](https://github.com/cgjgh){rel=""nofollow""} for for great efforts and initiative in improving Node-RED Dashboard 2.0. For anyone else that's interested in contributing to the project, please do reach out, and we'll be happy to help you get started. We also have a [Contributing Guide](https://dashboard.flowfuse.com/contributing/){rel=""nofollow""} if you want to dive straight in. ## Progressive Web App (PWA) Support The biggest community contribution we saw in this release was the addition of Progressive Web App (PWA) support. This feature was added by [@cgjgh](https://github.com/cgjgh){rel=""nofollow""}, and allows you to install your Node-RED Dashboard 2.0 applications directly onto your platform, including Windows, iOS and Android. This work will give your Dashboard's a much more native/natural feel when running on your own machines, and mean you no longer need to go via your browser to access your applications. ## New Vuetify (Preview) Components Available Vuetify is the component library on which most of our Dashboard 2.0 components are built. Our core widgets implement the more fundamental UI elements, but that doesn't stop you from building out fully customized interfaces yourself using our [ui-template node](https://dashboard.flowfuse.com/nodes/widgets/ui-template.html){rel=""nofollow""} with the vast collection of Vuetify components. Within the `ui-template` node, we natively support any of the [core Vuetify components](https://vuetifyjs.com/en/components/all/#containment){rel=""nofollow""}, but Vuetify itself is always evolving and often they release components into their [Vuetify Labs](https://vuetifyjs.com/en/labs/introduction/#what-is-labs){rel=""nofollow""}. In their latest releases, a few of the new components have caught our eye as we've seen them regularly requested in Dashboard 2.0. As such, we've now made available the following Vuetify components inside a `ui-template` node: #### Number Input ([docs](https://vuetifyjs.com/en/components/number-inputs/#installation){rel=""nofollow""}) ![Number Input](https://flowfuse.com/blog/2024/04/images/vuetify-numeric.png)*An example v-number-input from Vuetify's component library* We do have [plans](https://github.com/FlowFuse/node-red-dashboard/issues/41){rel=""nofollow""} for this to become a core widget, and will likely introduce this sooner, rather than later, however, in the mean time, you can now use the `v-number-input` component in a `ui-template` node to create your own number inputs instead. ```html ``` #### Sparkline ([docs](https://vuetifyjs.com/en/components/sparklines/#installation){rel=""nofollow""}) ![Sparkline](https://flowfuse.com/blog/2024/04/images/vuetify-sparkline.png)*An example v-sparkline rendering the output from a ui-slider* Sparklines are a great way to visualize data trends in a small space, and we've seen them requested a few times in the past. Now you can use the `v-sparkline` component in a `ui-template` node to create your own sparklines. This will also likely become a standalone node at some point too, possibly as a third-party widget, but for now implementing into a `ui-template` is very straight forward. In the following example `ui-template`, we append any incoming `msg.payload` to a `value` array and render the sparkline accordingly. ```html ``` There is no limitation on *where* you can use the sparkline either, we could, for example, add it to a `v-data-table` to show a sparkline of a particular feature for each row in the table: ![Data Table with Sparkline](https://flowfuse.com/blog/2024/04/images/vuetify-data-table-sparkline.png)*An example v-data-table that renders a v-sparkline on each row* Here we see the corresponding template for the above `v-data-table` example: ```html ``` #### Treeview ([docs](https://vuetifyjs.com/en/components/treeview/#installation){rel=""nofollow""}) ![Treeview](https://flowfuse.com/blog/2024/04/images/vuetify-treeview.png)*An example v-treeview from Vuetify Lab's component library* The `v-treeview` component is a great way to visualize hierarchical data in a tree-like structure. We've seen this requested a few times in the past, and now you can use the `v-treeview` component in a `ui-template` node to create your own treeviews. There is still scope for this to, one day, become a core or third party widget, but in the mean time, it's very easy to get this up and running in a `ui-template` node. The Treeview example, and other examples above are available in this sample flow: :iframe{allow="clipboard-read; clipboard-write" height="340px" src="https://flows.nodered.org/flow/0ac4d82aaf97409cb0dce9812cfa214c/share?height=300" style="border: none;" width="100%"} ## Other Highlights Whilst the above are the main highlights of this release, we've also had a number of other smaller improvements and fixes that have been added to the project. These include: - UI Radio Group - Dynamic radio options in [#765](https://github.com/FlowFuse/node-red-dashboard/pull/765){rel=""nofollow""} - UI Notification - Notification output & output msg when button clicked in [#766](https://github.com/FlowFuse/node-red-dashboard/pull/766){rel=""nofollow""} - UI Dropdown - Clear dropdown selection in [#775](https://github.com/FlowFuse/node-red-dashboard/pull/775){rel=""nofollow""} - UI Button - Add "Emulate Click" option in [#783](https://github.com/FlowFuse/node-red-dashboard/pull/783){rel=""nofollow""} You can see the full list of changes in the [1.8.0 Release Notes](https://github.com/FlowFuse/node-red-dashboard/releases/tag/v1.8.0){rel=""nofollow""}. ## Follow our Progress New features and improvements are coming to Node-RED Dashboard 2.0 every week, if you're interested in what we have lined up, or want to contribute yourself, then you can track the work we have lined up on our GitHub Projects: - [Dashboard 2.0 Activity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/1){rel=""nofollow""} - [Dashboard 2.0 Planning Board](https://github.com/orgs/FlowFuse/projects/15/views/4){rel=""nofollow""} - [Dashboard 1.0 Feature Parity Tracker](https://github.com/orgs/FlowFuse/projects/15/views/5){rel=""nofollow""} If you have any feature requests, bugs/complaints or general feedback, please do reach out, and raise issues on our relevant [GitHub repository](https://github.com/FlowFuse/node-red-dashboard){rel=""nofollow""}. # Displaying logged in user on Node-RED Dashboard 2.0 (2026) About a month ago, a powerful solution became available to the Node-RED community to deal with users and allow multiple to interact with the same dashboard in a personalized manner. It's called the Multli user Dashboard for Node-RED. In this guide, we will provide a step-by-step guide to show you how to secure your dashboard and access and display logged in user information on Dashboard 2.0. If you're new to Dashboard 2.0, refer to our blog post [Getting Started with Dashboard 2.0](https://flowfuse.com/blog/2024/03/dashboard-getting-started/) ## Enabling FlowFuse User Authentication Before we display logged-in user data on the dashboard, first we need to set up a login mechanism with FlowFuse for the dashboard. This simplifies securing [Node-RED Dashboard](https://flowfuse.com/platform/dashboard/)s and provides contextual user data within the Dashboard itself for who is logged in. 1. Navigate to the Instance "settings". 2. Select the "Security" tab. 3. Enable “FlowFuse User Authentication” Now, the first time you visit the dashboard, you'll need to log in with your registered FlowFuse username and password !["Screenshot displaying the configuration settings within the FlowFuse instance, enabling user authentication for enhanced security."](https://flowfuse.com/blog/2024/04/images/displaying-logged-in-user-flowfuse-instance-setting.png "Screenshot displaying the configuration settings within the FlowFuse instance, enabling user authentication for enhanced security."){dataZoomable=""} ## Exploring the FlowFuse User Addon The FlowFuse User Addon is a plugin developed for Dashboard 2.0, leveraging the FlowFuse API to retrieve information about logged in user. ### Installing Flowfuse user addon :cta-image{alt="Wenco deploys new dashboard pages in days with FlowFuse - book a demo" cta="demo" src="https://flowfuse.com/images/cta/wenco-book-demo.png"} 1. Click the Node-RED Settings (top-right) 2. Click "Manage Palette" 3. Switch to the "Install" tab 4. Search for `@flowfuse/node-red-dashboard-2-user-addon` 5. Click "Install" ### How it Works In this addon, user information is attached to the `msg` emitted by Dashboard 2.0 nodes. This user information object is attached as `msg._client.user`. Below is an example of how that object looks: ```text { "userId": "", // unique identifier for the user "username": "", // FlowFuse Username "email": "", // E-Mail Address connected to their FlowFuse account "name": "", // Full Name "image": "" // User Avatar from FlowFuse } ``` Behind the scenes, the user addon is appending the user object to the `msg`, via the SocketIO auth option. We make the socketio object available via a computed [setup](https://dashboard.flowfuse.com/contributing/guides/state-management.html#setup-store){rel=""nofollow""} object, this means that we can also access user data in any ui-template widget with `{{ setup.socketio.auth.user }}`, in the `