T15: Model Attributes
Attach values to the tree, inherit them downward, and use them in tags, queries, calculations, and instance names.
What you will build: an attribute set that drives tags, calculations, and instance names — turning a template into genuine reuse.
Time: about 35 minutes.
You need: T12 completed, with the Filler template and three instances.
Step 1 — Understand inheritance#
A model attribute is a named value attached to a point in the tree and inherited by everything beneath it.
Set the same attribute name lower down and the lower value wins for that subtree:
Riverside Plant Site Code = RVS
+-- Production Cost Centre = 4100
+-- Filler A Filler Number = 1
| +-- (metric) > sees Site Code=RVS, Cost Centre=4100, Filler Number=1
+-- Filler B Filler Number = 2, Cost Centre = 4200
+-- (metric) > sees Site Code=RVS, Cost Centre=4200, Filler Number=2
This is why folder structure and attribute design are the same exercise. Attributes only reach the right things if the tree puts those things underneath the right folder.
Set Site Code on your root folder and Cost Centre on Production now, and confirm a measure deep in the tree resolves all of them.
Step 2 — Attributes in tags (you have done this)#
From T12, your template's tag is:
FL00[ModelAttribute("Filler Number")].BottleCount
One template, three attribute values, three different tags. That is the mechanism that makes a template reusable rather than a copy-paste shortcut.
The value must match the source exactly. If tags were 010-FT-001, the attribute must be 010, not 10. Leading zeros, case, and trailing spaces all matter — and a mistyped attribute produces an instance that looks perfectly normal and reads a tag that does not exist.
Because attribute values are always strings, 01 and 1 are different values. Pad them to match the source system.
Step 3 — Attributes in calculations#
Add a design capacity to each filler folder:
| Folder | Design Capacity |
|---|---|
| Filler A | 12000 |
| Filler B | 12000 |
| Filler C | 18000 |
Now add a utilisation measure to the template (not to each instance):
| Field | Value |
|---|---|
| Name | Utilisation Daily |
| Interval | Day(s) |
| Unit | % |
| Format | 0.0 '%' |
SafeDivide(
Measure_<bottlesDailyId>.Value,
AttrAsDouble(Measure_<bottlesDailyId>.ModelAttributes, "Design Capacity")) * 100
Using the AttrAsDouble helper from T11:
public static double AttrAsDouble(ModelAttributeCollection attrs, string key, double fallback = 0)
{
if (attrs == null || !attrs.ContainsKey(key)) return fallback;
return double.TryParse(attrs[key].Value, out var v) ? v : fallback;
}
Release the template and update the instances.
Each filler now calculates utilisation against its own capacity — Filler C against 18,000, the others against 12,000 — from one expression.
Step 4 — Why the helper matters#
Attribute values are strings. Without the helper, every expression reading one needs:
Convert.ToDouble(Context.ModelAttributes["Design Capacity"].Value)
and that throws if the attribute is missing, blank, or not numeric — failing the whole calculation for that interval.
Test it: remove the Design Capacity attribute from Filler C and recalculate. With the helper, utilisation returns nothing (the fallback makes the denominator zero, and SafeDivide returns null). With a bare Convert.ToDouble, the calculation fails.
Put the attribute back.
Step 5 — Attributes in queries#
Attributes work in query-based connectors too. A single query on the connection can serve hundreds of measures:
SELECT Reading AS Value, QualityCode AS Quality
FROM ProcessData
WHERE UnitNo = '[ModelAttribute("Filler Number")]'
AND Stamp > [PeriodStart]
AND Stamp <= [PeriodEnd]
Note the half-open range — > on the start and <= on the end. With both bounds inclusive, a row landing on a boundary is counted in two consecutive periods, and totals come out slightly high in a way nobody notices.
Step 6 — Attributes in instance names#
When instantiating a template, attribute values can drive the names of created entities, so instances arrive named correctly rather than needing renaming.
Try instantiating with a name pattern using the sequence number {0} — 1-based — and an attribute pattern setting Filler Number to match.
Measures cannot use attribute-driven naming. Attribute-based name templating works on folders, metrics, and event schemes — not on measures. Template measure names come through exactly as authored.
Design around it: name template measures for the quantity and interval (Bottles Daily), and let the containing metric and folder carry the instance identity. The full path then reads Filler B → Filler → Bottles Daily, which is better anyway — repeating the filler number on every measure would be noise.
Step 7 — Attributes or manual measures?#
This is the design decision people get wrong, and it is worth being explicit.
| Model attribute | Manual entry measure | |
|---|---|---|
| Values over time | One current value, no history | One value per period |
| Who maintains it | A modeller, in Flow Config | An operator, through a form |
| Changes | Rarely | Regularly |
| Can you see last March's? | No | Yes |
Use an attribute for stable facts: unit number, design capacity, cost centre, site code, tag prefix.
Use a manual measure for anything that changes with any regularity: monthly targets, budgets, planned hours, ideal rate.
The test: if you would ever need to know what the value was last March, it was never an attribute. An attribute has one current value and no history — change it and the old value is simply gone, including for any historical period you recalculate afterwards.
Step 8 — Keep the set small#
Five or six attributes per template is comfortable. Twenty means the template is trying to be configurable about things that should be separate templates.
Name them for meaning, not for use: Design Capacity, not Divisor. In two years somebody will need to know what the number means, and the expression will not tell them.
Step 9 — Diagnose an attribute problem#
Break one deliberately: set Filler B's Filler Number to 2 — with a trailing space.
Recalculate. The tag resolves to FL002 .BottleCount, which does not exist, and the measure stops receiving values.
Now diagnose it as you would in the field: the instance looks correct, the template is fine, the other two instances work. Compare two instances attribute by attribute — one working, one not. The difference is almost always a string that does not look like what you assumed.
Fix it, and note the diagnostic technique. It is the fastest route to almost every template problem.
What you have learned#
Attributes inherit down the tree, and the nearest value wins.
Template = shape, attributes = specifics. That pairing is what makes reuse real.
Attributes work in tags, queries, calculations, and entity names — but not in measure names.
Values are strings. Convert defensively, and match the source's formatting exactly, including leading zeros.
Attributes are for stable facts; manual measures are for values with history.
Keep the set small and name for meaning.
Compare a working and a broken instance to diagnose attribute problems.
Try this before moving on#
Add a Product attribute and use it to select a different ideal rate per product for the OEE performance calculation from T13.
Then notice the limitation: an attribute has one current value, so it cannot express "this line ran product A this morning and product B this afternoon". That is a production run event with a product attribute — which is exactly the distinction between an attribute and an event, and the reason both exist.
Next: T16: Multi-Source Integration — combining a historian, a database, and an API in one KPI.