Smart Manufacturing

Designing Downtime Reason Codes That Operators Actually Use

How to build a downtime reason code tree that stays short, specific and actionable, plus an operator input flow that publishes reasons over MQTT alongside automatically detected machine states.

Downtime reason codes illustration showing a three-level reason tree on an operator tablet beside a stopped machine publishing its state over MQTT
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Downtime reason codes decide whether your downtime data leads to action or just fills a database. This guide shows how to design a short, structured reason tree that operators can use in a few seconds, how to map it to OEE losses, and how to build an input flow that publishes reasons over MQTT next to the machine states your sensors already detect. You will leave with a starter tree, a JSON format for it and working code for the operator side.

Why Most Reason Code Lists Fail

Plants usually start downtime tracking with good intentions and a long list. Within months the data shows the same pattern: a large share of stops coded as "Other" or "Miscellaneous", a few codes used for everything, and dozens never used at all. The root causes are predictable:

  • Too many codes. Operators facing a scrolling list of sixty options pick the first plausible one.
  • Codes named after departments. "Maintenance" or "Quality" describes who is responsible, not what happened, so nobody can act on it.
  • Overlapping meanings. "Jam" and "Material problem" both fit a misfeed, so the same event gets two codes depending on who is on shift.
  • No link to detection. If the operator also has to record when the stop started and ended, accuracy collapses. Timing should come from the machine; only the reason should come from a person.

The fix is a short hierarchy, specific wording, and automatic state detection.

Principles for Good Downtime Reason Codes

  1. Separate detection from explanation. Machines report RUNNING and STOPPED automatically; operators only answer "why?".
  2. Use a tree, not a flat list. Two or three levels: a category, a reason and optionally a detail. Each screen shows only a handful of choices.
  3. Name the cause, not the department. "Waiting for material" beats "Logistics".
  4. Make codes mutually exclusive. If two codes could describe the same event, merge them or write a one-line definition that separates them.
  5. Keep a small fixed top level. Map the top level to OEE losses so reports roll up automatically.
  6. Limit "Other". Require a free-text comment when "Other" is chosen and review those comments every week; recurring ones become new codes.
  7. Let machine alarms fill in where possible. If the controller reports a fault code, propose the matching reason and let the operator confirm.
  8. Review the tree regularly. Retire unused codes and split codes that hide too much.

A Starter Reason Code Tree

Use this as a starting point and adapt the second level to your process. The top level maps cleanly to availability, performance and planned time.

Category (level 1) Reason (level 2) Code OEE impact
Breakdown Mechanical failure BRK-MECH Availability
Breakdown Electrical or control fault BRK-ELEC Availability
Breakdown Tooling or fixture failure BRK-TOOL Availability
Changeover Product changeover CHG-PROD Availability
Changeover Tool change CHG-TOOL Availability
Changeover First-article approval CHG-FAI Availability
Waiting Waiting for material WAIT-MAT Availability
Waiting Waiting for operator WAIT-OPR Availability
Waiting Waiting for quality release WAIT-QA Availability
Process Jam or misfeed PRC-JAM Performance (if short)
Process Quality adjustment PRC-ADJ Availability
Planned Break or meeting PLN-BRK Planned time
Planned Planned maintenance PLN-PM Planned time
Planned No orders scheduled PLN-NOS Planned time (affects TEEP)
Other Other (comment required) OTH Availability

Fifteen codes under six categories fit on two screens. Some plants add a third level for breakdowns (which unit failed), but only once the second level is used consistently. Notice how planned categories are kept separate: they are excluded from OEE but matter for OEE vs TEEP comparisons.

Short stops below your micro-stop threshold should not ask for a reason at all. They are too frequent for manual entry and are better explained by machine fault codes, as shown in the packaging line monitoring use case.

Storing the Tree as Data

Keep the tree in a single JSON document and publish it as a retained MQTT message. Every operator terminal subscribes to it, so changing a code means updating one message instead of reinstalling apps.

{
  "version": 3,
  "min_prompt_seconds": 180,
  "categories": [
    {"id": "BRK", "label": "Breakdown", "loss": "availability", "reasons": [
      {"code": "BRK-MECH", "label": "Mechanical failure"},
      {"code": "BRK-ELEC", "label": "Electrical or control fault"},
      {"code": "BRK-TOOL", "label": "Tooling or fixture failure"}
    ]},
    {"id": "CHG", "label": "Changeover", "loss": "availability", "reasons": [
      {"code": "CHG-PROD", "label": "Product changeover"},
      {"code": "CHG-TOOL", "label": "Tool change"},
      {"code": "CHG-FAI", "label": "First-article approval"}
    ]},
    {"id": "WAIT", "label": "Waiting", "loss": "availability", "reasons": [
      {"code": "WAIT-MAT", "label": "Waiting for material"},
      {"code": "WAIT-OPR", "label": "Waiting for operator"},
      {"code": "WAIT-QA", "label": "Waiting for quality release"}
    ]},
    {"id": "PLN", "label": "Planned", "loss": "planned", "reasons": [
      {"code": "PLN-BRK", "label": "Break or meeting"},
      {"code": "PLN-PM", "label": "Planned maintenance"},
      {"code": "PLN-NOS", "label": "No orders scheduled"}
    ]},
    {"id": "OTH", "label": "Other", "loss": "availability", "reasons": [
      {"code": "OTH", "label": "Other", "comment_required": true}
    ]}
  ]
}

Publish it once (retained) to a plant-level topic. On the free public MQTT broker, use your own random prefix:

mosquitto_pub -h broker.connectmq.com -p 1883 -i reasons-admin-b3z5 -r -q 1 -t "connectmq/demo/b3z5/plant1/config/downtime-reasons" -f reasons.json

The Operator Input Flow

The flow that works best on the floor is prompt-driven:

  1. The machine sensor or PLC publishes STOPPED on the machine's state topic. Timing starts automatically. See machine downtime tracking for the sensor side.
  2. If the stop lasts longer than min_prompt_seconds, the terminal at the machine shows the category screen.
  3. The operator taps a category, then a reason. Two taps, a few seconds.
  4. The terminal publishes the reason to the machine's reason topic with the stop's start time, so the logger can attach it to the right event even if it arrives late.
  5. When the machine restarts, the open event closes. If no reason was entered, the supervisor sees it in an "unexplained" list at the end of the shift.

Here is the core of a browser-based terminal using MQTT.js over secure WebSocket. Rendering the buttons is left to your UI framework; the MQTT logic is what matters.

import mqtt from "mqtt";

const PREFIX = "connectmq/demo/b3z5/plant1";
const MACHINE = "stamping/press04";
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
  clientId: "terminal-press04-" + Math.random().toString(16).slice(2, 8),
  protocolVersion: 5,
});

let tree = null;
let stoppedAt = null;
let promptTimer = null;

client.on("connect", () => {
  client.subscribe(`${PREFIX}/config/downtime-reasons`, { qos: 1 });
  client.subscribe(`${PREFIX}/${MACHINE}/state`, { qos: 1 });
});

client.on("message", (topic, payload) => {
  const data = JSON.parse(payload.toString());
  if (topic.endsWith("/downtime-reasons")) {
    tree = data;
    return;
  }
  if (data.state === "STOPPED" && stoppedAt === null) {
    stoppedAt = Date.now();
    promptTimer = setTimeout(() => showCategories(tree.categories), tree.min_prompt_seconds * 1000);
  } else if (data.state === "RUNNING") {
    clearTimeout(promptTimer);
    stoppedAt = null;
  }
});

export function submitReason(code, comment = "") {
  const msg = { code, comment, stop_started: stoppedAt, entered: Date.now() };
  client.publish(`${PREFIX}/${MACHINE}/reason`, JSON.stringify(msg), { qos: 1 });
  hidePrompt();
}

function showCategories(categories) {
  // Render one large button per category, then the reasons of the chosen one,
  // and call submitReason(reason.code) when a reason is tapped.
  console.log("Prompt operator with", categories.map((c) => c.label));
}

function hidePrompt() {
  console.log("Prompt closed");
}

If you prefer low-code, the same flow is easy in Node-RED with Dashboard 2.0 buttons and an mqtt out node, configured against broker.connectmq.com on port 1883 for testing or 8883 with TLS in production.

Rolling Out and Maintaining the Codes

  • Pilot on one line for two weeks. Watch which codes are used, which are never used and what people write under "Other".
  • Train with real examples. Show operators five recent stops and ask them to code them together. Disagreements reveal ambiguous codes.
  • Report back. Display the top reasons at the line each week. Operators enter better data when they see it used.
  • Version the tree. Keep the version field and store it with each reason, so reports remain correct after codes change.
  • Translate labels, not codes. In multilingual plants, keep codes stable and add label fields per language, so reports aggregate correctly regardless of the terminal language.
  • Audit reason timing. Compare when the reason was entered with when the stop began. Reasons entered long after the machine restarted are less reliable and point to prompt or staffing issues.

Because the tree lives in one retained message on your ConnectMQ broker, publishing a new version updates every terminal at the next reconnect, and the portal's topic explorer shows exactly which version is live.

Hosting the Data Flow

During design, the public MQTT test broker at broker.connectmq.com is enough. For production, reasons and states belong on a private broker. ConnectMQ offers a hosted MQTT broker with per-client credentials and secure WebSocket on port 8084, so browser terminals connect directly without a backend. The portal's live topic explorer makes it easy to check that reasons arrive on the right topics. For how reason data fits into a broader plant program, see the smart manufacturing guide.

Get Started with ConnectMQ

Sign up free at app.connectmq.com, create a Serverless broker with no credit card, publish your reason tree and connect your first operator terminal today.

Frequently Asked Questions

How many downtime reason codes should we have?

Few enough that operators can choose in two taps: usually five to seven categories with three to five reasons each. Grow the list only when the "Other" comments show a recurring cause.

Should operators enter start and end times?

No. Let the machine detect stops automatically and ask the operator only for the reason. Manual timing is the main source of inaccurate downtime data.

What should happen to stops without a reason?

Show them as unexplained to the supervisor at shift end. A high unexplained share usually means the codes or the prompt timing need work.

Can machine alarms replace operator reasons?

Partly. Alarm codes explain many faults and micro-stops well, but waiting, changeovers and material issues still need a human answer.

Related articles

OEE vs TEEP vs OOE: Which Metric Should You Use?

OEE, OOE and TEEP share the same performance and quality factors but measure against different time bases. This comparison works through one example and lists the MQTT signals each metric needs.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.