Google ai agent (ProgramObject)

A_I_agent.bog (69.0 KB)

USER MANUAL: Niagara ProgramObject “Gemini Agent”

  1. What it is :white_check_mark:
    This ProgramObject sends a single request to Google Gemini when you press updateNow and receives a text answer.
    It can automatically build a “station snapshot” (points/folders) and, optionally, add history data (BQL).
    Important: the read-only version does NOT write values to points and does NOT execute /set.

  2. What happens on updateNow (request flow)

  1. Checks enable.
  2. If the model list has not been loaded yet, it loads models first and then automatically continues with the request.
  3. Collects:
    • STATION_SNAPSHOT_JSON from pointsOrds
    • STATION_HISTORY_JSON from historyBqlOrds (if provided)
  4. Builds the request:
    • systemInstruction = instructions (except for gemma models; see below)
    • contents = (chat memory, if enabled) + current user text (prompt + snapshot + history)
  5. Sends HTTP POST to the generateContent endpoint.
  6. Writes results:
    • outText = model answer (or ERR: …)
    • answerOk = true/false
    • statusMessage = short status + tokens (if Gemini returns usage)

The block sends requests only on updateNow. It does not call the API in the background by itself.

  1. Quick start in 5 minutes :high_voltage:
    Step 1. Create the ProgramObject and add the slots (see section 4).
    Step 2. Put your Gemini API key into apiKey.
    Step 3. Press refreshModelsNow (or press updateNow; it can fetch models automatically).
    Step 4. Pick a model in modelPick (or type it into model).
    Step 5. Fill pointsOrds (folders/points, one ORD per line).
    Step 6. Write your task in prompt (if empty, it uses “Analyze station data.”).
    Step 7. Pulse updateNow = true.
    Read the answer in outText and check statusMessage.

  2. Slots: what they do (core)

Control

  • enable (BBoolean). false = block does nothing, resets buttons, cancels pending work.
  • updateNow (BBoolean, one-shot). Sends one request. Returns to false automatically.
  • refreshModelsNow (BBoolean, one-shot). Refreshes the model list.
  • restartNow (BBoolean, one-shot). Soft restart: cancels pending work, clears 429 pause, resyncs model selection.
  • clearMemoryNow (BBoolean, one-shot). Clears chat memory (chatMemory).

Access

  • apiKey (BString). Gemini API key.

Instructions and prompt

  • instructions (BString). “System” instructions (systemInstruction). For gemma models these are injected into the user text.
  • prompt (BString). Task text. If empty, “Analyze station data.” is used.
  • maxOutputTokens (BInteger). Max output tokens. Default 400. Range: 50…8192.

Model

  • modelPick (BDynamicEnum). Dropdown of models (filled after refreshModelsNow).
  • model (BString). Model id (e.g., gemini-1.5-pro). If you paste “models/…”, the prefix is removed.
  • modelsList (BStatusString). Text list of models (after refresh).
  • modelsLastUpdate (BAbsTime). Timestamp of last model refresh.

Chat memory

  • memoryModePick (BDynamicEnum). What to store in memory (see section 7).
  • memoryMaxMessages (BInteger). How many last messages to keep (0…200). Default 12.
  • chatMemory (BStatusString). Memory storage as text (usually view-only).

Outputs

  • outText (BStatusString). Model response or “ERR: …”.
  • answerOk (BBoolean). true only after a successful response.
  • statusMessage (BStatusString). State: loading/sending/success/ERR/paused + tokens.
  1. Station snapshot: pointsOrds + limits :pushpin:
    pointsOrds (BString): list of ORDs, one per line.
    You can pass:
  • a point ORD (often ends with /out)
  • a folder ORD (the block walks it recursively and collects points)

Examples:

  • station:|slot:/Drivers/BacnetNetwork/AHU_1/points
  • station:|slot:/Drivers/BacnetNetwork/AHU_1/points/Sensors/SupplyTemp/out
  • station:|slot:/Drivers/OpcUaNetwork/Line1/points

Optional human label prefix
You may put a label before “station:”

  • Room12_station:|slot:/Drivers/OpcUaNetwork/SB_A1/points
    “Room12” is attached to the produced names and helps separate similar devices in different areas.

Limits and filters (auto-created after first run)

  • snapshotMaxPoints (BInteger). Default 2500, range 10…5000.
  • snapshotMaxDepth (BInteger). Default 25, range 1…50.
  • ignorePointNames (BString). Blacklist of exact point names, comma-separated: Temp, Hum, CO2.
  • ignoreFolderNames (BString). Blacklist of exact folder names, comma-separated: Diagnostics, Alarms.

Snapshot JSON format
The request includes JSON like:
{“groups”:[{“folders”:[…],“points”:[ “…”, “…” ]}, …]}

Each point becomes a line like:
“<path/prefix><Identifier - >PointName = value unit”
Numbers use 2 decimals, text/enums are cleaned from newlines, long strings are truncated.

Optional: groupFormat
If you add slot groupFormat (BString) and set a BFormat string, the block groups points using that format.
If you do not know BFormat, skip it; default grouping is usually fine.

  1. History: historyBqlOrds :chart_increasing:
    History is added only if historyBqlOrds is not empty.
    Format: one BQL-ORD per line.

Example:

  • Room201_history:/PS/M12K4VAS01$20Sensor_TE_201_1?period=last7days|bql:select timestamp, value

You can also add a label prefix before “history:”

  • Room201_history:history:/PS/Temp?period=last24hours|bql:select timestamp, value

Parameters (auto-created after first run)

  • historyMaxRows (BInteger). Default 100, range 1…50000. Sends last N rows.
  • historyMaxSeries (BInteger). Default 20, range 0…100. How many series to include.

Soft JSON size limit: 180000 chars. If exceeded, the block stops adding more series.

What is sent per series
For each series the block tries to resolve a table (BITable) and sends:

  • ord (string)
  • name (series name derived from ORD + prefix)
  • columns (column names)
  • rows (last N rows)
  • min/max/avg/last for column “value” (if numeric)
  • count (rows actually sent)
    If the ORD does not resolve to a table, the series gets an “error” field (e.g., not_table).
  1. Chat memory: how to use :brain:
    Memory works like this:
  • Before sending, the block adds previous messages from chatMemory into contents.
  • After a successful response, it updates chatMemory.

Enable/disable

  • memoryMaxMessages = 0 → memory off.
  • memoryMaxMessages > 0 → memory on (default 12).

memoryModePick (what to store)
0) prompt_only — store prompt only

  1. snapshot_only — store station snapshot only
  2. answers_only — store model answers only
  3. prompt_snapshot — prompt + snapshot
  4. prompt_answers — prompt + answers
  5. snapshot_answers — snapshot + answers
  6. prompt_snapshot_answers — everything

Clear

  • clearMemoryNow = true → chatMemory becomes empty.

Note
Snapshot and history can be large. If you store snapshot in memory, it grows quickly and you hit token/quota limits sooner.
For long-term use, prompt_answers or answers_only is usually enough.

  1. Models: refresh and selection :robot:
  • refreshModelsNow updates modelsList and fills modelPick.
  • updateNow can also fetch models automatically if the list is empty.
  • If model is empty or not found, the block uses the first model from the list.
  • For gemma models the block does not use systemInstruction; it prepends instructions into the user text instead.
  1. Status messages
    statusMessage examples:
  • “Models: loading…” — fetching model list
  • “Models: updated” — model list refreshed
  • “Analyze: preparing…” — building snapshot/history/request body
  • “Analyze: sending…” — request sent, waiting for response
  • “Analyze: success | Tokens: …” — success + token usage (if returned)
  • “Analyze ERR: …” — error
  • “Analyze paused (429).” — paused due to rate limit/quota

outText:

  • model answer on success
  • “ERR: …” on error
  • “Pause (429) until ms=…” while paused (Unix time in ms)
  1. Troubleshooting :warning:

apiKey empty

  • Fill apiKey. Ensure no spaces/newlines inside the key.

Models ERR: …

  • Often invalid key or no API access.
  • Check that Gemini API is enabled in your Google project and that quota is available.

Analyze ERR: model empty

  • Pick a model: refreshModelsNow → modelPick, or type model manually.

ERR: HTTP 401/403

  • Invalid key, API disabled, no billing/quota, or access restrictions (project/region).

ERR: HTTP 429 (quota/rate limit) :white_check_mark:

  • You hit a rate limit or your quota is exhausted.
  • The block pauses automatically:
    • ~2 minutes for “normal” 429 (rate limit)
    • ~30 minutes for quota/RESOURCE_EXHAUSTED
  • To recover:
    1. restartNow (clears local pause) and retry later
    2. reduce load: fewer updateNow presses, fewer points/history, lower maxOutputTokens
    3. fix the cause in Google console (quota/billing/limits)

PARSE_ERR

  • Gemini returned an unexpected JSON shape. outText contains RAW excerpt. Often fixed by switching model or refreshing models.

history error: not_table

  • Your historyBqlOrds does not resolve to a table. Verify the BQL-ORD; it must return BITable.

Empty station snapshot

  • pointsOrds are wrong/unresolved or not pointing to /points or a point.
  • Verify each ORD in Workbench (Resolve/Go To).
  • Remove extra characters at the start of the line. The block trims trailing “)” but does not fix leading junk.
  1. Load and safety :locked:
  • apiKey is stored in a slot. Do not show it on PX pages without protection.
  • Bigger pointsOrds and higher historyMaxRows produce larger requests and hit limits faster.
  • Typical “daily analysis” settings:
    • snapshotMaxPoints 200…800
    • historyMaxRows 50…200
    • memoryMaxMessages 6…20, mode answers_only or prompt_answers
  1. Where to improve next :compass:
  • Show 429 pause time as station time (not Unix ms).
  • Add internal throttling for updateNow to avoid button “bounce”.
  • Add snapshot/history caching (do not resend if unchanged).
  • Add “quick/full analysis” profiles (a single switch that sets the limits).

Thank you for sharing this! I’ll give this a whirl later on this week!

Sounds awesome! I’ll also be giving this a whirle

Hi colleagues!

I am answering your questions about using AI agents. The idea came from my work with n8n — we still use this software successfully at some sites.

For large stations, split the tasks. One AI agent controls ventilation, another cooling, and a third heating. A master agent oversees them and collects all the data. Moving forward, this setup will allow us to fully automate entire complexes and scale our systems easily.

These agents excel at catching anomalies. Chillers, for example, have strict alarm limits for pressure or temperature. The AI agent spots worrying trends early, right when the metrics start creeping up or down. In the long run, this will help us prevent breakdowns long before standard alarms even trigger.

Writing requests for the AI is easy. Open ChatGPT or Google Gemini and type: “I want to create a request for a chiller. The AI agent needs to track anomalies and monitor readings. The data comes in JSON format.” Then just paste a sample of your telemetry. It is that simple.

Let’s share our ideas! We could build a shared library to store all our working modules and solutions. Pooling our experience will drastically speed up the development of new engineering systems in the future.


A module for a Tridium Niagara 4.15 station. Once installed, the station
itself becomes a server for AI agents (ZCode, Claude, Cursor — anything
that speaks MCP). The agent connects over HTTPS and works with the
station through ordinary requests: reads values, builds equipment,
edits logic. Nothing is installed on the agent’s computer except a
connection config. No cloud — everything stays inside your network.

What it can do

Reading:

  • value and status of any point (readPoint); point history/trends over
    a period (readHistory);
  • station tree: folders, components, slots (listChildren, getSlots,
    inspectComponent);
  • alarms: active and historical (getActiveAlarms, getAlarmHistory);
  • BQL queries — “all points of this type” etc. (bqlQuery).

Writing (when write mode is enabled):

  • change a point value or a slot (writePoint, setSlot);
  • create/remove components and folders (createComponent,
    removeComponent);
  • build logic: program blocks, links, ready-made diagrams
    (buildLogic, smartLink, layoutLogic);
  • assign a point to equipment, create equipment/space records;
  • save the station (commitStation).

Security:

  • access is by a long bearer token only;
  • writes run as a dedicated Niagara user (e.g. mcpoperator) — under
    its permissions and with an audit trail;
  • you can give an agent read-only access simply by leaving write mode
    off.

Limitations:

  • the module sees only the station it is installed on;
  • points on OTHER stations — only if they are visible through
    NiagaraNetwork (virtual points); that path depends on version and
    setup and needs to be verified separately.

Installation

You need: the dist/niagaramcp-niagara4.15/ kit, Workbench,
access to the station.

Step 1. Trust the certificate (once).
Workbench → Tools → Certificate Management → User Trust Store tab →
Import → pick niagaramcp.pem from the kit.

Step 2. The module.
Stop the station → put niagaramcp-rt.jar into the station’s modules
folder (keep the old jar as .bak for rollback) → start the station.

Step 3. The service.
Open the niagaramcp palette → drag McpPlatformService into the
station’s Services folder.

Step 4. The token.
In the service properties: Legacy Bearer Enabled = true, Api
Token
= a random secret of 32+ characters. This is the agent’s key:
whoever holds it controls the station — treat it like an admin
password.
For writing, additionally: Mcp Write User = a Niagara username
(e.g. mcpoperator; it must exist on the station). Its permissions are
granted by the fixWriteUser command or manually by the operator.

Step 5. HTTPS.
In the service, set Mcp Https Cert: the station certificate (alias)
and its password. Default port is 8443.

Step 6. Verify and save.
Save the station. Quick check — open
https://station-host:8443/niagaramcp/health in a browser (a
self-signed-certificate warning is normal): it should return
"status":"ok" and the version.

Upgrading the module is steps 1–2 again (trust re-import — only if the
release notes say the signing key changed).

Connecting an agent

On the agent’s machine, once:

python connect_station.py station-host --token-stdin

(type the token when prompted). The script pins the station
certificate, checks the connection and prints a ready config block.
Paste the block into your agent’s MCP settings and restart it.

Working with it

Just talk to the agent in plain language:

  • “check the station” → the agent calls health: version, write mode,
    tool count;
  • “show the CRAH 1-1 return temperature” → readPoint;
  • “history of this point for the last day, any gaps?” → readHistory;
  • “active alarms” → getActiveAlarms;
  • “create a NumericPoint in Logic and write 21.5” → createComponent
    • writePoint;
  • “build fan logic from these sensors” → buildLogic.

Troubleshooting

Symptom Cause Fix
niagaramcp palette empty module not loaded step 1 done? jar in modules? restart Workbench
Agent: connection refused service down / port McpPlatformService status, step 5, port 8443
Agent: 401 wrong token check Api Token (step 4)
Agent: certificate error station cert not pinned run connect_station.py again
Writes fail (-32011) mcpWriteUser not set step 4: does the user exist? permissions granted?
niagaramcp-niagara4.15.zip (345.5 KB)


I partially used code/ideas from the repository GitHub - mefodiytr/niagaramcp: Native Niagara Framework module exposing the Model Context Protocol — gives AI agents structured access to BMS points, alarms, and history MCP server as a Baja module for Tridium Niagara — runs inside the station, speaks HTTP/SSE on /mcp · GitHub