I sat down to add a feature and spent the day walking the data model instead, because the feature kept dead-ending. What I found is the kind of thing that is obvious afterward and invisible while you are shipping. The word "tag" meant three different things in three different places, and nothing but a string held them together.
The first is the physical acquisition tag. The thing the edge box actually reads. A protocol address, a data type, a scale, a deadband, a poll. The second is the analytics tag. Rows in a table the KPI logic is supposed to consume. The third is telemetry. The readings themselves flowing in. Three versions of "a tag." The only glue between the physical one and the logical one was a source string like gw/src/tag that some code parsed by convention. Which tag was the part counter and which was the run state was not modeled anywhere. It was a habit the code agreed to have.
You can build a long way on that. We did. But everything downstream was standing on it. KPIs, transformations, the ML work I want to do next. And the model was both fragmented and thin. Fragmented because there were three tag concepts and a string bridge. Thin because a logical tag could only attach to a single machine, had no idea what it was for, and could not tell a setpoint from a sensor. A plant-level power meter had nowhere to live. A line-level takt signal had nowhere to live. A derived signal, some expression over other signals, had no place to be authored at all. That last one is the dead-end that sent me walking the model in the first place.
The tempting fix is to collapse it. One big unified tag object, every field on it, done. I think that is the wrong instinct, and naming why was the useful part of the day. The physical world and the logical world are genuinely different concerns. An electrician thinks in addresses and scales. A plant manager thinks in "OEE on line 3." Forcing both into one model does not reduce confusion. It smears two mental models into a shape neither one recognizes. The two stages should stay separate.
The problem was not the seam. It was that the seam was an implicit string instead of real metadata. So the shape I landed on is two stages with a translation layer between them. Acquisition first. Gateway, source, raw tag. Exactly what the box reads, promoted from a JSON blob into real rows so it can be referenced. Then a mapping. Scale, unit, role, and which node in the plant hierarchy it belongs to. Then a signal. The analytics-facing thing that attaches at any level of the hierarchy and carries a purpose. KPI input, setpoint, sensor, or keep it, I will want it later. That is the shape real historians already use. A driver layer and a tag database. There is a reason it landed there. Each person works in the stage that fits their head, and the translation between them is explicit and queryable instead of buried in a function.
What I would check, next time I inherit a data model that works: pick one downstream feature and trace it all the way back to the wire. Where does this number come from, physically? What decided it means this? If the answer is "a string we all agree to parse the same way," you found the seam that is going to cost you. Not today. The day you try to build the second and third thing on top of it.
The surprise is that the model got simpler to explain once I stopped trying to unify it. "Two stages, one translation layer, a purpose on the signal" is a sentence a new engineer can hold in their head. "One tag object with sixteen optional fields, half of which only apply sometimes" is not. Clarity was not fewer concepts. It was the right concepts with a real boundary between them.
Notes for next time: the seam between controls and operations is the actual product, so model it on purpose instead of letting a string stand in for it. Do not collapse two honest mental models to feel unified. Make the translation between them a real thing. Give every signal a purpose early, because KPIs and ML both need to know why a number exists, not just what it reads. And when a feature keeps dead-ending, stop adding to the feature and go walk the model. The dead-end is usually the model telling you the truth.