Flows & Runs
Available
- Flow CRUD
- Scheduling (
scheduleMinutes) - Asynchronous parallel runs (adapter or generated data)
- Run history
- Alerts and reports (
settings.alerts,settings.reports)
Planned
- Cron schedules
- Live run updates (poll
GET /v1/runs/{id}today)
A Flow is an ordered chain of tiles evaluated per target (a named
location with a latitude/longitude). Each tile receives the previous tile’s output as
upstream; the flow triggers when every non-reportOnly tile is triggered; the flow’s
probability and colour come from the first acc (Probability) tile, if any.
Creating a flow
POST /v1/flows (user gate · keys need flows:write)
{
"displayName": "Heat then humid",
"notes": "demo",
"status": "draft", // draft | active | paused
"tiles": [
{ "row": 0, "tileKey": "gdd", "params": { "threshold": 300, "startDate": "2000-03-01", "stopDate": "2000-10-31" } },
{ "row": 1, "tileKey": "bv", "params": { "metric": "humidity.ambient", "trigger": "greater", "threshold": 50, "when": "last" } },
{ "row": 2, "tileKey": "acc", "params": { "mode": "time", "duration": 2, "timeUnits": "hours", "threshold": 40 } }
],
"targets": [ { "sourceId": "<id from /v1/sources>" }, { "name": "Inline", "latitude": 43.6, "longitude": -116.2 } ],
"settings": { "colors": { "useGradient": true, "color20": "#2e7d32", "color100": "#d32f2f" } }
}A flow record stores its definition (tiles, targets, settings, schedule). Run results live
on runs and lastRun on a flow is a read-time summary of the newest one,
so editing screens and run-inspection screens each fetch only what they need.
Reference tiles by tileKey (latest version) or pin tileDefinitionId + tileVersion. The
Engine validates every tile’s params against its JSON Schema, applies schema defaults, and
stores the resolved {tileDefinitionId, tileKey, tileVersion, params, reportOnly}, so a
later platform-tile update never changes a saved flow until you re-save it. Invalid input is a
422 with errors[].loc (e.g. tiles[0].params.threshold). settings is a free-form
pass-through for GUI/alert configuration:
settings.alerts: Engine-validated alert configsettings.colors: run colour (triggerColor/resetColor, optionaluseGradient+color20…color100). Create/Edit Flow settings writes this;color_forreads it on every runsettings.temperatureUnits:"celsius"|"fahrenheit"GUI display preference. Tile math and stored params stay °C; the GUI converts temperature fields, summaries, and run values.settings.gui: GUI extras (sourceIds, report schedules, per-flow reportcompanyName/companyLogo)
GET /v1/flows (paginated, q search, ?status=, tenant callers ?userKey=),
GET|PATCH|DELETE /v1/flows/{id}. Every flow carries ownerUserId / ownerUserKey; tenant
callers set ownerUserKey on create or PATCH (see who owns what).
PATCH with tiles or targets bumps version. Each flow carries its last run:
lastRunId, lastRunAt, lastRunStatus, lastRunTriggered, lastRunProbability, lastRunColor.
Area targets and sensor averaging
Binding an area Source as a target does more than pick a location: at run time the
Engine finds every active sensor Source whose location lies inside the area’s boundary
and fetches their series individually (one adapter request per sensor). Sensors that share a
sensorType are then averaged into the area’s series in 15-minute buckets; a
sensor missing data in a bucket is excluded from that point only. precipitation.interval is
re-derived from the averaged cumulative counter so rain totals stay correct.
- Give your sensor Sources a
sensorType(e.g.soil-probe,canopy-sensor) so only like-for-like sensors blend; different types contribute their own metrics. - Sensors outside any bound area (or bound to the flow directly) run as their own targets.
- An area containing no sensors falls back to a single request with the area’s own id, so an adapter may still serve area-level series.
- Each run records which sensors fed an area (
results[].target.sensorSources).
Running a flow
POST /v1/flows/{id}/run (user gate · keys need flows:run)
{ "now": "2026-07-15T12:00:00Z" } // optional run-time override (simulation)With the job queue enabled the call returns 202 with the queued run; targets execute in
parallel on workers; poll GET /v1/runs/{id} until status is succeeded/failed (a second
run-now while one is unfinished returns 409). A finished run looks like:
{
"id": "…", "flowId": "…", "flowVersion": 1, "status": "succeeded", "trigger": "manual",
"windowStart": "2026-03-01T00:00:00Z", "windowEnd": "2026-08-19T04:30:00Z", "durationMs": 347,
"summary": { "targetCount": 2, "triggeredCount": 2, "anyTriggered": true, "probability": 100, "color": "#d32f2f" },
"results": [ {
"target": { "name": "North block", "latitude": 43.6, "longitude": -116.2 },
"triggered": true, "probability": 100, "color": "#d32f2f",
"tiles": [ { "row": 0, "key": "gdd", "version": 1, "kind": "numeric", "isTriggered": true, "lastValue": 778,
"points": [ { "ts": 1772323200000, "value": 4.2, "isTriggered": false } /* … ≤ 2000 */ ], "trace": [ "…" ], "error": null } /* … */ ]
} ],
"error": null
}The run window is derived from the tiles (GDD/chill/scout date windows, tw look-backs;
otherwise the last 120 days). Data-consuming tiles use their dataSource: generate produces
deterministic demo data for the target (same location + day ⇒ same data); adapter fails the
run (status: "failed", error explains) until adapters are available. A failing tile script or
an unreachable runner also yields a failed run, never a 5xx.
GET /v1/flows/{id}/runs (newest first, summaries only; ?includeResults=true for detail) and
GET /v1/runs/{id} (keys need results:read).
Scheduling
Set scheduleMinutes (≥ 5) and keep the flow active: the Engine runs it every interval
(nextRunAt shows the next tick). Missed ticks are never back-filled; if the previous run is
still going, the tick is skipped and counted in skippedTicks. A per-tenant cap
(settings.maxRunsPerHour, default 120) bounds total runs per hour; remember that every scheduled
run fetches fresh data through your adapters.