4.1.1 Start Production Run

1. Overview
The Start Production Run page is the operator-facing interface for initiating a production run on a selected production line. It supports two distinct workflows:
Scheduled Production Run — selecting from a list of pre-planned Work Orders.
Quick Start (Ad-hoc Production Run) — creating an ad-hoc Work Order + Schedule + Run using QR/barcode scanning or manual input, without modifying the scheduled list.
All production runs (scheduled or ad-hoc) are started only after operator login and only when the operator confirms the Start Run pop-up.
This page does not allow operators to modify planned production data.
2. Page Structure and Workflow
2.1 Line Selection
Operators select the target production line (AAA, BBB, CCC, etc.).

This becomes the line_id used for queries and run creation.
Line dropdown (Line Name selector)
Schema reference
Table: line
id (bigserial)
name (text)
short_name (text)
disable (boolean)
SQL – mes_line_list
SELECT id, name, short_name FROM line WHERE disable = FALSE ORDER BY name;
Use this to populate the Line Name dropdown (AAA, BBB, etc., in real data).
2.2 Scheduled Work Orders Table (Left Panel)
The table displays only scheduled/planned Work Orders created by planners.

Columns shown (UI):
Work Order
Product Code
Remaining Qty
Scheduled Start
Scheduled Finish
The operator can click a row to load full details into the right-hand panel.
Why Remaining Qty only?
This minimizes cognitive load and shows what matters:
How much is left to produce.
Back-end Data Logic:
scheduled_qty from schedule.quantity
produced_qty = SUM(run.totalcount) where run.closed = true
remaining_qty = scheduled_qty – produced_qty
Table Data Source: Named Query
mes_scheduledWorkOrdersByLine(:line_id)
SQL - Scheduled Work Orders table (left panel) + detail panel data
produced_qty = SUM(run.total_count) for all closed runs on that schedule.
remaining_qty = schedule.quantity – produced_qty.
Schema reference
schedule
id (bigserial)
line_id (int8)
work_order_id (int8)
schedule_type (text)
note (text)
schedule_start_datetime (timestamp)
schedule_finish_datetime (timestamp)
quantity (integer)
entered_by (text)
timestamp (timestamp)
actual_start_datetime (timestamp)
actual_finish_datetime (timestamp)
actual_quantity (integer)
run_start_datetime (timestamp)
work_order
id (bigserial)
product_code_id (int8)
work_order (text)
quantity (integer)
closed (boolean)
hide (boolean)
timestamp (timestamp)
run
id (bigserial)
schedule_id (int8)
total_count (integer)
closed (boolean)
… (other columns not directly used here)
product_code
id (bigserial)
product_code (text)
description (text)
disable (boolean)
product_code_line
id (bigserial)
line_id (int8)
product_code_id (int8)
idealcycletime (real)
enable (boolean)
2.1 Named Query – mes_scheduled_workorders_by_line
Parameters
:line_id (int8)
SQL
SELECT s.id AS schedule_id, wo.id AS work_order_id, wo.work_order AS work_order_no, pc.product_code AS product_code, pc.description AS product_description, pcl.idealcycletime AS ideal_cycle_time_s, s.quantity AS scheduled_qty, COALESCE(SUM(r.total_count), 0) AS produced_qty, s.quantity - COALESCE(SUM(r.total_count), 0) AS remaining_qty, s.schedule_start_datetime, s.schedule_finish_datetime, s.note AS planner_note, CASE WHEN s.actual_finish_datetime IS NOT NULL THEN 'Completed' WHEN s.actual_start_datetime IS NOT NULL THEN 'In Progress' ELSE 'Planned' END AS status FROM schedule s JOIN work_order wo ON s.work_order_id = wo.id JOIN product_code pc ON wo.product_code_id = pc.id LEFT JOIN product_code_line pcl ON pcl.product_code_id = pc.id AND pcl.line_id = s.line_id LEFT JOIN run r ON r.schedule_id = s.id AND r.closed = TRUE WHERE s.line_id = :line_id -- Optional: only show schedules that still have something to produce -- AND s.quantity - COALESCE(SUM(r.total_count), 0) > 0 GROUP BY s.id, wo.id, wo.work_order, pc.product_code, pc.description, pcl.idealcycletime, s.quantity, s.schedule_start_datetime, s.schedule_finish_datetime, s.note, s.actual_start_datetime, s.actual_finish_datetime ORDER BY s.schedule_start_datetime;
Usage
Scheduled Work Orders table (left)
Show:
work_order_no
product_code
remaining_qty
schedule_start_datetime
schedule_finish_datetime
Work Order Information panel (right)
Bind details from selected row:
Work Order ID → work_order_no
Scheduled Quantity → scheduled_qty
Produced Quantity → produced_qty
Remaining Quantity → remaining_qty
Start/Finish Times → schedule_start_datetime, schedule_finish_datetime
Product Code → product_code
Ideal Cycle Time → ideal_cycle_time_s
Product Description → product_description
Planner Note → planner_note
2.3 Work Order Information Panel (Right Panel)

When a row is selected OR a Quick Start WO is in progress, the panel shows:
Work Order ID
Scheduled Qty
Produced Qty
Remaining Qty
Scheduled Start / Finish time
Product Code
Ideal Cycle Time (from product_code_line)
Product Description
Planner Note
This panel represents the actual Work Order that will be started.
3. Operator Login (Required for All Production Actions)

The Operator Login modal requires:
Operator ID
Password
Login unlocks:

Quick Start button
Start Work Order button
Without login:
Both buttons are disabled
Status message reads: Operator not logged in…
Logout reverses these states.
4. Quick Start (Ad-hoc Work Order Creation)

Important: No database operations occur during Quick Start form submission.
This is by design.
Why?
To prevent unapproved data from appearing in the scheduled/planned list.
Quick Start Workflow
Operator clicks Quick Start (only when logged in).
Modal opens with:
QR/Barcode scan field
Work Order ID
Product Code
Quantity
Start/Finish Time
Planner Note
Operator scans or fills data.
Click Apply to Work Order:
A temporary QuickStartWO object is created in memory.
The Work Order Information panel displays this data.
No new row is added to the Scheduled table.
When the operator then clicks Start Work Order, the backend logic detects whether the WO came from:
A scheduled record, or
A Quick Start form
…and executes the appropriate SQL transaction.
Product list for Quick Start (Product Code dropdown)
Schema reference
product_code
product_code_line
We want only enabled product codes for the selected line.
Named Query – mes_product_master_by_line
Parameters
:line_id (int8)
SQL
SELECT pc.id AS product_code_id, pc.product_code, pc.description, pcl.idealcycletime AS ideal_cycle_time_s FROM product_code pc LEFT JOIN product_code_line pcl ON pcl.product_code_id = pc.id AND pcl.line_id = :line_id WHERE pc.disable = FALSE AND (pcl.enable IS NULL OR pcl.enable = TRUE) ORDER BY pc.product_code;
Use this to populate the Product Code select in the Quick Start modal, and to fill Ideal Cycle Time + description in the detail panel.
Start Run – Quick Start (Ad-hoc WO + Schedule + Run)
This is used when the Quick Start form is filled and Start Work Order is pressed. Here we:
Create a work_order if it doesn’t exist.
Create a schedule for that line + WO.
Create a run for that schedule.
Link schedule.run_id.
Parameters (from Quick Start panel & tags)
:line_id (int8)
:work_order_no (text)
:product_code_id (int8)
:target_qty (integer) → goes into schedule.quantity and optionally work_order.quantity
:schedule_start (timestamp) – from Quick Start Start Time
:schedule_finish (timestamp) – from Quick Start Finish Time
:note (text)
:operator_id (text)
:start_infeed (integer)
:start_outfeed (integer)
:start_waste (integer)
Named Query – mes_quick_start_run
WITH existing_wo AS ( SELECT id FROM work_order WHERE work_order = :work_order_no ), ins_wo AS ( INSERT INTO work_order ( product_code_id, work_order, quantity, closed, hide, timestamp ) SELECT :product_code_id, :work_order_no, :target_qty, FALSE, FALSE, NOW() WHERE NOT EXISTS (SELECT 1 FROM existing_wo) RETURNING id ), wo AS ( SELECT id FROM existing_wo UNION ALL SELECT id FROM ins_wo ), ins_schedule AS ( INSERT INTO schedule ( line_id, run_id, work_order_id, schedule_type, note, schedule_start_datetime, schedule_finish_datetime, quantity, entered_by, timestamp, actual_start_datetime, actual_finish_datetime, actual_quantity, run_start_datetime ) SELECT :line_id, NULL::bigint, wo.id, 'ADHOC', :note, COALESCE(:schedule_start, NOW()), :schedule_finish, :target_qty, :operator_id, NOW(), NOW(), -- actual_start_datetime NULL, -- actual_finish_datetime 0, -- actual_quantity NOW() -- run_start_datetime FROM wo RETURNING id ), ins_run AS ( INSERT INTO run ( schedule_id, run_start_datetime, start_infeed, current_infeed, start_outfeed, current_outfeed, start_waste, current_waste, total_count, waste_count, good_count, availability, performance, quality, oee, setup_start_datetime, setup_end_datetime, runtime, unplanned_downtime, planned_downtime, total_time, timestamp, closed, estimated_finish_time ) SELECT s.id, NOW(), :start_infeed, :start_infeed, :start_outfeed, :start_outfeed, :start_waste, :start_waste, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NOW(), FALSE, NULL FROM ins_schedule s RETURNING id, schedule_id ) UPDATE schedule s SET run_id = r.id FROM ins_run r WHERE s.id = r.schedule_id RETURNING r.id AS run_id, s.id AS schedule_id;
This query returns run_id and schedule_id so you can:
Navigate to the Run Monitoring / Setup view with run_id.
Optionally refresh the Scheduled Work Orders list if you later want ad-hoc work to appear there (currently, in your design, it doesn’t).
5. Start Work Order Logic

When the operator presses Start Work Order, the system evaluates:
if quickStartWO exists → Start ad-hoc run else → Start scheduled run
The confirmation popup shows:
Work Order ID
Line Name
Operator ID
When confirmed, the backend executes the start run transaction.
Before executing the start run transaction (Quick Start or Existing Schedule), the Start Work Order button script performs a pre-start validation on the line:
Read the tag [Line]/OEE/RunID.
A new Production Run can only be started when RunID is -1 or NULL, meaning there is no active run on this line.
If RunID has any other value, the system shows an error message (e.g. “Line already has an active run. Please stop or close the current run first.”) and does not call the database start run Named Query.
This check keeps the tag layer and database layer consistent and prevents multiple active runs on the same line.
“The following sections define the UI script, database logic, and MES tag initialization required to correctly start any Production Run.”
5.1 Perspective Start Work Order Button – Pseudo-code
The Start Work Order button in the Start Production Run view implements the following logic in its onActionPerformed script:
Operator login check
Read the current operator ID from the session (e.g. session.custom.operatorId).
If no operator is logged in, show an error message and abort.
Resolve line context
Read the selected line_id from the view.
Use a helper function (e.g. project.mes_start_run.getLinePath(lineId)) to resolve the UDT tag path for the line, such as
[mes]Enterprise/Site/Area/Line 1/Line.
Pre-start validation from tag
Read [Line]/OEE/RunID.
Only allow a new Production Run to start if RunID is -1 or NULL.
If another value is present, display an error message (e.g. “Line already has an active Run. Please stop or close the current Run first.”) and do not call any Named Query.
Read live counters as start snapshot
Read the current counter tags from the Dispatch UDT:
[Line]/Dispatch/OEE Infeed/Count
[Line]/Dispatch/OEE Outfeed/Count
[Line]/Dispatch/OEE Waste/Count
Map these values into the :start_infeed, :start_outfeed, :start_waste parameters for the start-run Named Queries.
Branch: Quick Start vs Existing Scheduled Work Order
If a quickStartWO object exists in the view (created by the Quick Start modal), call the mes_quick_start_run Named Query with:
line_id, work_order_no, product_code_id, target_qty, schedule_start, schedule_finish, note, operator_id, and the three start_* values.
Otherwise, ensure that a row is selected in the Scheduled Work Orders table and call mes_start_run_existing_schedule with:
schedule_id, operator_id, and the three start_* values.
Both variants return at least run_id (and optionally schedule_id).
Initialise MES tags for the new run
Call a project script helper, e.g.
project.mes_start_run.initRunTags(linePath, lineId, runId, scheduleId, workOrderNo, targetQty, idealCycleTimeS, operatorId).
This helper writes to the Line and OEE UDTs to set:
OEE/RunID, OEE/Schedule ID, OEE/WorkOrder, OEE/Quantity, OEE/Target Count
Reset counts and downtime fields to zero
Set OEE/Standard Rate based on ideal_cycle_time_s
Set OEE/Start Time, Line/Start Time, Line/Run Enabled, Line/Collect Data
Set the RunID and Enable tags inside the Dispatch UDT (Infeed, Outfeed, Waste).
Navigation
Navigate to the Run Monitoring / Setup view, passing lineId and runId as view parameters.
The implementation details of getLinePath and initRunTags are encapsulated in a project script library (mes_start_run) so that mes_core remains unchanged and reusable.
Start Run – Existing Scheduled Work Order
This is the case where the operator selects a row in the Scheduled Work Orders table and presses Start Work Order.
Concept
Ensure schedule.actual_start_datetime is set (first time only).
Create a new run row.
Optionally set schedule.run_id & run_start_datetime.
Use tag counters as starting values.
Parameters (from Perspective)
:schedule_id (int8) – from selected row
:operator_id (text) – from Operator Login
:start_infeed (integer)
:start_outfeed (integer)
:start_waste (integer)
Named Query – mes_start_run_existing_schedule
(Shown as a transactional block – implementation in Ignition depends on how you handle multi-statement queries in PostgreSQL; you can also put this logic in a stored procedure.)
-- 1) Set actual_start_datetime if this is the first time we start this schedule UPDATE schedule SET actual_start_datetime = COALESCE(actual_start_datetime, NOW()) WHERE id = :schedule_id; -- 2) Insert new run INSERT INTO run ( schedule_id, run_start_datetime, start_infeed, current_infeed, start_outfeed, current_outfeed, start_waste, current_waste, total_count, waste_count, good_count, availability, performance, quality, oee, setup_start_datetime, setup_end_datetime, runtime, unplanned_downtime, planned_downtime, total_time, timestamp, closed, estimated_finish_time ) VALUES ( :schedule_id, NOW(), :start_infeed, :start_infeed, :start_outfeed, :start_outfeed, :start_waste, :start_waste, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NOW(), FALSE, NULL ) RETURNING id;
If you want to link the latest run back to schedule.run_id:
WITH new_run AS ( INSERT INTO run ( ... ) -- same as above VALUES ( ... ) RETURNING id, schedule_id ) UPDATE schedule s SET run_id = r.id, run_start_datetime = COALESCE(s.run_start_datetime, NOW()) FROM new_run r WHERE s.id = r.schedule_id RETURNING r.id AS run_id;
Machine state is always driven by the PLC.
The PLC (or other field device) writes to the line state tag, e.g. [Line]/State/Code or an equivalent state tag in the Line UDT.
MES Starter does not force the machine into Running. It only:
Knows which run_id is active for the line, and
Optionally sets a permissive such as [Line]/Control/RunEnable that the PLC can use in its own logic.
A separate state-history script (outside of this Start Production Run page) subscribes to the state tag and writes changes into the state_history table, which is then used for Availability and Downtime analytics.
Therefore, the Start Work Order logic:
Creates the new run in the database.
Initialises MES tags (RunID, ScheduleID, etc.).
Optionally enables a PLC permissive (Run Enable).
Does not write “Running” or any other machine state code. The state transition to Running must be generated by the machine/PLC program.
1) Perspective Start Work Order button – pseudo-code
This is the script on the Confirm Start / Start Work Order button (onActionPerformed).
# PSEUDOCODE – Perspective Start Work Order button # 1) Guard: operator must be logged in operatorId = session.custom.operatorId # example – depends on your login wiring if not operatorId: system.perspective.sendMessage( "mes_error", {"message": "Operator not logged in."}, scope="session" ) return # 2) Resolve line context lineId = self.view.custom.selectedLineId # bound from Line dropdown. Adjust this to match your actual binding. linePath = project.mes_start_run.getLinePath(lineId) # e.g. returns "[mes]Enterprise/Site/Area/Line 1/Line" # 3) Pre-start validation – ensure no active Run on this line runIdPath = linePath + "/OEE/RunID" currentRunId = system.tag.readBlocking([runIdPath])[0].value if currentRunId is not None and currentRunId != -1: system.perspective.sendMessage( "mes_error", {"message": "Line already has an active Run. Please stop or close the current Run first."}, scope="session" ) return # 4) Read live counters from Dispatch UDT as start_* snapshot tagPaths = [ linePath + "/Dispatch/OEE Infeed/Count", linePath + "/Dispatch/OEE Outfeed/Count", linePath + "/Dispatch/OEE Waste/Count", ] infeed, outfeed, waste = [q.value for q in system.tag.readBlocking(tagPaths)] startInfeed = infeed or 0 startOutfeed = outfeed or 0 startWaste = waste or 0 # 5) Decide path: Quick Start vs Scheduled quickStartWO = self.view.custom.quickStartWO # None if not using Quick Start if quickStartWO is not None: # --- Quick Start path --- params = { "line_id": lineId, "work_order_no": quickStartWO["work_order_no"], "product_code_id": quickStartWO["product_code_id"], "target_qty": quickStartWO["target_qty"], "schedule_start": quickStartWO["schedule_start"], # can be null → COALESCE in SQL "schedule_finish": quickStartWO["schedule_finish"], "note": quickStartWO["planner_note"], "operator_id": operatorId, "start_infeed": startInfeed, "start_outfeed": startOutfeed, "start_waste": startWaste, } resultDS = system.db.runNamedQuery("mes_quick_start_run", params) row = resultDS[0] runId = row["run_id"] scheduleId = row["schedule_id"] workOrderNo = quickStartWO["work_order_no"] targetQty = quickStartWO["target_qty"] idealCycle = quickStartWO["ideal_cycle_time_s"] else: # --- Existing Scheduled Work Order path --- selectedRow = self.view.custom.selectedScheduleRow # from table selection if selectedRow is None: system.perspective.sendMessage( "mes_error", {"message": "Please select a Scheduled Work Order."}, scope="session" ) return scheduleId = selectedRow["schedule_id"] workOrderNo = selectedRow["work_order_no"] targetQty = selectedRow["scheduled_qty"] idealCycle = selectedRow["ideal_cycle_time_s"] params = { "schedule_id": scheduleId, "operator_id": operatorId, "start_infeed": startInfeed, "start_outfeed": startOutfeed, "start_waste": startWaste, } # this Named Query returns run_id (and optionally schedule_id, depending on variant) resultDS = system.db.runNamedQuery("mes_start_run_existing_schedule", params) row = resultDS[0] runId = row["run_id"] # 6) Initialise MES tags for this line/run project.mes_start_run.initRunTags( linePath=linePath, lineId=lineId, runId=runId, scheduleId=scheduleId, workOrderNo=workOrderNo, targetQty=targetQty, idealCycleTimeS=idealCycle, operatorId=operatorId ) # 7) Navigate to Run Monitoring / Setup view system.perspective.navigate( view="MES/RunMonitoring", params={"lineId": lineId, "runId": runId} )
2) Project script: mes_start_run.initRunTags
You keep mes_core untouched and add a small helper in a project script library, e.g. project.mes_start_run.
# project script: mes_start_run.py def getLinePath(lineId): """ Resolve a numeric lineId to its UDT tag path. Implementation is project-specific (could be a mapping table or a naming convention). """ # EXAMPLE ONLY – replace with your real mapping return "[mes]Enterprise/Site/Area/Line %d/Line" % lineId def initRunTags(linePath, lineId, runId, scheduleId, workOrderNo, targetQty, idealCycleTimeS, operatorId): """ Initialise the Line / OEE / Dispatch tags for a newly started Run. """ import system now = system.date.now() # 1) Core OEE context oeePaths = [ linePath + "/OEE/RunID", linePath + "/OEE/Schedule ID", linePath + "/OEE/WorkOrder", linePath + "/OEE/Quantity", # or Target Count, depending on final UDT linePath + "/OEE/Target Count", linePath + "/OEE/Good Count", linePath + "/OEE/Bad Count", linePath + "/OEE/Total Count", linePath + "/OEE/Planned Downtime", linePath + "/OEE/Unplanned Downtime", linePath + "/OEE/Run Time", linePath + "/OEE/Total Time", linePath + "/OEE/Standard Rate", linePath + "/OEE/OEE Availability", linePath + "/OEE/OEE Performance", linePath + "/OEE/OEE Quality", linePath + "/OEE/OEE", linePath + "/OEE/Start Time", ] # simple conversion: parts / minute from ideal cycle time in seconds if idealCycleTimeS and idealCycleTimeS > 0: standardRate = 60.0 / float(idealCycleTimeS) else: standardRate = 0 oeeValues = [ runId, scheduleId, workOrderNo, targetQty, targetQty, 0, 0, 0, 0, 0, 0, 0, standardRate, 0, 0, 0, 0, now, ] system.tag.writeBlocking(oeePaths, oeeValues) # 2) Line-level control / flags linePaths = [ linePath + "/Start Time", linePath + "/Run Enabled", linePath + "/Collect Data", ] lineValues = [now, True, True] system.tag.writeBlocking(linePaths, lineValues) # 3) Dispatch UDT context (RunID for each counter family) dispatchPaths = [ linePath + "/Dispatch/RunID", linePath + "/Dispatch/OEE Infeed/RunID", linePath + "/Dispatch/OEE Outfeed/RunID", linePath + "/Dispatch/OEE Waste/RunID", linePath + "/Dispatch/Enable", ] dispatchValues = [runId, runId, runId, runId, True] system.tag.writeBlocking(dispatchPaths, dispatchValues)
This uses your existing mes_core UDT structure (Line / OEE / Dispatch) from the Bootcamp JSON.