PI System Trigger Node
Overview
The PI System Trigger Node starts a MaestroHub pipeline when an AVEVA PI Data Archive produces new values, streamed through the MaestroHub PI Agent. Unlike the PI System read nodes, which read inside an already-running pipeline, this trigger starts a new execution as values arrive.
The trigger is backed by a Subscribe to Tag Values function (pisystem.subscribe) authored on a PI System connection.
This is the single most important difference from every other industrial trigger, including PI Web API against the same historian.
PI can deliver values far faster than one pipeline execution per value would allow, so the connector emits one event per batch — up to 8,000 values. Your downstream nodes must iterate $trigger.result.values; reading "the value" from the event will only ever see the first one.
The PI System connector reaches PI through the MaestroHub PI Agent, a Windows service you install inside the plant network. Download it from https://portal.maestrohub.com/downloads/plugins and see the installation guide.
Core functionality
1. Event-driven pipeline execution Start pipelines as values arrive, with no polling loop of your own.
2. Batched delivery Each event carries a batch of values with their paths, timestamps and quality. Batch size follows the stream — a quiet tag set produces small events, a busy one produces large ones.
3. Flexible tag selection Subscribe by explicit tag list, by name wildcard, by AF element subtree, or any combination. The union is de-duplicated.
4. Automatic subscription lifecycle The subscription is created when the pipeline becomes enabled and torn down when it is disabled. After a connection or archive outage the agent recovers missed values and delivers them marked as backfill.
5. No connector-side deduplication
Every value carries its own timestamp, so consecutive batches differ even when a reading repeats. Like the OPC UA, Ignition and PI Web API triggers, this one does not hash-dedup, and there is intentionally no onChange toggle — one that could never suppress anything would mislead.
How PI System triggering works
| Aspect | Read nodes | Subscribe (trigger) |
|---|---|---|
| Model | Request–response (pull) | Streaming (push) |
| Execution | One read per call | Continuous — one execution per batch of values |
| Lifecycle | Stateless | Stateful subscription held by the agent |
| Payload | result.values for the paths you asked for | result.values for whatever changed |
When you enable a pipeline with a PI System Trigger:
- MaestroHub resolves the Subscribe function's selectors — tag paths, name filter, AF element path — into a concrete tag set, and reports how many matched.
- The agent registers those tags with PI and begins streaming.
- Values are decoded and grouped into batches; each batch becomes one pipeline execution.
- The pipeline runs with the batch available as
$trigger.result.valuesand the event's context as$trigger._metadata.
Reconnection and recovery
| Scenario | Behaviour |
|---|---|
| Transient network error | The connector reconnects and re-establishes the subscription automatically. |
| PI archive outage | The agent keeps running and retries. Once PI returns, values recorded during the gap are recovered and delivered marked backfill. |
| Agent restart | Subscriptions are re-established when the session reconnects. Values the agent had not yet delivered do not survive its restart. |
| MaestroHub restart | Triggers for enabled pipelines are restored on startup. |
Backfilled values arrive with $trigger._metadata.backfill set to "true" and frameType of backfill. If a downstream calculation should treat recovered history differently from live readings — or skip it entirely — branch on that field.
You can also turn recovery off entirely with the function's Include Recovered Values toggle, which discards those values instead of delivering them.
Configuration options
Basic information
| Field | Type | Description |
|---|---|---|
| Node Label | String (Required) | Display name on the pipeline canvas. Must be non-empty. |
| Description | String (Optional) | Explains what this trigger initiates. |
Configuration
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
| PI System Connection | Connection ID | "" | Yes | The PI System connection profile to subscribe through. Filtered to PI System connections. |
| Subscribe Function | Function ID | "" | Yes | The pisystem.subscribe function that defines which tags to stream. Filtered to subscribe functions on the selected connection. |
| Enable Trigger | Boolean | true | No | When disabled, the subscription is not created even if the pipeline is enabled. |
The selected function must be a Subscribe to Tag Values function (pisystem.subscribe). Read and write functions cannot drive this trigger.
If the connection has no subscribe functions yet, use the Edit connection link in the picker to jump to its Functions tab and author one.
Subscribe function configuration
The selected function controls which tags are streamed and how they are batched. These are set on the function, not on the node:
| Setting | Default | Description |
|---|---|---|
| Tag Paths | — | Tags to subscribe explicitly |
| Name Filter | — | Every tag matching this pattern; * and ? are wildcards |
| AF Element Path | — | Every PI point beneath this Asset Framework element |
| Maximum Tags | 10000 | Refuse the subscription if the selectors resolve to more tags than this (1–200,000) |
| Maximum Values Per Event | 0 | Split large batches into events of at most this many values. 0 keeps each batch whole. |
| Include Recovered Values | true | Deliver values the agent recovers after an outage |
The three selectors combine, and their union is de-duplicated. At least one must resolve to something — if all three select nothing, the subscription is refused with a message naming all three.
See Subscribe to Tag Values for the full reference.
The subscribe function's fields do not accept ((parameter)) placeholders. A subscription is resolved once when the pipeline is enabled, so there is no execution to take parameter values from.
Sample payload
The Parameters tab also holds a Sample Payload editor and a Sample Quality picker. When you run the node with Test Node in Sandbox mode, the trigger emits the sample payload as result instead of waiting for a real event. Sample Quality sets the quality the sample reports in $trigger._metadata.quality (good, uncertain or bad). The live subscription and the node's Fire Trigger action do not use the sample. When the editor is empty, a built-in sample batch is used. Write your own sample in the same shape — a batch, not a single reading:
{
"values": [
{ "path": "\\\\PISRV01\\SINUSOID", "t": "2026-09-01T08:00:00Z", "v": 42.5, "q": "good" },
{ "path": "\\\\PISRV01\\CDT158", "t": "2026-09-01T08:00:00Z", "v": 118.2, "q": "good" }
]
}
Keep the sample payload in this shape. A mock carrying one flat value would let you build downstream mappings that break on the first real event.
Settings
| Setting | Options | Default | Description |
|---|---|---|---|
| On Error | Pipeline Default / Stop Pipeline / Continue Execution | Pipeline Default | Behaviour when the node fails. |
Output data structure
Each event produces two top-level keys: _metadata and result.
Output format
{
"_metadata": {
"type": "pisystem_trigger",
"protocol": "pisystem",
"connectionId": "bf29be94-fc0a-4dc4-8e5c-092f1b74eb4b",
"functionId": "aef374c3-aa2b-454e-aabc-5657faac5950",
"valueCount": "3",
"frameType": "live",
"quality": "good"
},
"result": {
"values": [
{ "path": "\\\\PISRV01\\SINUSOID", "t": "2026-09-01T08:00:00Z", "v": 42.5, "q": "good", "uom": "m" },
{ "path": "\\\\PISRV01\\CDT158", "t": "2026-09-01T08:00:00.250Z", "v": 118.2, "q": "good" },
{ "path": "\\\\PISRV01\\VALVE01", "t": "2026-09-01T08:00:00.500Z", "v": "Open", "q": "good", "ordinal": 1, "digital": true }
]
}
}
result.values[] fields
| Field | Type | Description |
|---|---|---|
path | string | The PI point path |
t | string | The PI source timestamp, ISO 8601 UTC |
v | number | string | boolean | null | The value |
q | string | Quality — good, uncertain or bad |
uom | string | Engineering units. Present only when the point defines them. |
ordinal | number | Present only for a digital point — the underlying state number |
digital | boolean | Present and true only for a digital point |
systemState | number | Present only when v is null — the PI system-state code explaining why |
On a subscription, a digital point's value arrives as its state name ("Open"), with the raw ordinal alongside it and digital: true.
A read of the same point returns the ordinal number instead, because a read result is not scoped to the streaming dictionary that carries the state names. If a pipeline both streams and reads the same digital tag, handle the two shapes explicitly.
_metadata fields
| Field | Type | Description |
|---|---|---|
type | string | Always "pisystem_trigger" |
protocol | string | Always "pisystem" |
connectionId | string | The PI System connection profile ID |
functionId | string | The subscribe function ID |
valueCount | string | How many values are in this batch. A string, not a number. |
frameType | string | live for streamed values, backfill for recovered ones |
quality | string | Worst-of quality across the batch — good, uncertain or bad. Per-value quality stays in result.values[].q. |
backfill | string | Present and "true" only when the batch is recovered data |
outOfOrder | string | Present and "true" when values in the batch may not be in timestamp order |
valueCount, backfill and outOfOrder are strings — "3", "true" — because the event metadata is a string map. Compare them as strings ({{ $trigger._metadata.backfill === "true" }}), or convert before doing arithmetic.
Referencing in downstream nodes
$trigger.result.values— the batch (an array)$trigger.result.values[0].v— the first value only$trigger._metadata.valueCount— how many values arrived$trigger._metadata.quality— worst-of quality for the batch$trigger._metadata.backfill—"true"when this is recovered data$trigger._metadata.connectionId/$trigger._metadata.functionId— which connection and function produced it
A JavaScript node is the usual way to work with the batch:
// Keep only good live readings, and reshape for a downstream sink.
const values = $trigger.result.values;
return values
.filter((row) => row.q === "good")
.map((row) => ({
tag: row.path,
value: row.v,
at: row.t,
}));
PI data pipes do not guarantee timestamp order, and the agent deliberately does not sort — ordering costs time, and only your pipeline knows whether it needs it. When $trigger._metadata.outOfOrder is "true", sort by t yourself if order matters.
Validation rules
Node Label
- Must not be empty or whitespace-only
PI System Connection
- Must be provided and non-empty
Subscribe Function
- Must be provided and non-empty
- Must be a
pisystem.subscribefunction belonging to the selected connection
Enabled Flag
- Must be a boolean if provided
Usage examples
Stream a production line into the UNS
Key configuration
- Label:
Line 1 Tag Stream - Connection:
Production PI System - Function: Subscribe with Name Filter
LINE1.* - Enabled: true
Downstream usage: iterate $trigger.result.values in a JavaScript node to map each row to a UNS topic, then publish to the Unified Namespace. Because each event carries a batch, one execution publishes many tags.
Quality-gated calculation
Key configuration
- Label:
Reactor Calculation - Connection:
Production PI System - Function: Subscribe with an explicit Tag Paths list
Downstream usage: branch on $trigger._metadata.quality with a Condition node to skip a batch containing bad readings entirely, or filter per value on row.q inside a JavaScript node when partial data is still useful.
Live-only processing
Key configuration
- Label:
Live Alarm Watch - Function: Subscribe with Include Recovered Values left on
Downstream usage: gate on {{ $trigger._metadata.backfill !== "true" }} so recovered history does not re-trigger alarms that already fired. Leaving recovery on but filtering downstream keeps the data available for other branches.
Best practices
Subscription design
- Subscribe to what you act on. Maximum Tags exists to stop a wildcard quietly claiming the whole archive; when it binds, the error names how many matched.
- Leave Maximum Values Per Event at
0unless a downstream node genuinely needs smaller payloads. Whole batches are the efficient shape. - Prefer a name filter or AF path over a long explicit list when the tag set changes — the selectors are expanded when the subscription starts.
Designing for reliability
| Practice | Rationale |
|---|---|
Iterate result.values, never index [0] alone | Batch size varies with the stream; a mapping built on one value silently drops the rest |
Check _metadata.quality or per-value q | A batch can mix good and bad readings |
Handle outOfOrder if sequence matters | The agent does not sort, by design |
| Watch the connection's Health tab | subscriptionsNotStreaming and tagsNotStreaming name tags PI refuses to stream — future-data points are the common case |
| Use one connection per agent | The agent serves a single session; a second MaestroHub instance would displace the first |
Error handling
For critical workflows: set On Error to Stop Pipeline and alert on stopped pipelines.
For best-effort processing: set On Error to Continue Execution and make sure downstream nodes tolerate partial batches.
Enable vs. disable
- Use Enable Trigger to pause streaming without changing pipeline state.
- Disable during maintenance windows to avoid processing recovered data you do not want.
- Record why a trigger is disabled in the node's Description.
A subscription can be established successfully while specific tags deliver nothing — PI refuses to sign future-data points up to a snapshot pipe, and on a real archive those can be several percent of all points.
This is reported rather than hidden. The connection's Health tab shows streaming.subscriptionsNotStreaming as a count and connector.tagsNotStreaming as a list of paths with the reason for each. Check it before concluding a tag is missing.