Skip to main content

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 via params.<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 makes TASK := '<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: AnalogTag ships built into every runtime (raweu linear scaling + auto-provisioned alarms + historian opt-in). You can write VAR_GLOBAL (Pump1_Flow) INSTANCE_OF AnalogTag without 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:

FieldMeaning
nameTemplate name (the (GVL_Pump) label)
taskTask it was seen in (when scoped)
paramsArray of {name, type, default, required}
variablesArray of {name, type, pragma} — the VARS, with flattened pragma map
instancesNames 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:

FieldMeaning
nameInstance name (e.g. Pump_17)
templateThe template it derives from
taskBound scan task (for BODY templates)
paramsFully resolved param map — defaults merged with the instance's args
override_countNumber of per-instance pragma overrides
overridesfield.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 write Pump1_Flow.raw, logic reads Pump1_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 missing TASK := '<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/templates and /api/instances return [] on a project that declares none; only GET /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.