Program & Tag Templates (Reusable Parameterized POUs)
June 2026 | ControlForge v1.0.1069
A tag template lets you define the shape of a device once — its variables, metadata, and optional per-scan behavior — and then stamp it out N times with per-instance parameters. Twenty pumps, forty motors, a hundred actuators collapse from hundreds of copy-pasted VAR_GLOBAL lines into one template plus one short INSTANCE_OF block per device. Each instance gets named, qualified tags (Pump_17.running, Pump_17.flow_gpm) — not a nameless array index — so HMI bindings, alarms, and the historian stay readable.
Templates are declared entirely in Structured Text (no TEMPLATE_* ST functions exist). The runtime exposes a read-only HTTP surface so the IDE and the embedded AI assistant can browse every template, its parameters, and every instance expanded against it.
The two ST constructs
A template is a VAR_GLOBAL block tagged TEMPLATE, with up to three labelled sections. An instance is a VAR_GLOBAL block tagged INSTANCE_OF <template>.
VAR_GLOBAL (GVL_Pump) TEMPLATE
PARAMS:
max_flow : REAL := 500.0; (* no initializer = required param *)
VARS:
running : BOOL;
runtime_hr : REAL {units := 'h', logged := TRUE};
flow_gpm : REAL {units := 'gpm', range := [0.0, params.max_flow]};
BODY:
IF running THEN
runtime_hr := runtime_hr + 0.001;
END_IF;
END_BODY
END_VAR
VAR_GLOBAL (Pump_17) INSTANCE_OF GVL_Pump
max_flow := 750.0; (* override the param for this instance *)
TASK := 'Main'; (* required when the template has a BODY *)
END_VAR
PARAMS:— typed inputs the instance supplies. A param with an initializer (:= 500.0) is optional and defaults; a param with no initializer is required, and an instance that omits it faults at load.VARS:— the per-instance tags. These expand to qualified runtime tags named<instance>.<var>(e.g.Pump_17.running). Pragmas ({units := ...},range,logged, alarm thresholds) ride along and can interpolate params viaparams.<name>or'{params.<name>}'in strings.BODY:(optional) — ST that runs once per scan, per instance, against that instance's own copy of the VARS. A template that declares a BODY makesTASK := '<task>';mandatory on every instance (the body has to be scheduled somewhere). Pure data templates (no BODY) don't need a TASK.
An instance block assigns param values and optional pragma overrides by field.key := value;. The reserved TASK := '...' binds the body's scan task and is intercepted before normal param resolution.
System template:
AnalogTagships built into every runtime (raw→eulinear scaling + auto-provisioned alarms + historian opt-in). You can writeVAR_GLOBAL (Pump1_Flow) INSTANCE_OF AnalogTagwithout declaring the template yourself. It only appears in the template API once a project actually declares an instance of it.
Validate any template/instance ST against the running target before deploying:
curl -s -X POST http://localhost:8302/api/programs/validate \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"source":"VAR_GLOBAL (GVL_Pump) TEMPLATE\n PARAMS:\n max_flow : REAL := 500.0;\n VARS:\n running : BOOL;\nEND_VAR\nPROGRAM Main\nEND_PROGRAM"}'
# → {"valid":true}
The read API
All four endpoints are idempotent GETs (tag templates). Template declarations come from ST source and change only through a program edit + reload — there is no POST/PUT/DELETE here. Get a token first:
TOKEN=$(curl -s -X POST http://localhost:8302/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"goplc","password":"goplc"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
List every template
curl -s http://localhost:8302/api/templates -H "Authorization: Bearer $TOKEN"
Returns an array of template objects (empty [] when none are declared). Each carries:
| Field | Meaning |
|---|---|
name | Template name (the (GVL_Pump) label) |
task | Task it was seen in (when scoped) |
params | Array of {name, type, default, required} |
variables | Array of {name, type, pragma} — the VARS, with flattened pragma map |
instances | Names of every instance currently expanded against this template |
A param's required is a convenience flag: true exactly when it has no default.
Get one template
curl -s http://localhost:8302/api/templates/GVL_Pump -H "Authorization: Bearer $TOKEN"
Returns the full template object above for one name (case-insensitive match), or 404 {"error":"template not found"}.
List instances of one template
curl -s http://localhost:8302/api/templates/GVL_Pump/instances -H "Authorization: Bearer $TOKEN"
Flat view of every instance across all tasks
curl -s http://localhost:8302/api/instances -H "Authorization: Bearer $TOKEN"
Both instance endpoints return an array (empty [] when none) of instance objects:
| Field | Meaning |
|---|---|
name | Instance name (e.g. Pump_17) |
template | The template it derives from |
task | Bound scan task (for BODY templates) |
params | Fully resolved param map — defaults merged with the instance's args |
override_count | Number of per-instance pragma overrides |
overrides | field.key fingerprints of the overrides (values live in the resolved tags) |
/api/templates/{name}/instances filters to one template; /api/instances is the unfiltered project-wide list the IDE uses to decorate every GVL node in its tree.
Notes & limits
- ST + reload is the only way to change a template. The API is read-only by design — templates are source, not runtime state. Edit the
.st, reload the program/task, then re-query. - Instance VARS become qualified tags
<instance>.<var>. Read and write them like any other global (drivers writePump1_Flow.raw, logic readsPump1_Flow.eu). Auto-provisioned alarms surface as<instance>.<var>:hi,:hihi,:lo,:lolo. - BODY ⇒ TASK required. A behavior template (one with a
BODY:section) faults any instance missingTASK := '<task>';. Pure data templates don't. - A required param (no default) faults an instance that doesn't supply it — this is the typo-catcher that copy-paste VAR blocks never had.
- Empty arrays, not 404, for "none."
GET /api/templatesand/api/instancesreturn[]on a project that declares none; onlyGET /api/templates/{name}404s on an unknown name. - This is tag grouping (named data + metadata), distinct from function blocks (a scan-time compute unit you instantiate for behavior). Use a template when you want N named, metadata-rich device tag sets; reach for an FB when you want reusable logic.