Skip to main content
Version: 1.0.4

Python in Pipelines

Two pipeline nodes run Python: Python Script (once per message) and Batch Transform (once per item, in parallel).

The transform contract

# Supported modules: system, math, json, datetime
# Globals: payload (data), message (metadata)

def transform(payload, message):
"""
:param payload: the primary data object from the previous node
:param message: the full message including metadata and context
:return: the value passed to the next node
"""
return payload
payloadWhat the previous node produced
messageThe full envelope — payload, metadata, correlation, context
return valuePassed to the next node
return NoneEnds this branch — the idiomatic filter

Batch Transform

def transform(item, message):
"""
:param item: the current item of the incoming array
:param message: the parent message context
:return: the transformed item
"""
return {**item, "celsius": round((item["fahrenheit"] - 32) * 5 / 9, 2)}

Items are processed in parallel and the array stays intact. Prefer this over Array Iterator when items are independent — Array Iterator is for when each item must traverse the rest of the graph separately.

Working with tags

def transform(payload, message):
tags = system.tag.readAll([
"Line1/Filler/Motor1/Speed",
"Line1/Filler/Motor1/Current",
"Line1/Filler/Motor1/Running",
])

if any(t["quality"] != "Good" for t in tags):
return None # do not compute from untrustworthy data

speed, current, running = (t["value"] for t in tags)
return {
"speed": speed,
"current": current,
"running": running,
"load_pct": round(current / 12.5 * 100, 1) if running else 0.0,
}

Two habits that matter: check quality, and batch reads. readAll is one round trip; a list comprehension of read is N.

Writing to a database

def transform(payload, message):
system.db.runNamedQuery("insert_production", {
"ts": payload["ts"],
"line": payload["line"],
"units": payload["units"],
})
return payload

Prefer routing writes through a SQL Query node rather than doing them in Python: the node's writes are covered by store-and-forward, so an unreachable database buffers and replays instead of failing.

Error handling

An uncaught exception fails the node; the branch stops and the error is logged against the execution. Other branches continue.

Handle it explicitly when you want a different outcome:

def transform(payload, message):
try:
rows = system.db.runNamedQuery("lookup_recipe", {"sku": payload["sku"]})
except Exception as e:
return {"error": str(e), "sku": payload.get("sku")} # route to a notify branch

if not rows:
return None # unknown SKU: drop quietly
return {**payload, "recipe": rows[0]}

Capability scopes

Script calls are gated by capability scopes, enforced at the worker. A script cannot reach an API its execution context is not scoped for.

CallRequired scope
system.tag.readtag:read
system.tag.writetag:write
system.db.runNamedQuerydb:read
system.db.runQuery, callProc, any transaction calldb:write
system.hist.getTrendshist:read
system.vars.get / keys / listvars:read
system.vars.set / delete / incrvars:write
system.files.readfile:read
system.files.savefile:write
system.message.sendscript:execute

Note that runQuery requires db:write while runNamedQuery requires only db:read — arbitrary SQL may write, a named query cannot. That is a concrete reason to prefer named queries: they let you run scripts at a lower privilege.

The gate is fail-safe: an unmapped method on a sensitive namespace falls back to that namespace's strong scope, so a newly-added API is gated by default rather than silently open.

Timeouts

The Python node's default timeout is 60 seconds. Exceeding it fails the node.

If work legitimately takes minutes, it does not belong in a pipeline node driven by a fast trigger. Move it to a Gateway script, or split it across a queue.

Performance

SymptomFix
High CPU on the Python workerFilter with RBE before the Python node; not every message needs Python
Slow tag accessOne readAll instead of a loop of read
Slow database accessOne query returning many rows, not many queries returning one
Memory growthStream with Array Iterator instead of materialising large arrays
Pipeline falls behind its triggerLengthen the interval, or split the work across pipelines

Set-based work belongs in SQL. Python is for the logic SQL cannot express, not for iterating rows.

Development workflow

  1. Write the transform in the node editor; the linter flags syntax errors as you type.
  2. Attach a Debug node downstream and watch the output.
  3. Use trigger-node to run just this node with its current input.
  4. Read the execution log for failures.

Execution & debugging

Next

Gateway scripts