LED manufacturing · Automation case study

From the SMT line
to the ledger: factory auto-pilot

An AI operations agent for an LED manufacturing company. It reads the production line, counts good vs. scrap units, posts finished-goods stock and component consumption into Zoho Inventory, auto-raises a supplier PO the moment a part drops below its reorder point, and WhatsApps the shift summary — all without a person re-typing a single number.

Run the live line demo Jump to the code
OPC-UA / Modbus / MQTT MES → Zoho Inventory Auto reorder POs Yield / OEE WhatsApp shift report
01 — In plain English

What it is, with zero jargon

An LED factory has machines that place tiny components on circuit boards (the "SMT line"), bake them, inspect them, and pack finished bulbs and tubes. Every shift, someone counts how many good units came out, how many were scrap, what raw materials were used, and types it all into the inventory and accounting system. This agent is the worker who does that counting and typing — instantly, accurately, every shift, and it shouts the moment a part is about to run out.

What the factory does

  • Runs the line as normal — the machines already keep counters
  • Or the supervisor sends a WhatsApp: /post Line-A 2026-06-07 Shift-1
  • Reads the summary: "4,820 good, 180 scrap, 96.4% yield — post?"
  • Replies "yes"

What the agent does

  • Reads the line counters (PLC / MES) — good, scrap, downtime
  • Works out yield and how much raw material was consumed (the BOM)
  • Adds finished bulbs to Zoho Inventory; subtracts the components used
  • Checks every component against its reorder point
  • If a part is low, drafts a purchase order to the supplier
  • Shows a dry-run, waits for "yes", then posts + WhatsApps the shift report

Same seatbelt as the ERP Smart Executive

It is the exact same safe pattern as the Excel + WhatsApp → ERP Smart Executive — just wired to the production line instead of a spreadsheet. Nothing is posted to inventory or sent to a supplier without a human-approved dry-run. Every post is logged. Errors are surfaced loudly, never hidden.

02 — Why it matters

The shift paperwork, gone

~20 sec
to post a shift
vs. ~45 min by hand
live
stock accuracy
every shift
0
stock-outs from
missed reorders
100%
posts logged
+ approved

Design targets for this build — your real numbers depend on line instrumentation and BOM accuracy. The demo runs the same logic in your browser so you can judge it yourself.

03 — How it works

Line to ledger, six steps

1

Sense the line

Read good/scrap counters and downtime straight from the PLC or MES — via OPC-UA, Modbus/TCP, an MQTT topic, or the MES REST API. No clipboard counting.

OPC-UAModbusMQTT
2

Build the batch

Roll the shift into a production batch: SKU, good qty, scrap qty, yield %, and the component consumption exploded from the Bill of Materials.

BOM explosionyield / OEE
3

Validate / QC

Flag anomalies before they hit the books: yield below the floor, AOI reject spikes, counts that don't reconcile, negative stock. Bad batches are held, not posted.

QC gates
4

Lookup & plan

Match every finished SKU and component in Zoho Inventory, then plan: stock-in finished goods, consume components, and reorder anything below its point.

idempotent
5

Dry-run → execute

Show the planned stock adjustments and draft POs, wait for "yes", then post one Zoho call per line and capture each record ID. Errors are loud; the batch aborts above a 20% failure rate.

human in the loop
6

Report

Write the audit log and WhatsApp the production WhatsApp group the shift summary: units, yield, stock posted, POs raised.

audit logWhatsApp
04 — See it live

Run a shift right here

Pick a line, press Start line and watch good/scrap units tick up with live yield. Then Post the shift to run the pipeline: build batch → QC → look up SKUs → dry-run the stock adjustments and auto-POs → approve → post to Zoho Inventory → WhatsApp the shift report. Everything runs in your browser; Live API mode calls your own Zoho + WhatsApp endpoints (your keys, never stored).

0
Good units
0
Scrap
Yield
0
Units / min
📥
SMT Place
0
🔥
Reflow
0
🔎
AOI / QC
0
📦
Pack
0
STEP 01
Build batch
Idle — start the line.
STEP 02
QC / validate
Waiting…
STEP 03
Lookup SKUs
Waiting…
STEP 04
Dry-run
Waiting…
STEP 05
Post Zoho
Waiting…
STEP 06
Report
Waiting…
Execution log
# shift log appears here…
WhatsApp · production group

The simulator injects a little random scrap each run, so occasionally the yield dips below the 95% floor and QC holds the batch — exactly what should happen on a real line.

05 — The technical document

Everything an engineer needs to build it

The full spec for the factory variant: the agent's system prompt, the connector configuration for line telemetry and Zoho, the WhatsApp Cloud API number, and the stand-up checklist.

A · System prompt paste into the Claude Project

The same skeleton as the ERP Smart Executive, retargeted at the production line.

# LED Factory Ops Agent
You are the LED Factory Ops Agent for [CLIENT_COMPANY_NAME]. Your job:
read the production line, build the shift's production batch, and post finished-
goods stock + component consumption to Zoho Inventory — raising supplier POs
when components fall below their reorder point.

## Tools
1. line     — MES / PLC telemetry (OPC-UA, Modbus, MQTT, or MES REST). Read-only.
2. zoho     — Zoho Inventory + Books MCP. ALL stock writes + PO creation.
3. whatsapp — WhatsApp Cloud API. Read /post commands, send shift summaries.
4. analysis tool — compute yield, OEE, BOM explosion. Never eyeball counts.

## Hard rules
1. Never invent a Zoho item/PO ID. Look up by SKU/vendor first.
2. Never post stock or raise a PO without a dry-run + explicit "yes"
   (unless auto_approve:true for the run).
3. One adjustment / PO = one tool call.
4. Counts are integers; yield = good/(good+scrap). Empty telemetry = null, not 0.
5. Idempotency: one batch_id per (line, date, shift). Re-running updates, never doubles.
6. QC gate: if yield < floor or counts don't reconcile, HOLD the batch and report.
7. Errors are loud: surface Zoho 4xx/5xx verbatim + WhatsApp. Retry once max.
8. Audit log: batch_log_{line}_{date}_{shift}.md (item, action, qty, Zoho ID, notes).

## Workflow
Sense -> Build batch -> QC/validate -> Lookup -> Dry-run -> Execute -> Report.

## WhatsApp inbound
Commands: /post [line] [date] [shift], /status [line], /hold [line], /help.
Free text EN/UR/AR/HI -> reply same language. Never post from chat alone — the
batch must come from line telemetry or an attached production sheet.

## Refuse
- Posting a held/failed-QC batch.
- Writing to items/vendors not in item_schema.md / vendor_allowlist.md.
- Sending WhatsApp to numbers not in whatsapp_allowlist.md.
- Bulk stock deletions (done manually in Zoho).

## Tone
Operational, terse, factual. Tables for >3 rows. No emojis, no preamble.
B · Connectors — line telemetry + Zoho MES · PLC · Zoho
1 · Line telemetry (read-only)

Pick whatever your line already speaks — most modern SMT lines speak at least one:

  • OPC-UA — the standard for PLC/SCADA. Read tag nodes for good/scrap counters (see code).
  • Modbus/TCP — read holding registers off older PLCs.
  • MQTT — subscribe to a broker topic the line publishes to (Industry 4.0 gateways).
  • MES REST API — if you run a MES (e.g. line-control software), poll its shift endpoint.

Wrap the chosen reader behind a tiny MCP server (or a FastAPI service) so the agent calls one tool: line.read_shift(line, date, shift).

2 · Zoho Inventory + Books

Same setup as the ERP build — native Zoho MCP at zoho.com/mcp (or Composio's Zoho Inventory toolkit). Enable: inventory.items.list/get, inventory.inventoryadjustments.create, inventory.purchaseorders.create, inventory.vendors.search. Add it as a custom Web connector named zoho; connect via OAuth.

3 · WhatsApp Cloud API

Identical to the ERP build — get a number on Meta (or use a Twilio sender). The full provisioning steps and the send call are documented on the ERP page → section C. The agent posts shift summaries to the production group.

Sanity check
  • "Read the last shift on Line A" returns good/scrap counts.
  • "List 5 items in Zoho Inventory" returns real SKUs.
  • "Send a WhatsApp test to {your number}" arrives.
C · Stand-up checklist order of operations
Phase 1 — Provisioning
  • Confirm how each line exposes counts (OPC-UA / Modbus / MQTT / MES)
  • Zoho Inventory live, items + BOMs + reorder points loaded
  • Vendor records + lead times in Zoho; vendor_allowlist.md
  • WhatsApp Cloud API number (or Twilio) + production group
Phase 2 — Connectors
  • Stand up the line reader (OPC-UA/Modbus/MQTT/MES) behind one tool
  • Add Zoho MCP + WhatsApp connectors; connect via OAuth
  • Define item_schema.md (SKU → BOM, reorder point)
Phase 3 — Project + test
  • Create the Project; paste the system prompt; upload the schema files
  • Dry-run one historical shift against a Zoho sandbox
  • Confirm stock-in, component consumption, a triggered PO, the audit log + WhatsApp
Phase 4 — Cutover
  • One supervised live shift post; then schedule the end-of-shift poller
  • Hand over the supervisor SOP; 1-week check-in
06 — The code

More wires, more code

Because a factory has more moving parts than a spreadsheet, here is the full kit: read the line over four different industrial protocols, explode the BOM, compute yield, post to Zoho Inventory, auto-raise reorder POs, and receive the line's events over a webhook. Pick the tab that matches your shop floor.

# line_read.py — read good/scrap counters from a PLC over OPC-UA
from asyncua import Client   # pip install asyncua
import asyncio

OPC_URL = "opc.tcp://10.20.0.5:4840"
TAGS = {
    "good":     "ns=2;s=Line_A.Packing.GoodCount",
    "scrap":    "ns=2;s=Line_A.AOI.RejectCount",
    "downtime": "ns=2;s=Line_A.Status.DowntimeMin",
}

async def read_shift(line="A"):
    async with Client(url=OPC_URL) as client:
        out = {}
        for name, node_id in TAGS.items():
            node = client.get_node(node_id)
            out[name] = await node.read_value()
        out["good"], out["scrap"] = int(out["good"]), int(out["scrap"])
        out["yield"] = round(out["good"] / max(1, out["good"] + out["scrap"]), 4)
        return out

if __name__ == "__main__":
    print(asyncio.run(read_shift()))
# modbus_read.py — older PLC over Modbus/TCP holding registers
from pymodbus.client import ModbusTcpClient   # pip install pymodbus

def read_modbus(host="10.20.0.7", unit=1):
    c = ModbusTcpClient(host); c.connect()
    # 40001-40002 = good (32-bit), 40003 = scrap
    rr = c.read_holding_registers(0, 3, slave=unit)
    good = (rr.registers[0] << 16) + rr.registers[1]
    scrap = rr.registers[2]; c.close()
    return {"good": good, "scrap": scrap,
            "yield": round(good / max(1, good + scrap), 4)}

# mqtt_read.py — Industry-4.0 gateway publishing to a broker
import json, paho.mqtt.client as mqtt   # pip install paho-mqtt
latest = {}

def on_message(client, _u, msg):
    latest.update(json.loads(msg.payload))   # {"good":..,"scrap":..}

def subscribe(host="10.20.0.9", topic="factory/lineA/shift"):
    cli = mqtt.Client(); cli.on_message = on_message
    cli.connect(host, 1883); cli.subscribe(topic); cli.loop_start()
    return latest   # read latest["good"] / latest["scrap"] any time
# batch.py — turn raw counts into a production batch + BOM consumption
import datetime as dt

YIELD_FLOOR = 0.95

# Bill of Materials: finished SKU -> {component: qty per unit}
BOM = {
  "LED-A19-9W":  {"LED-CHIP-2835": 12, "PCB-A19": 1, "DRIVER-IC": 1, "HOUSING-A19": 1},
  "LED-T8-18W":  {"LED-CHIP-2835": 96, "PCB-T8": 1, "DRIVER-IC": 1, "TUBE-T8": 1},
  "LED-PNL-36W": {"LED-CHIP-2835": 144, "PCB-PNL": 1, "DRIVER-36W": 1, "FRAME-PNL": 1},
}

def build_batch(sku, line, shift, telem):
    good, scrap = telem["good"], telem["scrap"]
    y = good / max(1, good + scrap)
    batch = {
      "batch_id": f"{line}-{dt.date.today()}-{shift}",
      "sku": sku, "good": good, "scrap": scrap, "yield": round(y, 4),
      "consume": {c: q * good for c, q in BOM[sku].items()},
      "hold": y < YIELD_FLOOR,            # QC gate
    }
    return batch

def validate(batch):
    errs = []
    if batch["hold"]: errs.append(f"yield {batch['yield']:.1%} below floor")
    if batch["good"] < 0 or batch["scrap"] < 0: errs.append("negative count")
    return errs
# zoho_post.py — stock-in finished goods, consume components, auto-PO
import os, requests
BASE = "https://www.zohoapis.com/inventory/v1"
ORG  = os.environ["ZOHO_ORG_ID"]
H    = {"Authorization": f"Zoho-oauthtoken {os.environ['ZOHO_OAUTH_TOKEN']}"}

def item(sku):
    r = requests.get(f"{BASE}/items", headers=H, params={"organization_id": ORG, "sku": sku})
    hits = r.json().get("items", []); return hits[0] if hits else None

def adjust(sku, qty, reason):
    """One stock adjustment = one call. +qty stock-in, -qty consume."""
    it = item(sku)
    body = {"reason": reason, "adjustment_type": "quantity",
            "date": __import__("datetime").date.today().isoformat(),
            "line_items": [{"item_id": it["item_id"], "quantity_adjusted": qty}]}
    r = requests.post(f"{BASE}/inventoryadjustments?organization_id={ORG}", headers=H, json=body)
    r.raise_for_status(); return r.json()["inventory_adjustment"]["inventory_adjustment_id"]

def reorder_check(sku):
    """Below reorder point? Draft a PO to the preferred vendor."""
    it = item(sku)
    if float(it["stock_on_hand"]) >= float(it.get("reorder_level", 0)): return None
    qty = int(it.get("reorder_level", 0)) * 2   # simple reorder qty
    po = {"vendor_id": it["vendor_id"], "line_items": [
            {"item_id": it["item_id"], "quantity": qty, "rate": float(it["purchase_rate"])}]}
    r = requests.post(f"{BASE}/purchaseorders?organization_id={ORG}", headers=H, json=po)
    r.raise_for_status(); return r.json()["purchaseorder"]["purchaseorder_number"]

def post_batch(batch):
    log = [("stockin", batch["sku"], batch["good"], adjust(batch["sku"], batch["good"], "Production"))]
    for comp, qty in batch["consume"].items():
        log.append(("consume", comp, -qty, adjust(comp, -qty, f"BOM {batch['batch_id']}")))
        po = reorder_check(comp)
        if po: log.append(("reorder", comp, qty, po))
    return log
# webhook.py — receive line events + WhatsApp /post commands (FastAPI)
from fastapi import FastAPI, Request
import os, requests, line_read, batch, zoho_post

app = FastAPI()

def whatsapp(to, body):
    requests.post(f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages",
        headers={"Authorization": f"Bearer {os.environ['WA_TOKEN']}"},
        json={"messaging_product": "whatsapp", "to": to,
              "type": "text", "text": {"body": body}})

@app.get("/webhook")                       # Meta verification handshake
def verify(req: Request):
    p = req.query_params
    return int(p["hub.challenge"]) if p.get("hub.verify_token") == os.environ["VERIFY"] else "no"

@app.post("/webhook")                      # inbound WhatsApp messages
async def inbound(req: Request):
    data = await req.json()
    msg = data["entry"][0]["changes"][0]["value"]["messages"][0]
    text, sender = msg["text"]["body"], msg["from"]
    if text.startswith("/post"):
        _, line, date, shift = text.split()
        telem = await line_read.read_shift(line)
        b = batch.build_batch(SKU_FOR[line], line, shift, telem)
        errs = batch.validate(b)
        if errs:
            whatsapp(sender, f"HELD {b['batch_id']}: {', '.join(errs)}"); return {"ok": True}
        # dry-run summary first — wait for "yes" before zoho_post.post_batch(b)
        whatsapp(sender, f"{b['good']} good, {b['scrap']} scrap, "
                         f"{b['yield']:.1%} yield. Reply YES to post.")
    return {"ok": True}
07 — From demo to real app

What's a demo here, and what's real

Already real today

  • The OPC-UA / Modbus / MQTT / MES readers above
  • Yield + BOM explosion + QC-gate logic
  • The exact Zoho Inventory adjustment + PO request shapes
  • The FastAPI webhook + WhatsApp dry-run gate

What I switch on for you

  • The line reader pointed at your real PLC/MES tags
  • Your Zoho Inventory connected (items, BOMs, reorder points, vendors)
  • Your WhatsApp Cloud API number + the production group
  • End-of-shift poller so each shift posts itself

"Ask for API and it'll do it"

The line demo runs simulated so anyone can try it. Flip to Live API, paste your Zoho + WhatsApp endpoints, and it posts for real from your browser — nothing stored. For the full plant build (real PLC tags, scheduled posting, supplier PO automation) it's the cutover in the checklist — message me and we'll wire it to your floor.

Put your factory on auto-pilot

Line in, ledger out — finished goods stocked, components consumed, POs raised, every post logged and approved. Same safe pattern as the ERP Smart Executive, wired to your shop floor.

WhatsApp Aziz See the Excel + WhatsApp ERP version →