Skip to Content
ConceptsFlows & Runs

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 config
  • settings.colors: run colour (triggerColor / resetColor, optional useGradient + color20color100). Create/Edit Flow settings writes this; color_for reads it on every run
  • settings.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 report companyName / 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.