Tag Structures That Won't Haunt You in Five Years
Every controls engineer has opened a five-year-old project and found 4,000 flat controller-scope tags named things like MOTOR_SPD_2_NEW_B. The person who wrote it was smart. They were also in a hurry. This guide is the set of rules that keeps "in a hurry" from becoming "unmaintainable."
01UDTs are your schema. Treat them like one.
A User-Defined Type is a contract: every pump in the plant gets the same members, the same HMI mapping, the same alarm structure. Design them once, deliberately.
// UDT: udt_Motor — one type for every motor on site
udt_Motor
├─ Cmd udt_Motor_Cmd // Start, Stop, Reset (from HMI/logic)
├─ Sts udt_Motor_Sts // Running, Faulted, AtSpeed (to HMI)
├─ Cfg udt_Motor_Cfg // AccelTime, FaultDelay — set once
└─ Data udt_Motor_Data // Speed, Current, RunHours — live values
- Group by direction and lifecycle (commands in, status out, config static, data live). Your HMI security model and your comms optimization both fall out of this for free.
- Order members largest-first (DINTs/REALs, then INTs, then BOOLs). Logix pads memory to 4-byte boundaries — interleaved BOOLs waste space and, worse, make the layout confusing in raw comms.
- Fill in the description on every member. Descriptions propagate to every instance. One sentence at design time saves a thousand crosshair hovers later.
You cannot edit a UDT online. Every member you forget is a download — which on a running line means a scheduled outage. Design UDTs with 2–3 spare members (Spare_DINT1, Spare_REAL1) if the process is still evolving. Inelegant? Yes. Cheaper than a shutdown? Also yes.
02AOI or UDT + subroutine? Decide with one question.
"Does the logic need to be locked and versioned as a unit?"
- Yes → AOI. Device control (motors, valves, PIDs), anything you'll reuse across projects, anything a system integrator hands over. AOIs carry a revision number and can be signed — the logic ships with the type.
- No → UDT + routine. Sequences, batch steps, anything you expect the site engineer to tune later. Debugging inside an AOI (one online view per instance context) frustrates people who just need to see the rung.
AOIs execute their scan in-place; a huge AOI with dozens of instances can bite scan time. Keep AOIs device-sized, not line-sized.
03Naming: pick a grammar, not just a style
Everyone argues Hungarian vs. Pascal. It matters less than this: a tag name should sort next to its siblings. Name by area → equipment → property:
A01_Feed_Pump.Sts.Running // area 01, feed pump, status
A01_Feed_Pump.Cmd.Start
A01_Surge_Tank.Data.Level_Pct
A02_Mixer.Cfg.AccelTime_s
- Prefix with area/cell code so the tag browser groups plant sections together.
- Put units in the name for analog values:
_Pct,_gpm,_degC,_s. Five years from now nobody remembers ifLevelwas percent or inches. - Never encode the PLC address or slot in the name. Hardware moves; names shouldn't.
04Controller scope vs. program scope
Default to program scope. Promote to controller scope only what genuinely crosses programs: line-level interlocks, produced/consumed tags, HMI-facing structures. A flat sea of controller tags is how projects rot — every routine can touch everything, so eventually everything does.
05Build the HMI-facing layer on purpose
Don't point the HMI at raw logic tags. Expose one branch of the UDT (.Sts and .Data) and accept commands only through .Cmd bits that logic validates before acting. You get three wins:
- Comms optimization — the HMI polls contiguous structure members instead of scattered tags.
- Security — an operator screen physically can't write to interlock logic.
- Sanity — when a value changes unexpectedly, the suspect list is short.
06Version your types like code (because they are)
- Keep a
UDT_Revisionnote in each type's description: date, initials, what changed. - Export UDTs and AOIs to
.L5Xand commit them to a repo — they diff as readable XML. The ACD file doesn't diff; the L5X does. - When a UDT must change on a live system: create
udt_Motor_v2, migrate device-by-device on planned stops, delete v1 when the count hits zero. Never mutate in place under time pressure.
Open your tag browser and imagine a new engineer, day one, no handover. Can they find the feed pump's run status in under 30 seconds using only the names? If yes, your structure passes. If they'd need the cross-reference tool, keep working.
Structured tags, but the diff is built in.
In io.codes, every change to your logic and tag structure is a git commit — branch, diff, and roll back like the software it always was. No L5X exports, no FINAL_v2_REAL_final.ACD.
See git-native controls →