Rule Configuration
An externally-evaluated rule carries an evaluation block that answers three separate questions.
| Field | Question | Interpreted by dakkio? |
|---|---|---|
inputs | What to read | Shape only — validated, never interpreted |
parameters | How to judge | No. Completely opaque |
kind | Which computation | No. 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.
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.
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.
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
| Field | Constraint | Notes |
|---|---|---|
mode | "external" | internal is reserved and currently rejected |
kind | ≤ 64 chars | Optional but strongly recommended |
intervalSeconds | 60 – 86400 | How often the rule becomes due |
windowSeconds | 60 – 31536000 | Observation window |
inputs | 1 – 10 entries | Keys must be unique within a rule |
inputs[].key | starts with a letter | Letters, digits, underscores |
parameters | flat scalars | Opaque |
dwellSeconds | ≥ 0 | Defaults to windowSeconds |