Skip to main content
Version: Next

Runtime and modules

The runtime routes report the gateway's health and manage modules: what is installed, whether it runs, its log. Every module also gets a route namespace of its own under /api/v1/modules/{moduleId}, and every driver module answers the same baseline connection routes there. Background in Modules.

Health​

Method and pathRolePurpose
GET /api/v1/runtime/healthanonymous{"status": "healthy"} or {"status": "unavailable"}; signed-in callers also get eventBackbone and modules
GET /api/v1/healthanonymous{"status": "ok", "time": "<ISO-8601>"}

status says whether the internal event backbone is connected. For an authenticated caller the body adds eventBackbone (the backbone's health record) and modules, a summary per module: id, name, version, license, kind, technology, driver, state, error. Anonymous callers get only status: internal addresses, errors and what is installed are not for them.

Runtime replay over REST​

GET /api/v1/runtime/replay?from=<ISO-8601>&limit=1000 (viewer) reads the backbone's retained metadata events and value batches from from onward, merged and ordered by timelineSequence, at most limit (1 to 10000) items:

[
{ "timelineSequence": 90311, "type": "value", "timestamp": "2026-11-08T12:00:00.123+00:00",
"value": { "tagId": 17, "sequence": 120, "value": 42, "quality": "good", "qualityDetail": null, "correlationId": null, "producerModuleId": "io.scadex.opcua" } },
{ "timelineSequence": 90312, "type": "metadata", "timestamp": "…", "metadata": { "…": "the tag metadata event" } }
]

This is the raw backbone window (EventBackbone__ValueRetention, default 7 days). Time-series reads for charts use tag.replay over the socket (see Live data over /sync) or the historian routes on History. The tag value routes under /api/v1/runtime/tags are on Tags.

Modules​

Method and pathRolePurpose
GET /api/v1/runtime/modulesviewerEvery installed module and its state
GET /api/v1/runtime/modules/{id}viewerOne module
POST /api/v1/runtime/modules/{id}/startengineerStart a stopped or failed module
POST /api/v1/runtime/modules/{id}/stopengineerStop it, and keep it stopped across restarts
POST /api/v1/runtime/modules/{id}/restartengineerStop, reload the code, start
POST /api/v1/runtime/modules?replace=false&start=trueadminInstall from a zip in the multipart field file
DELETE /api/v1/runtime/modules/{id}?purgeData=falseadminRemove an installed module
PUT /api/v1/runtime/modules/{id}/log-levelengineerSet the module's log level
GET /api/v1/runtime/modules/{id}/logsengineerRead the log, newest first
GET /api/v1/runtime/modules/{id}/logs/streamengineerServer-sent events
GET /api/v1/runtime/modules/{id}/logs/downloadengineerThe kept log files as text

The module object​

{
"id": "io.scadex.opcua", "name": "OPC UA", "version": "0.1.0", "vendor": "Scadex", "license": "…", "sdkVersion": "1.3",
"requiresLicense": false, "licensedTo": null, "licenseExpiresAt": null,
"kind": "driver", "technology": "opcua", "driver": { "…": "the driver experience descriptor" }, "description": "", "capabilities": [],
"state": "running", "error": null, "endpoints": [ { "…": "per endpoint" } ], "valueConsumer": null,
"origin": "…", "enabled": true, "lockedByConfiguration": false, "removable": true,
"startedAt": "…", "stateChangedAt": "…", "restarts": 0,
"log": { "level": "Information", "warnings": 0, "errors": 0, "lastError": null }
}

kind is driver, api, consumer or similar as the module declares it, or unknown until the module has loaded. state is one of starting, stopping, running, stopped, failed, disabled, incompatible, unlicensed, unsigned, tampered; error says why for failed and the refused states. lockedByConfiguration is true for a module in Modules__Disabled. Timestamps are ISO-8601. The lifecycle is described in Modules.

Start, stop, restart​

Each answers with the module object: check state afterwards. Starting a failed module reads its folder afresh. Stopping sets enabled: false, so it stays stopped when the gateway restarts.

Install​

POST /api/v1/runtime/modules as multipart/form-data with the zip in the field file. Query: replace (default false; set true to replace an installed version of the same id) and start (default true). A missing or empty file, or one over the size limit, is 400. Response:

{ "module": { "…": "the module object" }, "replacedVersion": "0.0.9" }

Remove​

DELETE /api/v1/runtime/modules/{id}?purgeData=false. The module's connections, settings, data and tags stay, so installing it again picks up where it left off, unless purgeData=true, which deletes what the module stored itself. Tags are never deleted. Response:

{ "id": "io.scadex.historian", "purgedData": false, "tagsStillBound": 12, "restoredBuiltIn": false, "module": null }

Log level​

PUT /api/v1/runtime/modules/{id}/log-level body {"level": "debug"}: trace, debug, information (or info), warning (or warn), error, critical. Anything else is 400. Returns the module object.

Reading the log​

GET /api/v1/runtime/modules/{id}/logs?level=&search=&instance=&before=&after=&limit=200

ParameterMeaning
levelMinimum level; default everything
searchText filter
instanceOnly lines tagged with this connection id
before, afterEntry ids; pass nextBefore as before for the next page
limit1 to 1000, default 200

Response { "entries": [...], "nextBefore": 8812 | null, "level": "Information" }.

GET /api/v1/runtime/modules/{id}/logs/stream?level=&after= answers text/event-stream: a : connected comment, the backlog since after, then each line as it is written as an event: log with the entry as JSON in data and the entry id in id, and a : keepalive comment every 15 seconds.

GET /api/v1/runtime/modules/{id}/logs/download streams the kept log files, oldest first, as text/plain named <id>.log. A gateway with no log files on disk sends the last 1000 buffered lines instead.

404 {"statusCode":404,"message":"There is no module '…'."} for an unknown id on any of these.

The module route namespace​

A module's own endpoints are served under /api/v1/modules/{moduleId}/.... The gateway sets their policy, not the module: a GET needs viewer and any other method needs engineer, except endpoints the module marked as writing tag values, which operators may call. While a module is stopped, starting, stopping or failed its routes answer 503 with problem details titled Module unavailable; a module that was never loaded answers 404. See Errors.

The routes of the built-in modules: History for io.scadex.historian, Other routes for io.scadex.cron. OPC UA and MQTT are drivers and expose the baseline below plus whatever their descriptors add; see OPC UA and MQTT.

Baseline driver routes​

Core maps these for every module that implements the driver contract, so every driver has the same connection API. {m} is the module id, for example io.scadex.opcua.

Method and pathRolePurpose
GET /api/v1/modules/{m}/driverviewerThe driver experience descriptor
GET /api/v1/modules/{m}/connectionsviewerEvery connection
GET /api/v1/modules/{m}/connections/{connectionId}viewerOne connection
POST /api/v1/modules/{m}/connectionsengineerCreate a connection
PUT /api/v1/modules/{m}/connections/{connectionId}engineerUpdate a connection
DELETE /api/v1/modules/{m}/connections/{connectionId}engineerDelete a connection (204)
POST /api/v1/modules/{m}/connections/{connectionId}/testengineerTest it now
GET /api/v1/modules/{m}/connections/{connectionId}/tagsviewerThe gateway tags bound to this connection
POST /api/v1/modules/{m}/connections/{connectionId}/browseengineerBrowse the device's address space
POST /api/v1/modules/{m}/connections/{connectionId}/discoverengineerTurn browsed ids into tag proposals

Shapes​

The descriptor (GET .../driver): { "connectionLabel", "connectionLabelPlural", "connectionFields": [...], "rootBrowseId" }. connectionFields lists the fields a connection's configuration takes; each driver's page documents them.

A connection:

{ "id": "…", "name": "S7_VN_NIRO", "enabled": true, "configuration": { "…": "driver-specific" }, "createdAt": "…", "updatedAt": "…" }

Create body: name (string, required), enabled (boolean, optional), configuration (object, required). Update body: all three optional. Unknown connection ids answer 404 {"message":"Connection not found."}.

Test result: { "success": true, "status": "connected", "message": null, "latencyMs": 12.4 }; status and message are the driver's words.

Browse body: { "parentId": null, "options": {} } (both optional; parentId absent means the root). Result:

{ "status": "ok", "message": null, "continuationToken": null,
"items": [ { "id": "ns=3;s=DB1.Motor1.Speed", "label": "Speed", "kind": "variable", "hasChildren": false, "dataType": 4, "binding": { "…": "ready to use as a tag's binding" }, "metadata": { } } ] }

Discover body: { "sourceIds": ["…"], "targetPath": "Plant.Line1", "options": {} } (sourceIds required). Result: a list of proposals { "sourceId", "suggestedPath", "kind", "dataType", "binding", "metadata" }, which the caller turns into tags with POST /api/v1/tags (see Tags).

Linked tags (GET .../tags): [{ "tagId", "path", "name", "enabled", "binding" }].

Every connection also has @gateway.connection.* status tags; see Connections.