Config schema

Each diagnostic is one YAML file in .scadable/diagnostics/. The file name is cosmetic — only the id: field is wired into codegen and cloud ingestion.

Minimal example

# .scadable/diagnostics/motor-health.yaml
id: motor_health
type: function
name: "Motor Bearing Health"
description: "Reads vibration RMS + bearing temp; fails if either exceeds baseline."
timeout_secs: 10
run_after_ota: true
function:
  symbol: motor_health

Five fields are mandatory: id, type, name, description, and the per-type config block matching type: (here function: with a symbol:). Everything else has a default.

Field reference

FieldRequiredTypeDefaultDescription
idYesstringStable identifier. Regex ^[a-z][a-z0-9_]{1,62}$. Used as the dashboard ID and as the correlation key from cloud trigger to chip-side dispatch. Renaming this is a breaking change — see "Rename semantics" below.
typeYesenumfunction is the only value v1 chips can run. Future types (api_call, sensor_read, mqtt_check, etc.) are reserved and accepted by the cloud, but a v1 chip returns TEST_RESULT_TYPE_NOT_SUPPORTED if asked to run them.
nameYesstringOperator-facing label. Truncated to 60 chars in dashboard rows. Hard cap 80 chars.
descriptionYesstringMulti-line OK. Rendered as Markdown in the dashboard drawer. Cap 500 chars.
timeout_secsNoint30Wall-clock cap on the test function. Library returns TEST_RESULT_TIMEOUT if exceeded. Range 1–300.
run_after_otaNoboolfalseWhen true, the cloud auto-runs this diagnostic on every device after every successful OTA apply. The release status flips to verifying while it runs. Auto-rollback fires if it fails. See Post-OTA verification.
required_sensorsNolist[string][]Cosmetic in v1. List of register names from .scadable/digital-twin/ this test reads. Future "this test cannot run on this gateway class" UI will use this.
<type>:YesmappingPer-type config nested under a key matching type:. For function, the only required field is symbol: (the C identifier the customer registers).

Three realistic diagnostics

# .scadable/diagnostics/motor-health.yaml
id: motor_health
type: function
name: "Motor Bearing Health"
description: |
  Reads vibration RMS + bearing temperature, compares against the baseline
  learned during commissioning, and fails if either exceeds the configured
  threshold.
timeout_secs: 10
run_after_ota: true
required_sensors:
  - vibration_rms_g
  - bearing_temp_c
function:
  symbol: motor_health
# .scadable/diagnostics/thermal-sweep.yaml
id: thermal_sweep
type: function
name: "Thermal Sweep (slow)"
description: |
  Steps the heater through 25 to 80 C in 5 C increments and validates the
  thermistor tracks within 2 C. Takes about 45 s — do NOT enable
  run_after_ota for this one, or every deploy stalls a minute.
timeout_secs: 120
run_after_ota: false
function:
  symbol: thermal_sweep
# .scadable/diagnostics/network-roundtrip.yaml
id: network_roundtrip
type: function
name: "Network Round-trip"
description: "Pings the broker over MQTT and validates RTT under 500 ms."
timeout_secs: 5
run_after_ota: true
function:
  symbol: network_roundtrip

Validation rules

The build pipeline validates every YAML at codegen time and the cloud re-validates on ingestion. Failures are surfaced in the release detail page in the dashboard.

RuleFailure code
id matches ^[a-z][a-z0-9_]{1,62}$SCAD_E1001: invalid diagnostic id
name length 1–80SCAD_E1002: name too long
description length 1–500, UTF-8SCAD_E1003: description too long
timeout_secs integer in 1–300SCAD_E1004: timeout_secs out of range
required_sensors[N] matches a register declared in .scadable/digital-twin/ (warning only if folder absent)SCAD_E1005: required_sensors[N] references unknown register "X"
type is a value v1 chips can runSCAD_E1006: type not supported in this libscadable version, requires v0.X.Y+

SCAD_E1006 is the one to watch as you adopt future diagnostic types — see "Future types" below.

How changes propagate

The flow from git commit to "the cloud knows about your diagnostic":

git push --tags
    │
    ▼
GitHub webhook fires (push event with vX.Y.Z tag)
    │
    ▼
SCADABLE GitHub App clones at the tag SHA
    │
    ▼
Build pipeline parses .scadable/diagnostics/*.yaml
    │
    ▼
Codegen emits scadable_generated.c (the registration calls) and links into firmware
    │
    ▼
Same parsed catalog is POSTed to the cloud's diagnostic-catalog ingestion endpoint
    │
    ▼
service-app upserts diagnostic_definitions for this release
    │
    ▼
Cloud now knows your diagnostics; dashboard shows them on the next gateway view

The ingestion is keyed on (code_project_id, id). New entries insert. Renames or deletions soft-delete the old row but preserve its run history forever. The release detail page shows orphaned definitions with a "(removed)" pill so you can still inspect old failures.

Rename semantics

Changing id: foo to id: bar is an orphan-and-new operation, not a rename. The cloud cannot disambiguate "I renamed foo to bar" from "I deleted foo and added a brand-new bar that happens to read the same sensors."

When you push the rename:

  • The new bar row inserts. Its run history starts from this release.
  • The old foo row is soft-deleted (its deleted_at is set). Its history is preserved indefinitely.
  • The dashboard shows foo runs in history with a "(removed)" pill; bar shows no history yet.

If you need a true rename later, file an issue — a replaces: foo field is on the roadmap. Until then, only rename when you're OK losing the per-test history continuity.

Future types

The type: field exists so the framework can grow without breaking existing chips. v1 only ships type: function, but the cloud already accepts other type values:

# .scadable/diagnostics/weather-api-check.yaml — NOT runnable on v1 chips
id: weather_api_check
type: api_call
name: "Weather API reachable"
description: "Hits the upstream weather API; fails if status != 200."
timeout_secs: 5
run_after_ota: false
api_call:
  url: "https://api.openweathermap.org/data/2.5/weather"
  method: GET
  expect_status: 200
  expect_body_contains: "main"

If you add this to your repo today, the cloud accepts the row and the dashboard shows the diagnostic in your gateway's tab — but with a badge that says "This diagnostic type requires a newer firmware (v0.X.Y+). Upgrade your gateway to enable." Any cloud-triggered run yields immediate TEST_RESULT_TYPE_NOT_SUPPORTED with no firmware crash.

This forward-compatibility means you can declare api_call diagnostics now and have them spring to life the moment you bump libscadable past the version that adds the type.

Where this lives in the customer repo

.scadable/
├── config.toml
├── digital-twin/
│   └── *.toml
├── diagnostics/
│   ├── motor-health.yaml
│   ├── thermal-sweep.yaml
│   └── network-roundtrip.yaml
└── .schema/   (auto-generated by `scadable init`; .gitignored)

The .schema/ folder gets you autocomplete in editors that respect # yaml-language-server: $schema=... comments — handy but not required.

See also