Skip to main content

Rule Configuration

An externally-evaluated rule carries an evaluation block that answers three separate questions.

FieldQuestionInterpreted by dakkio?
inputsWhat to readShape only — validated, never interpreted
parametersHow to judgeNo. Completely opaque
kindWhich computationNo. A routing label for your evaluator

Keeping these apart is what lets one evaluator workflow serve many rule shapes without dakkio learning anything about your domain.

Creating a rule

Requires an admin key — integration keys deliberately cannot create or modify rules.

curl -X POST https://api.dakkio.io/api/alerts \
-H "X-API-Key: dakkio_a_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"bucketId": "69e75875049cfa22d51464ae",
"dataSourceId": "6a04b41c049cfa22d51464d1",
"name": "Weight below minimum - Scale 3",
"naturalLanguageQuery": "weight drops below 20 kg",
"evaluation": {
"mode": "external",
"kind": "threshold",
"intervalSeconds": 300,
"windowSeconds": 900,
"inputs": [
{ "key": "v", "field": "weight" }
],
"parameters": { "operator": "<", "threshold": 20 }
}
}'

Omit the evaluation block entirely and you get a plain rule that is never returned as due.

The evaluation block

{
mode: 'external'; // only 'external' is implemented
kind?: string; // opaque routing label
intervalSeconds: number; // how often the rule becomes due (min 60)
windowSeconds: number; // observation window (min 60)
inputs: EvaluationInput[]; // 1–10 series to read
parameters: Record<string, number | string | boolean>;
dwellSeconds?: number; // defaults to windowSeconds
}

Inputs — what the rule reads

This is the part that differs most between rules. A threshold rule reads one field; a band rule reads two and compares them; a weight rule reads a different field on a different cadence.

{
key: string; // your evaluator refers to the series by this name
field: string; // must be declared on the data source
dataSourceId?: string; // defaults to the rule's own
aggregation?: 'avg' | 'sum' | 'min' | 'max' | 'count';
groupBy?: 'minute' | 'hour' | 'day' | 'week' | 'month';
windowSeconds?: number; // overrides the rule window for this input
}

The vocabulary is deliberately identical to the data query endpoint, so an input becomes a query with no translation layer.

Validation catches mistakes at creation, not at 3am

dakkio verifies that each dataSourceId exists in the rule's bucket and that field is declared in that data source's schema. A rule that your evaluator would fail to execute cannot be created in the first place, and the error tells you which fields are available.

aggregation and groupBy travel together

Supplying one without the other is rejected. An aggregate with no bucket size, or a bucket size with no aggregate, is almost always a mistake, and guessing which you meant would produce silently wrong data.

Parameters — how the rule judges

A flat map of numbers, strings and booleans. dakkio validates that shape and nothing more — it never reads the values, never compares them, never derives anything from them.

{ "bandMin": 33, "bandMax": 36, "minExtSpread": 8 }

Kind — which computation

An opaque label your evaluator switches on:

switch (rule.kind) {
case 'threshold': return evaluateThreshold(rule);
case 'thermal-band': return evaluateThermalBand(rule);
default: return { outcome: 'inconclusive' };
}

Defaulting unknown kinds to inconclusive means adding a rule type in dakkio can never make an old evaluator produce a wrong verdict — it simply holds state until you implement the branch.

Worked examples

Simple threshold — one input

{
"mode": "external",
"kind": "threshold",
"intervalSeconds": 300,
"windowSeconds": 900,
"inputs": [{ "key": "v", "field": "internalTemp" }],
"parameters": { "operator": ">", "threshold": 30 }
}

No aggregation, so raw points. The evaluator compares the latest value.

Thermal band — two series compared

The rule external evaluation exists for.

{
"mode": "external",
"kind": "thermal-band",
"intervalSeconds": 3600,
"windowSeconds": 172800,
"inputs": [
{ "key": "int", "field": "tempInt", "aggregation": "avg", "groupBy": "hour" },
{ "key": "ext", "field": "tempExt", "aggregation": "avg", "groupBy": "hour" }
],
"parameters": { "bandMin": 33, "bandMax": 36, "minExtSpread": 8 }
}

Two series, hourly averages, 48-hour window. The evaluator computes the spread of each, gates on the external spread, and checks whether the internal series held the band.

Weight drop — different field, different cadence

{
"mode": "external",
"kind": "weight-drop",
"intervalSeconds": 86400,
"windowSeconds": 604800,
"inputs": [
{ "key": "w", "field": "weight", "aggregation": "avg", "groupBy": "day" }
],
"parameters": { "dropPercent": 15 },
"dwellSeconds": 172800
}

Daily averages over a week, checked once a day.

Predicate strictness matters

Expressed as "held the band across the whole window", a single out-of-band sample breaks the condition and it cannot recover until that sample ages out — the window itself provides the guarantee.

Expressed as "inside the band for 90% of the window", the count moves by one each tick and can cross the threshold repeatedly. The clear-dwell covers both, but the strict form is preferred.

Updating a rule

PATCH /api/alerts/:id replaces the whole evaluation block. Runtime state — the current state, lastEvaluatedAt and conditionClearedAt — is preserved, so reconfiguring a rule never silently re-fires an already-firing one or clears an in-progress dwell.

Field reference

FieldConstraintNotes
mode"external"internal is reserved and currently rejected
kind≤ 64 charsOptional but strongly recommended
intervalSeconds60 – 86400How often the rule becomes due
windowSeconds60 – 31536000Observation window
inputs1 – 10 entriesKeys must be unique within a rule
inputs[].keystarts with a letterLetters, digits, underscores
parametersflat scalarsOpaque
dwellSeconds≥ 0Defaults to windowSeconds