Skip to main content
Version: 1.0.8

Gateway Scripts

A Gateway script is a Python module in a project's Scripts → Gateway folder that exposes one or more entry points to the rest of the system. It runs server-side, so it is where transactional and long-running work belongs.

Why server-side​

Client scriptGateway script
Runs in the operator's browserRuns on the gateway
A disconnect abandons the work mid-flightA disconnect can only lose the reply
An open transaction can be strandedThe transaction completes or rolls back regardless
Bounded by the browser's sessionBounded by the server's timeout

If a script opens a database transaction, writes several tables, or takes more than a moment, it belongs here.

The @gateway decorator​

Only functions marked @gateway are callable. Everything else in the module is a private helper — default-deny, and it is a security boundary, not a convention.

Scripts/Gateway/reports.py
@gateway
def generateShiftReport(inputs):
shift = inputs["shift"]
rows = system.db.runNamedQuery("production_by_shift", {
"start": inputs["start"],
"end": inputs["end"],
})
csv = _to_csv(rows) # private helper — NOT callable
url = system.files.save(f"reports/{shift}.csv", csv) # Python: returns the URL
return {"url": url, "rows": len(rows)}


@gateway(timeout_ms=120000)
def rebuildAggregates(inputs):
...


def _to_csv(rows): # no decorator ⇒ not an entry point
...
  • Bare @gateway marks an entry point.
  • Parameterised @gateway(timeout_ms=...) is accepted and marks an entry point the same way, but the option is not applied: every entry point runs under the gateway's own budget of up to 120 s, and the caller decides how long it waits (see below).
  • gateway is injected by the runtime — no import needed.
  • Exports are discovered by a static AST scan, without executing the module, which is what drives the Designer's IntelliSense.

Calling one​

From a client script or event action
const res = await system.script.runOnGateway(
"reports.generateShiftReport",
{shift: "A", start: "2026-08-01T06:00:00Z", end: "2026-08-01T14:00:00Z"},
{idempotencyKey: "shift-A-2026-08-01", timeoutMs: 60000},
);
context.ui.notify({ message: `Report ready: ${res.rows} rows`, severity: "success" });

The handle is "<file>.<function>". The project is always the caller's own, resolved server-side — an opts.projectId is ignored.

The browser stops waiting after timeoutMs — 15 s by default, so set it for anything slower.

From a pipeline Python node
res = system.script.runOnGateway(
"reports.generateShiftReport",
{"shift": "A", "start": start, "end": end},
idempotencyKey="shift-A-2026-08-01",
)

New in 1.0.8 A pipeline Python node can call an entry point too, with the options as keyword arguments. It waits for the node's remaining budget, or 35 s. The entry point runs in a separate worker under the calling script's own scopes and write level, so it holds a second worker while the node waits.

Not from inside a Gateway entry point

A Gateway entry point that calls system.script.runOnGateway is refused: it is already server-side, and a function calling back into the gateway would hold one worker per level. Import the shared function into the module instead, or move it to a module both entry points import.

Idempotency​

{idempotencyKey: "shift-A-2026-08-01", idempotencyTTL: 7200}

With a key set, repeated calls within the TTL (default 7200 s / 2 h) return the cached result instead of re-running. A double-clicked button produces one report, not two.

Choose a key that identifies the work, not the click: shift-A-2026-08-01 is right; a random value defeats the purpose.

Transactions​

The reason Gateway scripts exist:

@gateway
def releaseBatch(inputs):
with system.db.transaction(connectionName="MES",
isolation="ReadCommitted",
timeoutMs=30000) as tx:
tx.execute("UPDATE batches SET status='released' WHERE id=?", [inputs["batchId"]])
tx.execute("INSERT INTO batch_audit (batch_id, action, actor) VALUES (?,?,?)",
[inputs["batchId"], "release", inputs["actor"]])
rows = tx.runQuery("SELECT * FROM batches WHERE id=?", [inputs["batchId"]])
return {"batch": rows[0]}

The with block commits when it ends cleanly and rolls back if anything inside it raises. Combined with the server-side timeoutMs, an abandoned transaction cannot hold locks indefinitely. In Python the options are keyword arguments; there is no callback form. → Transactions

Patterns​

Validated write, gated server-side

@gateway
def applyRecipe(inputs):
recipe = system.db.runNamedQuery("recipe_by_id", {"id": inputs["recipeId"]})
if not recipe:
return {"ok": False, "error": "unknown recipe"}

r = recipe[0]
if not (0 <= r["speed"] <= 3000):
return {"ok": False, "error": "recipe out of range"}

system.tag.writeAll(
["Line1/Filler/SpeedSP", "Line1/Filler/TempSP"],
[r["speed"], r["temperature"]],
)
return {"ok": True, "applied": r["name"]}

The client cannot bypass the validation because the client never writes the tags.

Report generation with delivery

@gateway
def monthlyReport(inputs):
rows = system.db.runNamedQuery("monthly_summary", inputs)
url = system.files.save(f"reports/{inputs['month']}.csv", _to_csv(rows)) # Python: the URL
system.message.send("report.ready", {"url": url}, {"scope": "project"})
return {"url": url}

Long work, a generated artifact, and a message to every screen in the project with an On message handler for report.ready. The message stays in the script's own project; there is no option to send it elsewhere. → Message bus

Security​

  • Default-deny exports. An undecorated function can never be invoked from a client, even by exact name.
  • Validate inputs. They arrive from a browser. Treat them as untrusted.
  • Capability scopes still apply — see Python nodes.
  • Do the authorisation here. A Gateway script is the right place to enforce "only a supervisor may release a batch", because the client cannot skip it.

Client scripts vs Gateway scripts​

Use a client scriptUse a Gateway script
Screen interaction, validation, UI stateDatabase transactions
Reading a tag to decide what to showMulti-step writes that must be atomic
A quick single tag writeWork that takes more than a moment
Anything purely presentationalAnything the operator must not be able to bypass

Next​

→ Client scripts