v1.0 · 2026
What is the ESE?

Finite-capacity scheduling that turns production demand into locked machine assignments

The Enterprise Scheduling Engine (ESE) reads every open production indent, groups same-item jobs into efficient batches, assigns each job to the best available machine within its shift windows, respects mould changeover constraints, and writes a committed schedule to TRESOURCEPLAN — all in a single automated run. Planners go from a pile of work orders to a precise Gantt in seconds.

10
Engine Steps
6
Lifecycle States
4
Decision Outcomes
∞
Horizon Days
2
DB Engines

System Data Flow

Planner UI
Gantt / OEE
→
POST TriggerRun
/MM/Scheduling
→
MM Microservice
.NET 9 · FastEndpoints
→
ESE Engine
10-step pipeline
→
TRESOURCEPLAN
SQL Server
→
Shop Floor
+ Gantt + OEE
TINDENT / TNESTINGPLAN
Open demand sources
→
Candidate Generation
Machines + Moulds
→
Decision Log
TSCHEDULINGRUNDETAILLOG
→
FirmPlan → Release
→ TINDENTRESOURCE

MRP vs ESE — What's the difference?

ConcernTraditional MRPESE (Finite-Capacity)
Capacity modelInfinite — assumes machines are always freeFinite — shift windows, breakdowns, booked slots subtracted per machine
OutputSuggested order / work-order datesExact machine assignment, start time, and end time per job
BatchingEach order processed independentlySame-item jobs merged up to batch-rule max, reducing setup overhead
Mould/toolingNot trackedCompatible moulds resolved per machine; changeover added as a 30-min mount task
Parallel machinesNot handledSPLITS ≥ 2 finds a simultaneous slot across N machines with distinct moulds
Decision auditNoneEvery EU logs Decision, FailureReason, CandidatesJSON, score, and slot wait
Reschedule safetyOverwrites all datesISLOCKED=1 rows (FirmPlan / Released / InProgress) are never deleted

Key Concepts — click to expand

⬡ Execution Unit (EU)
📄 Resource Plan
🔗 CombineId
📅 Horizon
📊 RCCP Check
🔧 Mount Task
Execution Unit (EU) — the atomic unit of scheduling. Each EU maps one indent detail to one routing step. Multiple EUs may be merged into a batch group before scheduling when the same item, SKU, work center, and process appear in multiple indents and a batch rule exists. The EU carries duration, deadline, earliest start, machine constraints, and demand type.
Resource Plan (TRESOURCEPLAN) — the main output table. One row per (EU × machine × segment). Each row records machine ID, work center, process, planned quantity, scheduled start/end, ISLOCKED status, and PLANSTATUS. The Gantt reads from this table; so does the shop floor dispatch screen.
CombineId (RESOURCEPLANCOMBINEID) — groups rows from the same EU across multiple machines or time segments. For interruptible jobs (split across shift breaks), each segment gets the same CombineId. For parallel-machine jobs (SPLITS ≥ 2), each machine's row shares the same CombineId. The Gantt uses CombineId to draw a single job bar across segments.
Horizon (HorizonFrom / HorizonTo) — the date range the engine plans within. Indents with EXPECTEDFIRSTDELIVERYDATE inside the horizon are included. Locked rows from prior runs that fall inside the horizon are pre-booked as constraints — the engine plans new demand around them.
RCCP Check (Rough-Cut Capacity Planning) — a pre-scheduling warning step. Total required minutes per work center is compared against shift capacity × machine count. If any work center exceeds 90%, an overload warning is added to the run log (RCCPJSON). The engine never blocks scheduling based on RCCP — it is informational only, alerting planners to add capacity or defer demand.
Mount Task (Mould Changeover) — when the best-fit machine needs a different mould than its currently-mounted one, the engine inserts a 30-minute preparation window before the job slot and writes a TTASK row (TASKNATURE=2). This task is visible in maintenance screens and is not counted as production time.
How the Engine Works

Six phases, fully automated — from demand stream to committed plan

Click any step for details on what it does, which tables it touches, and what can go wrong.

1
Load Demand
Stream open indents + nesting plans
2
Batch Grouping
Merge same-item jobs
3
Topological Sort
Respect job dependencies
4
RCCP Check
Capacity warning (non-blocking)
5
Slot Finding & Scoring
Find best machine + time window
6
Persist Results
Bulk-write to TRESOURCEPLAN

Candidate Scoring Formula

When multiple machines can take a job, each is scored 0–1. The highest scorer wins.

Urgency (deadline proximity)40 %
Machine utilisation (prefer less-busy)30 %
Changeover avoidance (mould already mounted)20 %
Indent priority10 %

Special Execution Paths

Parallel Machines (SPLITS ≥ 2)
Some casting steps require N machines running simultaneously with the same job split across them. The engine finds a time window where all N machines are free at once and assigns each a distinct mould. Decision log records all N machines; each gets its own TRESOURCEPLAN row under a shared CombineId.
Subcontract Steps
Routing steps marked Subcontract (ExecutionMode=1) do not consume machine time. Duration = SubcontractLeadTimeDays × 480 min. No machine candidate loop runs. The decision log records Decision=1 and no machine ID.
Monitor-Only Steps
Quality or inspection steps (LocationType=5) are logged with Decision=2 but produce no TRESOURCEPLAN row. They are visible in the decision log for audit purposes without consuming any capacity.
Plan Lifecycle

Six states from engine output to shopfloor completion

Click any state to see its operational meaning and the transition that enters it.

State Machine — click a state to explore

Status 0
Planned
ISLOCKED = 0
FirmPlan
Status 1
Firm Plan
ISLOCKED = 1
ReleasePlan
Status 2
Released
ISLOCKED = 1
StartWorkOrder
Status 3
In Progress
ISLOCKED = 1
StopWorkOrder
Status 4
Stopped
ISLOCKED = 0
Complete
Status 5
Completed
ISLOCKED = 0

ISLOCKED Behaviour Summary

PLANSTATUSISLOCKEDEngine treats this row as…Can be deleted by reschedule?
0 Planned0Deletable — overwrites on next run✅ Yes
1 FirmPlan1Pre-booked constraint — engine avoids this slot❌ No
2 Released1Pre-booked constraint + TINDENTRESOURCE written❌ No
3 InProgress1Pre-booked constraint + actual start recorded❌ No
4 Stopped0 or 1If ReleaseResource=true → 0 (slot freed). Otherwise stays 1.Only if ISLOCKED=0
5 Completed0Historical record onlyN/A — keep for audit

Lifecycle Endpoint Reference

EndpointMethodRouteKey Effect
FirmPlan POST /MM/Scheduling/FirmPlan Sets PLANSTATUS=1, ISLOCKED=1. Scope: by ResourcePlanIds, SchedulingRunId, or WorkCenterId+date range.
ReleasePlan POST /MM/Scheduling/ReleasePlan Sets PLANSTATUS=2. Transactionally: DELETE stale TINDENTRESOURCE → UPDATE PLANSTATUS → INSERT TINDENTRESOURCE. Shop floor can now see the assignment.
StartWorkOrder PUT /MM/Scheduling/StartWorkOrder Sets PLANSTATUS=3, ACTUALSTARTDATE=now. Machine is confirmed producing.
StopWorkOrder PUT /MM/Scheduling/StopWorkOrder Sets PLANSTATUS=4, ACTUALENDDATE, STOPREASONID, STOPREMARKS. If ReleaseResource=true: ISLOCKED=0 (slot freed). Optionally creates TCALL (breakdown) or TTASK (maintenance) to block machine in next run.

TINDENTRESOURCE — How ReleasePlan Dispatches to Shop Floor

Step 1 — Delete StaleDELETE TINDENTRESOURCE for affected IndentDetailIds (guards against re-release after plan change)
→
Step 2 — Update StatusUPDATE TRESOURCEPLAN SET PLANSTATUS=2 for released rows
→
Step 3 — Insert FreshINSERT TINDENTRESOURCE from TRESOURCEPLAN: IndentDetailId, WorkCenterId, MachineId, MachineTypeId, Duration
All three steps run inside a single database transaction. If any step fails, the entire operation rolls back — no half-written dispatch records.
Implementation Status

What's built, what's fixed, and what's pending

Bug Fixes Applied This Session

SPLITS > 1 Race Condition
Fixed
SchedulingEngine.cs
When a routing step required N parallel machines, the engine was selecting the top-N candidates without checking if they shared the same mould/pattern. Two machines ended up with the same PatternId in TRESOURCEPLAN. Fixed with a HashSet<int> distinct-pattern guard during topN selection — each machine in a parallel group must claim a different physical mould.
Wrong Column in GET_LAST_MOUNTED_PATTERN
Fixed
CandidateQB.cs
The SQL was querying the maintenance master tables (MKEYCOMPONENT / MKEYCOMPONENTDETAIL) and returning KEYCOMPONENTDETAILID aliased as PatternId — a completely different ID space than what the compatibility query returns. Fixed to query TRESOURCEPLAN (actual committed production plans) and return the real PATTERNID, ordered by SCHEDULEDSTARTDATE DESC.
Lazy-Load Never Called
Fixed
CandidateGenerator.cs
GetLastMountedPatternAsync existed in the DAL interface but was never invoked — the scheduling context always started with an empty LastMountedPatternId dictionary. The initial physical mount state was never loaded, so cross-machine conflict detection for prior-run moulds was silently bypassed. Fixed with a proper lazy-load pattern: DB is hit once per machine per run; subsequent lookups use the in-memory cache.

Plan Lifecycle — Implementation Complete

DAL Layer
Done
SchedulingRunDAL.cs · ISchedulingRunDAL.cs
FirmPlanAsync (by IDs, RunId, WorkCenter), ReleasePlanAsync (transactional DELETE+UPDATE+INSERT), StartWorkOrderAsync, StopWorkOrderAsync, GetLockedFutureSlotsAsync — all 29 methods implemented.
BLL Layer
Done
SchedulingRunBLL.cs · ISchedulingRunBLL.cs
Full validation, event log publishing, GB5Trace instrumentation, and TCALL/TTASK creation for machine blocking on StopWorkOrder. All BLL methods return i18n resource strings.
SL Layer (Endpoints)
Done
MMSL/EndPoints/Scheduling/
FirmPlan.cs, ReleasePlan.cs, StartWorkOrder.cs, StopWorkOrder.cs — all 4 FastEndpoints present and wired to BLL.

Database Migration Checklist

Action required: Run ESE_006_PlanLifecycle_Schema.sql on the dev database before testing FirmPlan, ReleasePlan, StartWorkOrder, or StopWorkOrder endpoints. Script location: MM/MMDAL/Script/ESE_006_PlanLifecycle_Schema.sql

Technology Stack

ConcernTechnology
Runtime.NET 9 · C# 13
HTTP FrameworkFastEndpoints 6.x — no MVC controllers
Data AccessDapper 2.x — raw parameterized SQL, no EF Core
Primary DBSQL Server (Microsoft.Data.SqlClient 6.x)
Secondary DBPostgreSQL (Npgsql 8.x) — runtime-switchable via LoginDTO
MessagingDapr pub/sub — event log publishing after every write
CachingHybridCache + Redis — transactional scheduling data: NOT_REQUIRED
ObservabilityOpenTelemetry → Zipkin/Jaeger · GB5Trace.Step() at every BLL decision point
Architecture3-tier per module: SL (FastEndpoints) → BLL → DAL