Agent configuration
The schema behind the JSON or YAML you upload to an agent — measurements, alarms, commands, and interlocks. Same shape for every protocol; only the driver block changes.
This is the reference for the config introduced in Connect a REST agent — worth a read once you're past the first measurement and alarm. One file, top-level keyed by agent name, the same whether it covers one agent or ten:
{
"my-agent": {
"type": "client",
"protocol": "rest",
"measurements": [ /* ... */ ],
"alarms": [ /* ... */ ],
"commands": [ /* ... */ ],
"interlocks": [ /* ... */ ]
}
}type is client for agents that read from equipment (REST, an IEC 60870-5-104 client) or server for agents that expose Khorde data to a field protocol (an IEC 60870-5-104 server). protocol picks the shape of every driver block below it.
For inline validation and autocomplete while you edit, point your editor at the published schema: https://schemas.khorde.io/v1/agent-config.json (or the .yaml variant).
A point read off the equipment. equipment and name together are its stable identity — written equipment::name — and that's how alarms, interlocks, and GraphQL queries all refer back to it.
type— discrete for boolean or multi-state values, analogical for a continuous reading.store— keep this measurement's history for later querying. Leave it off for values you only need live.tags— free-form strings for grouping and search; no schema meaning.driver— a signaltype(digital, discrete, or analogical), an optionalconversion, and the protocol's own address fields — see the driver reference for REST, IEC 60870-5-104, or IEC 61850.
conversion maps the raw value off the wire to the value Khorde stores, before anything else sees it:
linear—{ factor, offset }, applied asvalue * factor + offset. Scale raw counts to engineering units.table—{ table: { raw: mapped } }, an exact lookup. Anything not listed passes through unchanged.interval—{ interval: [{ from, to, value }] }, half-open[from, to)buckets. First match wins; unmatched values pass through unchanged.
A raw 0–1000 count converted to °C with linear:
{
"name": "temperature",
"equipment": "eqp",
"type": "analogical",
"store": true,
"driver": {
"type": "analogical",
"path": "14",
"conversion": { "type": "linear", "factor": 0.1, "offset": -40 }
}
}A four-position switch collapsed down to open/closed with table — raw 0 and 2 both read as closed, 1 and 3 as open:
{
"name": "status",
"equipment": "eqp",
"type": "discrete",
"store": true,
"driver": {
"type": "discrete",
"path": "16",
"conversion": {
"type": "table",
"table": { "0": 0, "1": 1, "2": 0, "3": 1 }
}
}
}A raw 0–100 tap position bucketed into low/mid/high bands with interval:
{
"name": "tap_position",
"equipment": "eqp",
"type": "discrete",
"store": true,
"driver": {
"type": "discrete",
"path": "17",
"conversion": {
"type": "interval",
"interval": [
{ "from": 0, "to": 34, "value": 0 },
{ "from": 34, "to": 67, "value": 1 },
{ "from": 67, "to": 101, "value": 2 }
]
}
}
}Evaluated off a measurement's value, referenced by meas as equipment::name. Two shapes, picked by type.
digital — one threshold in each direction: set and reset are the raw values that arm and clear it, with a single severity. Good for trip / no-trip style points.
{
"name": "trip_brk_10",
"area": "brk_10",
"type": "digital",
"set": 1,
"reset": 0,
"severity": "high",
"meas": "eqp::brk_10"
}discrete — a list of independent conditions off the same measurement, each with its own value, state (Set, Reset, or Event), and severity. Good for equipment with more than two meaningful states.
{
"name": "trip_brk_41",
"area": "brk_41",
"type": "discrete",
"meas": "eqp::brk_41",
"conditions": [
{ "id": "cond_1", "value": 1, "state": "Set", "severity": "low" },
{ "id": "cond_2", "value": 2, "state": "Reset", "severity": "medium" },
{ "id": "cond_3", "value": 3, "state": "Event", "severity": "high" }
]
}A writable operation on a piece of equipment. Like measurements, driver carries the protocol-specific address plus the value to send. interlock is optional — omit it and the command runs unconditionally for anyone with permission to send it; include it to require a condition first:
{
"name": "pos.open",
"equipment": "brk_10",
"driver": { "type": "discrete", "value": 1, "path": "1" },
"interlock": { "key": "brk_10_safe" }
}A reusable boolean condition, referenced from a command by key. The expression is a small logic tree: and, or, and not combine child nodes; eq, ne, gt, ge, lt, and le compare two operands, each either a measurement reference (equipment::name) or a plain number. ref inlines another interlock by its key, so shared logic is defined once:
{
"interlocks": [
{
"key": "brk_10_safe",
"description": "Only allow open below 100kW, or if already cleared",
"expression": {
"or": [
{ "lt": ["eqp.brk_d::power", 100] },
{ "ref": "load_cleared" }
]
}
},
{
"key": "load_cleared",
"description": "Reusable: both feeders are open",
"expression": {
"and": [
{ "eq": ["eqp::brk_20", 0] },
{ "eq": ["eqp::brk_30", 0] }
]
}
}
]
}Every agent image runs the same way: mount this config and the service account key from the control plane, publish whatever port it listens on, and give it a route to the Hub and IdP on your host if you're running locally. Only the image and its published port change per protocol:
services:
agent:
image: khorde/<agent-image>:latest
ports:
- "<port>:<port>"
volumes:
- ./config.yaml:/opt/agent/config.yaml
- ./service-user.json:/opt/agent/service-user.json
extra_hosts:
- "host.docker.internal:host-gateway"
- "auth.localhost:host-gateway"extra_hosts is only there to route the .localhost names through Docker's network — against Khorde Cloud there's nothing to route, since it's already public DNS. Each protocol page below has the concrete image name, port, and its own runtime config.yaml — the settings that talk to the field, not to Khorde.
Everything above is shared; only the driver address fields change per protocol. Pick yours:
REST
REST agent — HTTP in, HTTP out.
IEC 60870-5-104 (client)
IEC 104 client — reads a field device in.
IEC 60870-5-104 (server)
IEC 104 server — exposes SCADA data out.
IEC 61850
IEC 61850 agent — MMS objects on IEDs.