Skip to main content

Building Digital Work Instructions Dashboard for the Shop Floor

Give every operator their own work instructions, progress, and production history.

By Sumit Shinde, Technical Writer || 39 min read
Back to Blog Posts
Image representing null
TL;DR

Build a FlowFuse digital work instructions application that authenticates each operator, loads only their assigned work orders, guides them step by step with per-operator progress tracking, and lets them report defects, all tied to their logged-in identity.

Operators need to know what to build, how to build it, and what to do when something breaks. Digital work instructions put that on a screen, guided steps, a checklist, buttons to finish or flag a defect, and retire the paper binder.

But shared stations raise a question paper never had: who's using it? Without a way to tell operators apart, the work order, the current step, and the record of what happened all blur together. Authentication draws the line, the operator's identity becomes the thread the app hangs on.

Digital work instructions dashboard showing operator work orders, production status Digital work instructions dashboard showing operator work orders, production status

Operator interface displaying guided work instructions with step completion and defect reporting options Operator interface displaying guided work instructions with step completion and defect reporting options

In this article, we'll build a digital work instructions app in FlowFuse: an operator interface with work orders, step-by-step assembly guidance, defect reporting, and traceability via authentication.

Note: This demo is deliberately configured with a preset demo user so anyone can try it, even without being on the team where it's deployed. The version you build following this article uses FlowFuse User Authentication, which limits dashboard access to members of the same team.

You can interact with the live demo here: Try the Digital Work Instruction Dashboard Demo.

What You'll Need

Before you start building, get these ready:

  • A FlowFuse account. Sign up for FlowFuse Cloud, or use a self-hosted instance.
  • A FlowFuse instance up and running. If you don't have one yet, create a new instance from your FlowFuse Platform.
  • FlowFuse Dashboard installed. This tutorial uses @flowfuse/node-red-dashboard nodes (ui-template, ui-table, ui-form, ui-button, ui-control, ui-event, ui-text) to build the operator interface. Install it from the Palette Manager if it isn't already in your instance.
  • The FlowFuse Dashboard user addon. This is what attaches the logged-in user to every dashboard message. We'll install it in the first section.

Note: The Multi-User addon is available to Teams and Enterprise Self-Hosted customers. If you're on a different tier, contact us for the configuration to get started.

How the Application Works

Before we build anything, let's walk through what the app does and how the pieces fit together. There are three pages, and one idea holding them together.

  1. Home. After logging in, the application uses the operator's username to retrieve only the work orders assigned to them from the ERP or MES. It displays their station, production summary, the highest-priority work order, and the remaining queue. Selecting Start Work Order opens the instructions.
  2. Instructions. Operators follow step-by-step instructions with images, checklists, and target cycle times. They can progress through each step, complete the operation, or report an issue at any time.
  3. Report Issue. Operators can quickly log defects by selecting the issue type, severity, affected part, and description, with the issue automatically linked to the current work order.

The logged-in username acts as the application's lookup key. It retrieves the operator's assigned work orders and stores their active work order and current step, allowing them to resume exactly where they left off, even on shared production stations.

There are two different questions the app has to answer, and it's worth keeping them separate from the start:

  • "Where is this operator's data stored?" — under a persistent global key named after their username, e.g. global.get('operator1', 'persistent'). This is a durable, shared store, and it's meant to be: it's what lets progress survive reloads and reconnects.
  • "Who is acting right now, in this message?" — this must be read fresh from the message that triggered the flow (msg._client.user), not from a single shared "current user" variable. A shared variable has only one value at a time; the moment a second operator's browser connects, it overwrites the first operator's identity for the whole running flow. We'll come back to this the moment it becomes relevant, but it's the one rule that makes every "per operator" claim in this tutorial actually true when two people are logged in at once.

Importing the Simulated Flow

Instead of connecting to a real ERP or MES, we'll use a simple simulated backend that serves sample work orders through an HTTP API.

  1. Import the following flow into FlowFuse and click Deploy.

The flow initializes a set of sample work orders and exposes REST APIs that the Digital Work Instructions application uses throughout this tutorial. The GET /workorders endpoint returns the active work orders, while the other endpoints simulate completing a work order, reporting defects, and retrieving production statistics. This lets you build and test the application without a live ERP or MES. Later, you can replace these endpoints with calls to your production system while keeping the rest of the application unchanged.

Setting Up the Dashboard Layout

The application consists of three pages. Most widgets are placed inside named groups, while a few are scoped differently. See the Dashboard layout docs if you're new to how pages, groups, and bases relate. The app bar greeting is UI-scoped, and the work instruction widget is page-scoped, allowing it to fill the entire Instructions page without requiring a group. Before adding any widgets, create the dashboard structure by setting up the pages and groups.

  1. Create three ui-page nodes. The Dashboard automatically creates a base dashboard the first time you add a Dashboard node to the canvas.

    • Home (path: /home) – The operator's landing page after login. Set the layout to Notebook to match the demo application.
    • Instructions (path: /instructions) – Displays the step-by-step work instructions. Set the layout to Grid.
    • Report Issue (path: /report-issue) – Contains the defect reporting form. Set the layout to Grid.
  2. Configure the theme for each page as desired.

  3. On the Home page, create four ui-group nodes:

    • Current Station
    • Stats
    • Current Work Order
    • Up Next
  4. On the Report Issue page, create a single ui-group named Report Issue. This group will contain the issue reporting form and its action buttons.

The Instructions page does not require a ui-group because the work instruction widget is page-scoped and automatically occupies the full page.

With this layout in place, each widget added in the following sections can be assigned to the appropriate group, or directly to the page when required.

Enabling FlowFuse User Authentication

Everything starts with a login. Without authentication, the dashboard cannot identify who is using it, so every visitor sees the same experience.

Note: FlowFuse User Authentication grants dashboard access to members of the same team as the instance. Operators you want to sign in must belong to that team, otherwise they won't be able to reach the dashboard. If you need people outside the team to use the application, you'll need to add them to the team.

  1. Open your FlowFuse instance Settings.
  2. Select the Security tab.
  3. Enable FlowFuse User Authentication.

Screenshot: the Security tab in instance settings with FlowFuse User Authentication enabled Enable FlowFuse User Authentication in the instance Security settings. You'll also create a Personal Access Token here for authenticating API requests later in the tutorial.

The first time someone opens the dashboard, they will be prompted to sign in using their FlowFuse username and password. Once authenticated, the dashboard can personalize the experience for each operator throughout the rest of this tutorial.

Because this tutorial also interacts with the FlowFuse API, you'll need to create a Personal Access Token:

  1. In the same Security tab, click Add Token.
  2. Enter a name for the token and choose an expiration date.
  3. Click Create, then copy the generated token and save it somewhere secure. You will need it later in the tutorial.

Security note: Every http request node in this tutorial authenticates with this token, and for the sake of a self-contained tutorial we reference it as a plain Bearer token on each node. Don't leave it hardcoded that way in a real deployment. Store it as an environment variable on your FlowFuse instance and reference that variable from each http request node's auth config instead, so the raw token never sits in exported flow JSON or version control.

Installing the User Addon

Authentication identifies the user, but your flows also need access to that information. The FlowFuse User Addon attaches the authenticated user's details to every Dashboard message.

  1. Open Manage Palette.
  2. Go to the Install tab.
  3. Search for @flowfuse/node-red-dashboard-2-user-addon.
  4. Click Install.

After installation, every Dashboard message includes a msg._client.user object:

{
"userId": "",
"username": "",
"email": "",
"name": "",
"image": ""
}

The same information is available in a ui-template using setup.socketio.auth.user (or this.setup.socketio.auth.user in the script).

  1. In the FF Auth sidebar tab, ensure Include Client Data is enabled.
  2. Also enable Accept Client Data for ui-template, ui-control, ui-event, ui-form, and ui-table so each browser session is handled independently and operators don't affect each other's dashboard.

Screenshot: FF Auth settings showing Include Client Data and Accept Client Data enabled for Dashboard nodes Enable Include Client Data and Accept Client Data to make Dashboard interactions user-specific.

This is the setting that matters most in this tutorial, and it's worth being explicit about why. With Accept Client Data enabled, every message a widget sends carries msg._client.user for the session that sent it. That's a per-session identity attached directly to the message, as opposed to a single shared variable that every session would otherwise read and overwrite. We'll use msg._client.user as the source of truth for "who is acting right now" in every function node from here on, rather than a global variable.

Greeting the Logged-In Operator

The first, simplest payoff: show the operator their name and avatar in the app bar, so it's obvious who's signed in at this station. This uses Vue's Teleport to render into the header's action area from a single widget that lives on every page, so you don't have to add it per page.

  1. Add a ui-template node and name it "App Bar User Info".
  2. Set its type to Widget (UI-Scoped) and select your "My Dashboard" ui-base. UI-scoped widgets render on every page automatically, so one widget covers the whole app, no group needed.
  3. Paste in the snippet below:
<template>
<!-- Teleport into #app-bar-actions, the action bar's right-hand corner -->
<Teleport v-if="loaded" to="#app-bar-actions">
<div class="user-info">
<img :src="setup.socketio.auth.user.image" />
<span>Hi, </span>
</div>
</Teleport>
</template>

<script>
export default {
data() {
return { loaded: false };
},
mounted() {
// Wait for mount so #app-bar-actions exists before teleporting into it.
this.loaded = true;
}
}
</script>

<style>
.user-info { display: flex; align-items: center; gap: 8px; }
.user-info img { width: 24px; height: 24px; }
</style>

Deploy and open the dashboard. The signed-in operator's name and avatar appear in the top-right corner. You don't redeploy when a different operator logs in, the addon fetches each user's data at runtime, so everyone sees their own. This widget reads identity straight off the browser's own socket session (setup.socketio.auth.user), so it's already per-operator by construction, no shared state involved.

Screenshot: the dashboard app bar showing the logged-in operator's avatar and greeting The signed-in operator greeted by name and avatar in the app bar, rendered from a single UI-scoped widget.

Seeding the Operator into Context on Login

Greeting the operator is the visible half. The other half is making their identity available to every function node, not just the widgets.

You might reach for a single shared global here, global.set('user', msg._client.user, 'persistent') on connect, then read it everywhere. Don't. Global context is one store for the whole flow, not one per session, so if two operators are connected at once, whichever connects last overwrites the identity for everyone, and function nodes start attributing the wrong operator's actions to the wrong person.

Instead, read msg._client.user off each message for "who is acting right now," and use the username only as a durable storage key for saved state. A shared global is still handy as a bootstrap fallback for the very first tick of a page, before any client-tagged message has round-tripped.

  1. Add a ui-event node named "Client connected" and select the "My Dashboard" ui-base. It fires when a browser session connects.
  2. Add a change node named "Seed globals" with these set rules, in order:
    • user (global, persistent) → msg._client.user. Bootstrap fallback only, not the source of truth elsewhere.
    • StationContext.stationName (global) → your station's name, e.g. Wheel Assembly Station 12.
    • StationContext.stationId (global, persistent) → this station's ID, e.g. ST12.
    • Instructions (global, persistent) → the instruction set (steps, images, target times, checklists), cached so the Instructions page loads instantly.
  3. Wire "Client connected" into "Seed globals".

From here on, every function node that needs the current operator uses this pattern, per-session identity first, bootstrap global only as a fallback:

const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

Showing the Station and Stats on the Home Page

The Home page shows the current station and a quick summary of work order statistics. Both update whenever the page opens.

Display the Station Name

  1. From the Client connected event, add a change node named Read station name and set msg.payload to global.StationContext.stationName.
  2. Connect it to a ui-text node named Current Station in the Current Station group. Set the value to msg.payload.

Display the Stats

  1. Add a ui-control node named Page changed, set its event to change, and select your dashboard.
  2. Add a function node named Build /stats request URL and paste in the following code. Replace https://your-instance.flowfuse.cloud with your own FlowFuse instance URL.
const StationContext = global.get('StationContext', 'persistent');
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

const params = new URLSearchParams();
params.set('stationId', StationContext.stationId);
if (username) params.set('username', username);

msg.url = `https://your-instance.flowfuse.cloud/workorders/stats?${params}`;
msg.method = "GET";
return msg;
  1. Add an http request node that uses msg.method and msg.url, returns a parsed JSON object, and uses Bearer authentication.
  2. Configure the bearer token using the access token you created earlier. For production, store it as an environment variable instead of hardcoding it in the flow.
  3. Add a ui-template named Stats Cards in the Stats group and paste in the template below.
  4. Wire the nodes: Page changed → Build /stats request URL → HTTP Request → Stats Cards.

The request includes the logged-in username, so the Assigned to me count is personalized for each operator, and because ui-control is client-data-aware, msg._client.user reflects whichever operator's browser triggered the page change.

Placeholder: Stats Cards widget rendered on the Home page The Home page displays station statistics, including total, assigned to me, completed, and defected work orders.

Loading the Operator's Work Orders

Now for the main part of the Home page: fetching and displaying the operator's work orders. Because the request includes the logged-in username, each operator only sees the work orders assigned to them at the current station.

  1. Add a ui-control node named Page changed, select your dashboard, and set the event to change. It fires whenever the operator navigates to the Home page, triggering the work order request.

  2. Add a function node named Build /workorders request URL, paste in the code below, and replace https://your-instance.flowfuse.cloud with your own FlowFuse instance URL.

const StationContext = global.get('StationContext', 'persistent');
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

const stationId = StationContext?.stationId;

const params = new URLSearchParams();
params.set('stationId', stationId);
if (username) params.set('username', username);

msg.url = `https://your-instance.flowfuse.cloud/workorders?${params.toString()}`;
msg.method = 'GET';
return msg;
  1. Add an http request node, set the Method to Use msg.method, leave the URL field blank so it uses msg.url, set the Return type to a parsed JSON object, and configure Bearer authentication. Use the access token you created earlier as the bearer token. For production deployments, store the token as an environment variable and reference it here instead of hardcoding it in the flow.

  2. Connect the http request node to a link out node named Work Orders Out. The widgets that display the work orders will connect to this node using their own link in nodes, allowing the same API response to be reused without stretching wires across the canvas.

The username in the request is what makes the dashboard personal. Two operators can open the same page at the same station, yet each receives only the work orders assigned to them, because each one's ui-control event carries their own _client.user, not a shared one.

Splitting the Current Order From the Queue

The Home page shows one work order front and centre and the rest as a queue. A function splits the list, sorting by priority so the most urgent job is the one the operator sees first. This function also records which work order the operator is about to start, that's the record the Instructions page, the Complete Operation flow, and the Report Issue flow will all read back later, so it has to be attributed to the right operator from the start.

  1. Add a link in node named "link in 6" and point it at the "work orders out" link.
  2. Add a function node named "split current vs queue (by priority)" with 2 outputs:
const orders = Array.isArray(msg.payload) ? msg.payload : [];

// Sort by priority (Critical/Urgent -> High -> Medium -> Normal -> Low)
const priorityOrder = { critical: 0, urgent: 0, high: 1, medium: 2, normal: 3, low: 4 };
const sorted = [...orders].sort(
(a, b) => (priorityOrder[(a.priority || '').toLowerCase()] ?? 5)
- (priorityOrder[(b.priority || '').toLowerCase()] ?? 5)
);

const currentWorkOrder = sorted[0] || null;

// Record which work order the current operator is about to see/start.
// Identity comes from the triggering message's client data, not a shared global,
// so this is always attributed to the operator whose browser fetched this list.
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

if (username && currentWorkOrder) {
const userState = global.get(username, 'persistent') || {};
userState.workOrderId = currentWorkOrder.workOrderId;
global.set(username, userState, 'persistent');
}

return [
{ payload: currentWorkOrder }, // output 1: current order (for card)
{ payload: sorted.slice(1) } // output 2: remaining orders (queue)
];
  1. Import the Current Work Order Card ui-template below and assign it to the Current Work Order group. The complete component is provided below, so there's no need to recreate it.

Tip: Whenever you need a custom Dashboard widget, you don't have to write the Vue code yourself. Use FlowFuse Expert and describe the widget in plain English and it will generate the ui-template for you.

  1. Wire output 1 to the "Current Work Order Card" ui-template in the Current Work Order group. It shows the work order ID, station, operation and estimated cycle time, the vehicle (model, variant, VIN, colour), a priority chip, and a Start Work Order button. On click, the button sends { action: 'start', workOrderId, stationId }.

Placeholder: Current Work Order Card rendered on the Home page The Current Work Order card: work order ID, priority chip, station, operation, vehicle details, and the Start button.

  1. Add a ui-table node and assign it to the Up Next group. Turn off auto-columns and add two text columns as shown below: Work Order (workOrderId) and Priority (priority). Wire output 2 of the split function into it.

Placeholder: Up Next table listing queued work orders The Up Next table showing the remaining queued work orders with their priority.

  1. From the card, wire a change node named "go to Instructions" that sets payload (msg) to Instructions, then into a ui-control node with its event set to change to switch the page.

When the operator taps Start Work Order, the card sends them to the Instructions page. Because the split function already saved this operator's workOrderId under their own username, the Instructions page, Complete Operation, and Report Issue flows can all look it back up correctly, even if another operator is doing the exact same thing on another screen at the same moment.

Loading the Cached Instruction Set

The Instructions page needs the steps to show. Because the instruction set was cached in context on login, the page can paint it instantly without another round trip.

  1. Add a ui-event node named "Page opened" for the Instructions page.
  2. Add a change node named "Load cached instruction set". Set payload (msg) to the persistent global Instructions.
  3. Wire "Page opened" into it, and its output into the "Work Instruction Widget" you build next.

Remembering Each Operator's Step

This is something paper can't do and shared screens get wrong. If an operator completes the first two steps of a five-step wheel installation and returns later, they should resume at step 3, not start again at step 1. We achieve this by saving their progress against their username, the one they were identified as when the step was saved, not whichever username happens to be sitting in a shared global at read time.

The instruction widget emits two housekeeping actions: save_step whenever the operator moves between steps, and load_step when the page opens and needs to know where to resume. One function node handles both.

  1. Import the Work Instruction Widget ui-template below, assign it to the Instructions page, and set its scope to Page. The widget displays one instruction at a time with its image, target cycle time, and checklist. Operators can't move to the next step until every checklist item is completed, and the final step changes the button to Complete Operation.

Tip: Whenever you need a custom Dashboard widget, you don't have to write the Vue code yourself. Use FlowFuse Expert and describe the widget in plain English and it will generate the ui-template for you.

Placeholder: Work Instruction Widget showing a step with image, checklist, and navigation controls The Instructions page: step image, checklist, target time, and Previous / Report Issue / Next controls.

  1. Add a function node named "Track step (save/load per user)":
// Remembers the current step per user for the active work order.
// Handles two actions from the widget:
// save_step - persist the step the operator is on
// load_step - reply with the saved step so the widget can resume
const p = msg.payload || {};

// Identity comes from this message's own client data first. This is what
// keeps two operators using the widget at the same time from reading or
// writing each other's progress.
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;
if (!username) return null;

// Read the user's saved state
const userState = global.get(username, "persistent") || {
workOrderId: null,
stepIndex: 0
};

switch (p.action) {
case "save_step":
userState.stepIndex = p.stepIndex ?? 0;
global.set(username, userState, "persistent");
return null;

case "load_step":
msg.payload = {
workOrderId: userState.workOrderId,
stepIndex: userState.stepIndex ?? 0
};
return msg;

default:
return null;
}
  1. Wire the "Work Instruction Widget" output into this node, and wire the node's output back into the widget so the load_step reply can jump it to the saved step.

Look at the store key: global.get(username, ...) and global.set(username, ...). The operator's own username is the storage key, so two operators at the same station never overwrite each other, their progress lives under different keys, and because that key is now sourced from msg._client.user rather than a shared global, it's always the username of whoever's browser actually sent this particular save_step or load_step message.

Deploy, walk halfway through an operation, then reload the page. It reopens where you left off, because the step is filed under your name.

Completing the Operation as the Logged-In User

When the operator completes the final instruction, the widget sends { action: "complete_operation" }. The flow looks up the operator's stored work order and builds the payload that will be sent to the API.

  1. Add a link in node and connect it to the Work Instruction Widget actions link.
  2. Add a switch node named Route by Action and route messages where msg.payload.action equals complete_operation.
  3. Add a function node named Build Complete Payload and paste in the following code.
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

if (!username) {
node.warn('No logged-in user found — cannot complete operation');
return null;
}

const stored = global.get(username, 'persistent') || {};
const workOrderId = stored.workOrderId || null;

msg.payload = { workOrderId };
return msg;
  1. Add an http request node, set the Method to POST, configure Bearer authentication using your access token, and set the URL to https://your-instance.flowfuse.cloud/workorders/complete. Replace https://your-instance.flowfuse.cloud with your own FlowFuse instance URL.

  2. Add a change node that sets msg.payload to Home, then connect it to a link out node. This will be used in the next step to reset the operator's state and navigate the operator back to the Home page.

Because the payload is built from the logged-in operator's stored state, keyed off that same operator's per-message identity, the API always completes the correct work order, even when multiple operators are using the dashboard at the same time.

Reporting a Defect as the Logged-In Operator

If an operator encounters a defect, they can report it from the Work Instruction Widget. The report is automatically linked to the work order they're currently performing, so there's no need to enter a work order ID manually.

The Report Issue button sends { action: "Report Issue" }. Use that action to open the report form.

  1. Add a link in node named Widget Action In and connect it to the Work Instruction Widget actions link.

  2. Add a switch node named Route by Action and route messages where msg.payload.action equals Report Issue.

  3. Add a change node that sets msg.payload to Report Issue, then connect it to a ui-control node to navigate to the Report Issue page.

  4. Add a ui-form node named Report Issue Form to the Report Issue group with four required fields:

    • Issue Type (dropdown)
    • Severity (dropdown)
    • Affected Part (text)
    • Description (multiline)

    Populate the dropdown fields with the issue types and severity levels used in your application.

Screenshot: Report Issue form fields showing the Issue Type, Severity, Affected Part, and Description inputs The Report Issue form collects the defect details before submitting them against the current work order.

Screenshot: Configuring the dropdown options for the Issue Type and Severity fields in the ui-form node Configure the Issue Type and Severity dropdowns with the options used in your application.

  1. Add a function node named Prepare Defect Payload and paste in the following code.
const p = msg.payload || {};

const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

if (!username) {
node.warn('No logged-in user found — cannot report defect');
return null;
}

const stored = global.get(username, 'persistent') || {};
const workOrderId = stored.workOrderId || null;

const issueType = p.type;
const severity = p.severity;
const affectedPart = p['affected-part'];
const description = p.description || null;

if (!workOrderId || !issueType || !severity || !affectedPart) {
node.warn('Missing required defect fields — defect not sent');
return null;
}

msg.payload = {
workOrderId,
issueType,
severity,
affectedPart,
description
};

return msg;
  1. Add an http request node, set the Method to POST, configure Bearer authentication using your access token, and set the URL to https://your-instance.flowfuse.cloud/workorders/defect. Replace https://your-instance.flowfuse.cloud with your own FlowFuse instance URL, then connect the request to a link out node so the flow continues to the reset step.

  2. Add a ui-button named Back to Work Instructions to the same group and connect it to a ui-control node that navigates back to the Instructions page. Since the operator's progress is stored against their username, they return to the same instruction they left.

Screenshot: Completed Report Issue page showing the defect reporting form and Back to Work Instructions button The completed Report Issue page where operators can submit a defect or return to the work instructions.

The work order ID is retrieved from the logged-in operator's stored state, ensuring every defect report is associated with the correct work order, and the lookup key for that state again comes from this message's own client data, not a value that could have been overwritten by someone else's login in the meantime.

Clearing State When Work Ends

When an operation is completed or a defect is reported, clear the operator's saved progress so the next work order starts from step 1 instead of resuming a finished one.

  1. Add a link in node named Complete / Defect Result In and connect it to the link out nodes from the Complete Operation and Report Defect flows.
  2. Add a switch node that checks msg.statusCode equals 200, so the state is only cleared after a successful API response.
  3. Add a function node named Clear User Step State and paste in the following code.
const username = msg._client?.user?.username
|| global.get('user', 'persistent')?.username;

if (!username) return null;

global.set(username, {}, 'persistent');

msg.payload = "Home";
return msg;
  1. Connect the function node to a ui-control node to navigate the operator back to the Home page.

Because the state is stored and cleared by username, and that username is read from the message that carried the successful API response rather than a shared global, resetting one operator's progress never affects anyone else, even if a second operator has logged in on another screen in between.

Deploy the flow and open the dashboard. After signing in, operators see only the work orders assigned to them. If they leave and return, they resume from the same instruction. Once they complete the operation or report a defect, their progress is cleared and they're returned to the Home page, ready for the next work order.

Where to Go Next

You've built a personalized digital work instructions application that knows who the operator is, what work they're assigned, and where they left off, correctly, even when several operators are on the dashboard at once. Replacing the simulator with a real ERP or MES is simply a matter of pointing the HTTP Request nodes to your production /workorders, /workorders/complete, and /workorders/defect endpoints.

From there, you can extend the application with production history, quality tracking, role-based experiences, and OEE dashboards, all powered by the same operator identity. To see how FlowFuse fits into modern automotive manufacturing, explore the Automotive solutions page.

Want to connect this to your real ERP or MES?

Talk to our team about integrating digital work instructions with your production systems, adding full traceability, and rolling this out across your shop floor.

Frequently Asked Questions

About the Author

Sumit Shinde

Technical Writer

Sumit is a Technical Writer at FlowFuse who helps engineers adopt Node-RED for industrial automation projects. He has authored over 100 articles covering industrial protocols (OPC UA, MQTT, Modbus), Unified Namespace architectures, and practical manufacturing solutions. Through his writing, he makes complex industrial concepts accessible, helping teams connect legacy equipment, build real-time dashboards, and implement Industry 4.0 strategies.

Related Articles:

Sign up for updates