Guide

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:

config
{
  "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. A third type, calc, has no protocol: it derives measurements (and alarms on them) from other agents' measurements, and takes no commands or interlocks — see Calculated agent.

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).

01
Measurements

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 signal type (digital, discrete, or analogical), an optional conversion, and the protocol's own address fields — see the driver reference for REST, IEC 60870-5-104, IEC 61850, or TwinCAT ADS. A calculated measurement's driver holds an expression instead of an address.

conversion maps the raw value off the wire to the value Khorde stores, before anything else sees it:

  • linear — { factor, offset }, applied as value * 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:

measurement
{
  "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:

measurement
{
  "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:

measurement
{
  "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 }
      ]
    }
  }
}
Alarms

Evaluated off a measurement's value, referenced by meas as equipment::name. Three 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. The optional delay is covered below.

alarm
{
  "name": "trip_brk_10",
  "area": "brk_10",
  "type": "digital",
  "set": 1,
  "reset": 0,
  "severity": "high",
  "meas": "eqp::brk_10",
  "delay": "1s"
}

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.

alarm
{
  "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" }
  ],
  "delay": "500ms"
}

Digital and discrete alarms take an optional delay: how long the value must stay unchanged before the alarm triggers, as a duration string like 500ms or 1s. It filters out a point that chatters across a threshold. The same delay applies to activation and to return to normal, and on a discrete alarm every condition — events included — shares it. Omit it (or set it to 0s) and the alarm triggers immediately.

analog — limit alarm on an analogical measurement. Up to four conditions: high_high, high, low, and low_low, each with a value threshold, its own severity, and an optional deadband (hysteresis applied when returning to normal, default 0). Omit any limit you don't need. Analog alarms don't take a delay.

alarm
{
  "name": "temperature_limits",
  "area": "eqp",
  "type": "analog",
  "meas": "eqp::temperature",
  "conditions": {
    "high_high": { "value": 90, "severity": "high", "deadband": 2 },
    "high": { "value": 75, "severity": "medium", "deadband": 2 },
    "low": { "value": 5, "severity": "low", "deadband": 1 }
  }
}
Commands

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:

command
{
  "name": "pos.open",
  "equipment": "brk_10",
  "driver": { "type": "discrete", "value": 1, "path": "1" },
  "interlock": { "key": "brk_10_safe" }
}
Interlocks

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
{
  "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] }
        ]
      }
    }
  ]
}
02
Running the image

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:

docker-compose.yaml
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.

Next

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.

TwinCAT ADS

ADS agent — symbols on Beckhoff PLCs.

Calculated

Calculated agent — new measurements from existing ones.

Taking this past your laptop?

Talk to us