<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:discourse="http://discourse.org/rss/modules/discourse/">
<channel>
<title>Gomes on Roque</title>
<link>https://j-roque.com/authors/gomes/</link>
<description>Recent content in Gomes on Roque</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<managingEditor>joao.roque.94@gmail.com (João Roque)</managingEditor>
<webMaster>joao.roque.94@gmail.com (João Roque)</webMaster>
<copyright>© 2026 João Roque</copyright>
<lastBuildDate>Tue, 28 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://j-roque.com/authors/gomes/index.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Blueprint vs. Building: How MES's Data Model Actually Works</title>
<link>https://j-roque.com/posts/20260728-mes-dynamic-model/</link>
<pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
<author>joao.roque.94@gmail.com (João Roque)</author>
<guid>https://j-roque.com/posts/20260728-mes-dynamic-model/</guid>
<description>MES doesn&amp;rsquo;t let you extend the data model by writing code — it lets you describe what you want and generates the rest at runtime. Here&amp;rsquo;s how that actually works, table by table.</description>
<content:encoded>&lt;p&gt;&lt;img src="https://j-roque.com/posts/20260728-mes-dynamic-model/featured.png" alt="Blueprint vs. Building: How MES's Data Model Actually Works" /&gt;&lt;/p&gt;&lt;p&gt;Most enterprise software gives you two options when the out-of-the-box data model doesn&amp;rsquo;t fit: fork the code, or live without the feature. CM MES was built to reject that choice entirely.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Overview
&lt;div id="overview" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#overview" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;The MES &lt;strong&gt;dynamic model&lt;/strong&gt; is a the mechanism that lets you add a brand-new business entity to the system by filling in a wizard, not by shipping a deployment. It&amp;rsquo;s one of those pieces of architecture that&amp;rsquo;s easy to take for granted until you&amp;rsquo;re asked to explain &lt;em&gt;why&lt;/em&gt; it works.&lt;/p&gt;
&lt;p&gt;This post covers the static/dynamic split, what happens under the hood when you create and generate an entity, the different ways MES lets you attach data to that entity, and when to reach for a table instead.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Why the split exists
&lt;div id="why-the-split-exists" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-the-split-exists" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Before MES existed in its current form, the requirement on the table was blunt: the system needed to be extensible &lt;strong&gt;by configuring metadata, not by writing code&lt;/strong&gt;. Every other application on the market made you choose between using what the vendor shipped, or hand-rolling a new set of database tables, access layers, and UI screens from scratch every time the business needed a new concept.&lt;/p&gt;
&lt;p&gt;That flexibility is dangerous on its own. Opening the door to &lt;code&gt;add whatever entity you want&lt;/code&gt; may end up with every site running a system that only superficially resembles every other site, nothing comparable, nothing maintainable centrally. So the second requirement showed up right behind the first: whatever gets built has to stay &lt;strong&gt;consistent&lt;/strong&gt;. A custom entity created by customization needs to be structurally comparable to a base entity shipped by the product.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Flexibility without consistency isn&amp;rsquo;t extensibility, it&amp;rsquo;s just fragmentation with extra steps.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Put those two requirements together and you get controlled &lt;strong&gt;scalability&lt;/strong&gt;: customers can extend the system in a thousand different directions, but every one of those directions is built on the same underlying structure, with the same meta-structure.&lt;/p&gt;
&lt;h2 class="relative group"&gt;The blueprint and the building
&lt;div id="the-blueprint-and-the-building" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-blueprint-and-the-building" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;The solution was to split the data model into two halves that depend on each other but are governed by completely different rules.&lt;/p&gt;
&lt;p&gt;The &lt;a
href="https://developer.criticalmanufacturing.com/explore/reference/datadictionary/models/static-model/"
target="_blank"
&gt;static model&lt;/a&gt; is the metadata layer — &lt;strong&gt;the blueprint&lt;/strong&gt;. It defines entity types, their properties, and their relationships, and it lives in tables like &lt;code&gt;T_EntityType&lt;/code&gt; and &lt;code&gt;T_EntityTypeProperty&lt;/code&gt;. It also happens to be where MES stores everything the framework needs to operate that isn&amp;rsquo;t business data: security, transaction history, state transitions, query execution structures, DEE actions. Critically, the static model &lt;strong&gt;cannot be extended&lt;/strong&gt;. If you want to add something new to it, you implement it explicitly, new tables, new stored procedures to read and write it, the works. Nothing about it is automatic.&lt;/p&gt;
&lt;p&gt;The &lt;a
href="https://developer.criticalmanufacturing.com/explore/reference/datadictionary/models/dynamic-model/"
target="_blank"
&gt;dynamic model&lt;/a&gt; is the data layer — &lt;strong&gt;the building&lt;/strong&gt;. It&amp;rsquo;s where actual business information lives: materials, production orders, steps, flows, and anything a custom entity type produces.&lt;/p&gt;
&lt;p&gt;On the database, it splits into two schemas:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CoreDataModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;System-level, out-of-the-box entities, generic tables, and smart tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserDataModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anything created through customization — custom entities, custom generic tables, custom smart tables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you&amp;rsquo;re staring at a database and trying to figure out what&amp;rsquo;s &lt;strong&gt;core product&lt;/strong&gt; versus what a &lt;strong&gt;customization project&lt;/strong&gt; created, the schema is explicit.&lt;/p&gt;
&lt;p&gt;If you crack open the online database and browse alphabetically, the dynamic model — &lt;code&gt;CoreDataModel&lt;/code&gt; and &lt;code&gt;UserDataModel&lt;/code&gt; combined — is roughly &lt;strong&gt;90% of the entire MES data model&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The static model is a comparatively small, fixed core that exists purely to describe how to build everything else.&lt;/p&gt;
&lt;h2 class="relative group"&gt;The runtime generation engine
&lt;div id="the-runtime-generation-engine" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-runtime-generation-engine" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the part that actually does the work. Since the static model is fixed and explicit, and the dynamic model is neither, something has to translate one into the other. That something is the runtime generation engine, and it&amp;rsquo;s arguably the single most important mechanism in the whole platform.&lt;/p&gt;
&lt;p&gt;Create a new entity type through &lt;strong&gt;System &amp;gt; Administration &amp;gt; Entity Types&lt;/strong&gt;, and the wizard calls &lt;code&gt;CreateEntityType&lt;/code&gt;, which does exactly one thing: it writes a row into &lt;code&gt;T_EntityType&lt;/code&gt; and rows into &lt;code&gt;T_EntityTypeProperty&lt;/code&gt;. That&amp;rsquo;s it. No tables, no DLLs, nothing else exists yet.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT *
FROM [dbo].[T_EntityType]
WHERE [Name] = &amp;#39;ACustomEntity&amp;#39;
SELECT etp.*
FROM [dbo].[T_EntityTypeProperty] etp
INNER JOIN [dbo].[T_EntityType] et ON etp.EntityTypeId = et.EntityTypeId
WHERE et.[Name] = &amp;#39;ACustomEntity&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="New Entity"
src="https://image.j-roque.com/posts/20260728-mes-dynamic-model/new-entity.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;That gap between defining the entity and generating it isn&amp;rsquo;t an oversight — it&amp;rsquo;s deliberate. Once the schema is generated, properties are locked. You can&amp;rsquo;t rename or remove them. So the system gives you a window to iterate on the definition before it becomes permanent.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re happy with the definition, you hit &lt;strong&gt;Generate Schema&lt;/strong&gt;, and two things fire: &lt;code&gt;GenerateEntitySchema&lt;/code&gt;, a SQL procedure that builds the main table, the history table, and — depending on the entity&amp;rsquo;s configuration — relation tables, effective version tables, and attribute tables; and a second step that generates the C# assembly.&lt;/p&gt;
&lt;p&gt;That second part matters because a database table by itself is useless to the application layer. The generator produces a &lt;code&gt;CMF.&amp;lt;Tenant&amp;gt;.BusinessObjects.&amp;lt;EntityName&amp;gt;.dll&lt;/code&gt;, dropped straight into the application host, with the same method surface as every out-of-the-box entity&amp;rsquo;s DLL.&lt;/p&gt;
&lt;p&gt;This will generate two new tables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T_[EntityName] - which stores all the instances of that entity in the system. The structure of the table is created based on the metadata defined for the table in the T_EntityTypeProperty table.&lt;/li&gt;
&lt;li&gt;T_[EntityName]History - stores the complete changes history of the T_[EntityName] table. It will have a similar structure but additionally it will have the DatabaseOperation, the ServiceHistoryId and the OperationHistorySeq&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Entity Tables Generated"
src="https://image.j-roque.com/posts/20260728-mes-dynamic-model/new-entity-tables.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;A custom entity and a CM MES entity &lt;code&gt;Area&lt;/code&gt; share the exact same shape at the code level they were built by the same machine.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tenant name casing has to match exactly between where the assembly is built and where it runs. A mismatched case on an otherwise-identical tenant name is a real, reported failure mode.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 class="relative group"&gt;Anatomy of an entity
&lt;div id="anatomy-of-an-entity" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#anatomy-of-an-entity" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Once an entity exists, MES gives you &lt;strong&gt;five&lt;/strong&gt; different places to hang data off it, and picking the wrong one is the most common way to paint yourself into a corner.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Stored where&lt;/th&gt;
&lt;th&gt;Access&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Property&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Column on the main table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Entity.Load&lt;/code&gt; / &lt;code&gt;Entity.Save&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native, fastest, can reference other entities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom Property&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Column on the main table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Entity.LoadAttributes&lt;/code&gt; / &lt;code&gt;SaveAttributes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requires a schema regeneration every time one is added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attribute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate attribute table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Entity.LoadAttributes&lt;/code&gt; / &lt;code&gt;SaveAttributes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scalar types only, can be an array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operation Attribute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate attribute table, written by operations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OperationAttributeCollection&lt;/code&gt; on the orchestration call&lt;/td&gt;
&lt;td&gt;Not visible on the entity page — only in history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Main table (primary) or &lt;code&gt;T_[Entity]State&lt;/code&gt; (secondary)&lt;/td&gt;
&lt;td&gt;Standard state APIs&lt;/td&gt;
&lt;td&gt;Only the &lt;em&gt;first&lt;/em&gt; state model lives on the main table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Properties&lt;/strong&gt; are the default and the fast path — direct columns, and &lt;code&gt;Entity.Load&lt;/code&gt; with zero levels fetches only them. Bump the load level and you start pulling in every referenced entity behind them, which is the single easiest way to accidentally drag your whole database into memory. Load in collection, and keep lazy-loading to a minimum — every level you add is a database round trip you didn&amp;rsquo;t need. More information on &lt;a
href="https://developer.criticalmanufacturing.com/explore/best-practices/performance/"
target="_blank"
&gt;developer portal&lt;/a&gt; and in a &lt;a
href="https://devblog.criticalmanufacturing.com/blog/20250630_performance_analysis/"
target="_blank"
&gt;blog post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attributes&lt;/strong&gt; live in a dedicated table and can&amp;rsquo;t reference other entities — scalar types only. They can also be arrays, which is a feature almost nobody in the field seems to use. Each array element is its own database row:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;/* Get attributes for Site and Materials */
SELECT *
FROM CoreDataModel.T_SiteAttribute
SELECT *
FROM CoreDataModel.T_MaterialAttribute&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;rsquo;s worth sitting with for a second: if an attribute array holds 100,000 values, loading it means fetching 100,000 rows. It&amp;rsquo;s a legitimate tool, but not one to reach for without thinking about the ceiling on cardinality first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Custom Properties&lt;/strong&gt; are the odd one out — stored as columns on the main table like a native property, but accessed through the attribute API like an attribute. The entire justification for their existence is performance: pull a heavily-used attribute onto the main table so it&amp;rsquo;s a column scan instead of a join. The catch is that adding one means regenerating the schema, and the payoff isn&amp;rsquo;t guaranteed — if the main table is already wide, or the entity already sees heavy traffic, bringing more columns onto it can make things worse, not better. This needs to be measured case by case, not assumed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operation Attributes&lt;/strong&gt; are the one most people haven&amp;rsquo;t touched, and probably should more. They&amp;rsquo;re not tied to the entity — they&amp;rsquo;re tied to a specific operation performed on it, and they only show up in history, never on the entity&amp;rsquo;s own page.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;/* Only for all Operation attributes - Each operation writes on top of the last */
SELECT TOP (1000) *
FROM [CoreDataModel].[T_MaterialOperationAttribute]
/* Generic for all attributes (Operation attributes are attributes too) */
SELECT TOP (1000) *
FROM [CoreDataModel].T_MaterialAttributeHistory&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The canonical example is a track-out with a recorded loss: a &lt;code&gt;LossReason&lt;/code&gt; array and a &lt;code&gt;PrimaryQuantityLoss&lt;/code&gt; array, matched by index — position 0 of one array corresponds to position 0 of the other. That&amp;rsquo;s how a single operation reports &amp;ldquo;this much scrap, for this reason, and this much scrap, for that other reason&amp;rdquo; without inventing a new entity for it.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Operation Attribute"
src="https://image.j-roque.com/posts/20260728-mes-dynamic-model/operation_attribute.gif"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Finally, &lt;strong&gt;State Models&lt;/strong&gt;. Every entity&amp;rsquo;s primary state lives directly on the main table — &lt;code&gt;MainStateModelId&lt;/code&gt; and &lt;code&gt;MainStateModelStateId&lt;/code&gt; are just columns:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT MainStateModelId, MainStateModelStateId, *
FROM [CoreDataModel].[T_Resource]
WHERE ResourceId = 2509290318110000004
SELECT Name, *
FROM dbo.T_StateModel
WHERE StateModelId = 1805111618120000001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;T_[Entity]State&lt;/code&gt; table only fills up once you attach a &lt;em&gt;second&lt;/em&gt; state model to the same entity. Query &lt;code&gt;T_ResourceState&lt;/code&gt; on a resource that only has its one universal state model, and you&amp;rsquo;ll find it empty — not a bug, just a table that only exists for the overflow case.&lt;/p&gt;
&lt;h3 class="relative group"&gt;Access, localization, and validation
&lt;div id="access-localization-and-validation" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#access-localization-and-validation" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h3&gt;
&lt;p&gt;Every property carries a bitmask access level — &lt;strong&gt;hidden&lt;/strong&gt;, &lt;strong&gt;read-only&lt;/strong&gt;, or &lt;strong&gt;editable&lt;/strong&gt;, independently configurable at &lt;strong&gt;create&lt;/strong&gt;, &lt;strong&gt;view&lt;/strong&gt;, and &lt;strong&gt;update&lt;/strong&gt; time, and independently at the entity level versus the template level. That last part is a genuinely useful trick: lock a property down at the entity level while leaving it editable at the template level, and you&amp;rsquo;ve built yourself an administrator override without writing a line of custom code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The access level is stored as a raw integer (e.g 530) on the database, which is exactly as unreadable as it sounds if you&amp;rsquo;re building custom entities by code rather than through the UI — there&amp;rsquo;s an &lt;a
href="https://j-roque.com/posts/20260728-mes-dynamic-model/AccessLevelHelper.xlsx"&gt;AccessLevelHelper&lt;/a&gt; spreadsheet for translating the bit flags into something you can reason about.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two more small but underused levers: a property can carry a &lt;strong&gt;localized message&lt;/strong&gt; to decouple its internal name from what the operator actually sees, and a &lt;strong&gt;validation rule&lt;/strong&gt; (regex or range) can be attached directly at the property level, which cuts down on the number of custom Dynamic Execution Engine actions you&amp;rsquo;d otherwise write just to reject bad input.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Relations are entities too
&lt;div id="relations-are-entities-too" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#relations-are-entities-too" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Relations aren&amp;rsquo;t a separate concept bolted onto entities, they &lt;em&gt;are&lt;/em&gt; entity types, just ones with a &lt;strong&gt;source&lt;/strong&gt; and a &lt;strong&gt;target&lt;/strong&gt; instead of a flat property list. There are two main scenarios that push you toward a relation instead of a plain property:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A genuine &lt;strong&gt;N:N&lt;/strong&gt; relationship — &lt;code&gt;EmployeeCertification&lt;/code&gt;, where one employee holds many certifications and one certification belongs to many employees.&lt;/li&gt;
&lt;li&gt;A relationship that needs to carry &lt;strong&gt;more information than a single property can hold&lt;/strong&gt; — &lt;code&gt;MaterialContainer&lt;/code&gt;, where it&amp;rsquo;s not enough to know &lt;em&gt;which&lt;/em&gt; container a material sits in; you also need to know &lt;em&gt;where in it&lt;/em&gt; (slot 9, say).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one question that actually matters when you design a relation is: &lt;strong&gt;what happens to the relation when one side of it terminates?&lt;/strong&gt; The default rule is straightforward — terminate the source, and the relation terminates with it; terminate the target, and the termination operation should fail. For &lt;code&gt;MaterialContainer&lt;/code&gt;: terminate the material, the relation is meaningless and dies with it; terminate the container while materials still reference it, and that termination should be rejected, because those materials would be left pointing at nothing.&lt;/p&gt;
&lt;p&gt;Like most default rules, there are exceptions carved out in business logic, for example &lt;code&gt;MaterialProductionOrder&lt;/code&gt; allows both sides to terminate independently, because a production order closing shouldn&amp;rsquo;t force the physical materials still sitting on the line out of existence.&lt;/p&gt;
&lt;p&gt;If we have a use case where &lt;em&gt;at production order close, report to the ERP the sum of scrap quantity per step&lt;/em&gt;. We would create a custom relation with information that includes the Production Order, the Step and the Scrap information.&lt;/p&gt;
&lt;p&gt;The instinctive answer is to make &lt;strong&gt;Step&lt;/strong&gt; the source, since you&amp;rsquo;re aggregating by step&amp;hellip;&lt;/p&gt;
&lt;p&gt;But if you see it from a terminate perspective: terminate the step, and every production order that used it would have to stop — clearly not acceptable. Terminate the production order, and you just lose the scrap counter, which is fine, because the production order no longer existing means the counter is irrelevant anyway. So &lt;strong&gt;Production Order is the source, Step is the target&lt;/strong&gt;, the relation dies cleanly when the thing it&amp;rsquo;s describing (a specific order&amp;rsquo;s run) goes away, and survives the thing it&amp;rsquo;s merely referencing (a step definition that other orders still use).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re not sure which side of a relation is the source, ask what should happen when each side terminates. The answer is a good rule of thumb.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Relations also carry a &lt;code&gt;LockType&lt;/code&gt; — &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;LockSource&lt;/code&gt;, &lt;code&gt;LockTarget&lt;/code&gt;, or &lt;code&gt;LockBoth&lt;/code&gt; — which forces a row lock on the relevant entity&amp;rsquo;s main table every time the relation is created or updated. That&amp;rsquo;s a real DB lock, so it needs a real justification.&lt;/p&gt;
&lt;p&gt;The textbook case is a sub-resource relation, where you need the parent resource&amp;rsquo;s state update to wait for the child relation update to finish, because state propagation logic depends on it.&lt;/p&gt;
&lt;p&gt;Reach for it deliberately, not by default.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Recap - Generated tables per Entity
&lt;div id="recap---generated-tables-per-entity" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#recap---generated-tables-per-entity" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;A closer look at the other generated tables for a more complex Entity configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T_[EntityName]Attribute&lt;/code&gt; &amp;amp; &lt;code&gt;T_[EntityName]AttributeHistory&lt;/code&gt;- Store current and historical values of instance attributes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T_[EntityName]OperationAttribute&lt;/code&gt;- Stores values for operation attributes performed on the entity.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T_[EntityName]EffectiveVersionHistory&lt;/code&gt;- Stores the history for the effective version.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T_[EntityName]State&lt;/code&gt; &amp;amp; &lt;code&gt;T_[EntityName]StateHistory&lt;/code&gt; - Store the current and historical states across different state models. Its only populated when an entity has more than one StateModel.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class="relative group"&gt;Entity limitations worth knowing up front
&lt;div id="entity-limitations-worth-knowing-up-front" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#entity-limitations-worth-knowing-up-front" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;A short list of hard constraints that are easy to discover the expensive way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Properties and custom properties &lt;strong&gt;cannot be deleted&lt;/strong&gt; once the schema is generated — you&amp;rsquo;d be dropping live database columns.&lt;/li&gt;
&lt;li&gt;Properties can only be &lt;strong&gt;added before&lt;/strong&gt; the first schema generation.&lt;/li&gt;
&lt;li&gt;Custom properties &lt;strong&gt;can be added after&lt;/strong&gt; generation, but each addition requires a schema regeneration.&lt;/li&gt;
&lt;li&gt;Adding a mandatory custom property to an entity that already has instances requires a default value, or the existing rows would have no way to satisfy the constraint.&lt;/li&gt;
&lt;li&gt;Attributes and operation attributes are the most forgiving of the five, they&amp;rsquo;re new rows in an attribute table, addable at any point in the entity&amp;rsquo;s life.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 class="relative group"&gt;Smart and generic tables
&lt;div id="smart-and-generic-tables" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#smart-and-generic-tables" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Entities aren&amp;rsquo;t the only way to store business data. When you just need a lookup or a temporary mapping, generic and smart tables exist specifically so you don&amp;rsquo;t reach for a full entity type by default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generic tables&lt;/strong&gt; are the simple case: general-purpose key/value storage with a &lt;code&gt;T_GT_&lt;/code&gt; prefix, a main table and a history table, and nothing more exotic than that. Keys are mandatory, resolution is a straight lookup, and if an entity referenced by a key gets terminated, the row disappears with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Smart tables&lt;/strong&gt; (&lt;code&gt;T_ST_&lt;/code&gt; prefix) add three things generic tables don&amp;rsquo;t have: &lt;strong&gt;validation rules&lt;/strong&gt; that run before and after a value changes, support for &lt;strong&gt;more than one value per key&lt;/strong&gt;, and &lt;strong&gt;precedence keys&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Precedence keys define a priority order for resolving a lookup from most specific to most generic, evaluated top-down until something matches.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see an example: a table keyed by &lt;code&gt;Step&lt;/code&gt; (mandatory), &lt;code&gt;Flow&lt;/code&gt;, &lt;code&gt;Product&lt;/code&gt;, and &lt;code&gt;Material&lt;/code&gt;. Building the precedence order out loud forces you to reason about which keys generalize which:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Step + Flow + Material&lt;/code&gt; — most specific&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step + Material&lt;/code&gt; — same specificity of business meaning, one less dependency&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step + Flow + Product&lt;/code&gt; — abstract away Material into Product&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step + Product&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step + Flow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step&lt;/code&gt; — most generic, always resolves&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The rule for building that list: every combination should use all the keys that matter to it, and any combination that would resolve identically to another (&lt;code&gt;Step + Product + Material&lt;/code&gt; next to &lt;code&gt;Step + Material&lt;/code&gt; — since a material only ever belongs to one product) should be dropped as redundant noise.&lt;/p&gt;
&lt;p&gt;&lt;a
href="https://developer.criticalmanufacturing.com/explore/guides/customizations/business/resolvingsmarttables/?h=resolve&amp;#43;smart&amp;#43;tables"
target="_blank"
&gt;Resolution&lt;/a&gt; itself comes in two flavors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standard&lt;/strong&gt; requires every precedence key to match the input exactly.
&lt;strong&gt;Partial&lt;/strong&gt; loosens that — a row still matches if a key was left out of the input entirely, or if the key column is null and the input passed null for it too.&lt;/p&gt;
&lt;p&gt;That distinction is what lets a smart table serve both a fully-specified lookup and a fallback default from the same structure.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Generic Table&lt;/th&gt;
&lt;th&gt;Smart Table&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resolution&lt;/td&gt;
&lt;td&gt;One fixed key combination, all keys mandatory&lt;/td&gt;
&lt;td&gt;Multiple precedence combinations, evaluated top-down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pre/post validation rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple values per key&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial / null matching&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change control&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If none of those extra capabilities are in play, a generic table is genuinely a smart table with exactly one precedence key using every column — there&amp;rsquo;s no reason to pay for the complexity you don&amp;rsquo;t need.&lt;/p&gt;
&lt;h3 class="relative group"&gt;Overriding the framework&amp;rsquo;s own tables
&lt;div id="overriding-the-frameworks-own-tables" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#overriding-the-frameworks-own-tables" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ContextResolution&lt;/code&gt; generic table is how the product itself decides which smart table governs a given resolution context — and it&amp;rsquo;s user-overridable.&lt;/p&gt;
&lt;p&gt;Point it at your own smart table, and you own the resolution logic. Do that for a step-related context and the custom table must carry a &lt;code&gt;Step&lt;/code&gt; column typed against the &lt;code&gt;Step&lt;/code&gt; entity; same pattern for &lt;code&gt;Resource&lt;/code&gt;. The one discipline worth holding onto here: before building a custom smart table that overrides a step- or resource-based context, go look at what precedence keys the out-of-the-box table already uses. Ship a replacement that&amp;rsquo;s missing functionality the standard one had, and the client will absolutely notice a few months later.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Final thoughts
&lt;div id="final-thoughts" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#final-thoughts" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Strip away the SQL and the wizard screens, and the whole dynamic model is one idea, applied consistently: describe what you want in the static model, and let the runtime generation engine build the database objects, the assemblies, and the application-layer plumbing for you. Static is the blueprint. Dynamic is the data. Everything else — entities, custom properties, attributes, relations, smart tables — is a variation on how much structure you&amp;rsquo;re willing to ask the generator to build on your behalf.&lt;/p&gt;
&lt;p&gt;The part that&amp;rsquo;s easy to miss from the outside is that this isn&amp;rsquo;t just a code-generation convenience. It&amp;rsquo;s the mechanism that lets a base product entity and a customer&amp;rsquo;s custom entity be structurally indistinguishable at the framework level — which is the entire reason the platform can be extended without slowly turning into an unmaintainable pile of one-off systems per customer.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This blog post was based on a talk by Marcelo Gomes in 2025-10-02 @CM-Portugal&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded>
<enclosure url="https://j-roque.com/posts/20260728-mes-dynamic-model/featured.png" type="image/png" length="0"/>
<media:content url="https://j-roque.com/posts/20260728-mes-dynamic-model/featured.png" type="image/png" medium="image"/>
</item>
</channel>
</rss>