Adapters: bring your own data connectors
Available
series,weatherandeventsdata adapters: CRUD, encrypted env, egress allow-list,POST /v1/adapters/{id}/test, flow runs withdataSource: adaptertargetsadapters: your source catalog, synced into the Engine hourly and on demandcommsadapters: your own email, SMS or voice sender for alerts and reports
Planned
- Python adapters
- Run logs
- Auto-disable on repeated failure
- Per-tenant quotas
The Engine stores no sensor, weather or event data. When a Flow runs, the Engine asks your adapter for exactly the window it needs, uses the data for that run, and discards it, so your systems stay the single source of truth. An adapter is a small script, stored in the Engine, that the Engine executes (sandboxed) whenever a Flow needs that facet of data.
An adapter is:
| Part | What it is |
|---|---|
| Facet | what it does. Data in: series (sensor readings), weather (forecasts), events (scout events); catalog: targets (supplies your source list, synced into the Engine); data out: comms (sends alert/report messages through your own provider); one adapter per facet |
| Script | your code: TypeScript / JavaScript today (Python is planned), one default-exported function |
| Env | a key/value set of environment variables (API tokens, base URLs…). Values are encrypted at rest and never returned after you write them; your script sees them as ctx.env |
| Egress allow-list | the hostnames your script may call; nothing else is reachable |
Scripts run in the same Worker sandbox as tiles: there is no filesystem or environment, and no
network except the hosts on your allow-list (an empty list means no network at all). Time and
output size are capped; run logs, auto-disable on repeated failure and per-tenant quotas are
planned. The last error is kept on the adapter (lastError).
Script contract
// facet: series. Canonical metric names (see the metric dictionary), epochs in milliseconds
export default async function (ctx, req: { sourceIds: string[]; metrics: string[]; startMs: number; endMs: number }) {
const res = await ctx.fetch(`${ctx.env.BASE_URL}/telemetry?from=${req.startMs}&to=${req.endMs}`, {
headers: { Authorization: `Bearer ${ctx.env.TOKEN}` },
});
const rows = await res.json();
return { series: { "temperature.ambient": rows.map((r) => [r.ts, r.temp]) } };
}Per facet: series gets {sourceIds, sourceKeys, metrics, startMs, endMs, userKey} and
returns {series: {<metric>: [[ts, value], …]}} · weather gets {lat, lng, startMs, endMs, userKey} and returns {days: […]} (the standard weather shape, ms epochs) · events gets
{sourceIds, sourceKeys, startMs, endMs, userKey} and returns {events: [{ts, key, value?}, …]}.
userKey is the tenantKey of the user behind the run (or null for key-initiated runs);
use it when your API needs a user identity. sourceKeys parallels sourceIds with each
source’s tenantKey (your own id for it, such as a device serial or a field id), falling
back to the Engine id when unset. Key your upstream lookups on it and register sources with
the tenantKey your system knows. ctx provides
env, the egress-restricted fetch, log(level, message) and now. Results are
shape-checked by the Engine; an invalid or over-size result fails the run with a clear error.
The targets facet supplies your source catalog instead of run data: the Engine calls
it with {userKey} (null on scheduled syncs; put your account identity in the adapter
env) and it returns {sources: [{key, name, hint?, sensorType?, location?, boundary?, timezone?, metadata?}]}. key is your stable id for the source and becomes its
tenantKey (exactly what your series/events adapters receive back as sourceKeys). The
Engine reconciles the list into /v1/sources hourly and on POST /v1/adapters/{id}/sync:
new keys are created (marked metadata.managedBy: "targets-adapter"), matching
hand-registered sources are adopted, managed sources missing from your list are
disabled, never deleted, and re-enabled if they return. Sources you registered by
hand without a matching key are left alone.
The outbound comms facet inverts the direction: the Engine calls it to send a
message instead of fetch data. It gets {channel: "email" | "sms" | "voice", to, subject?, message, html?} and returns {ok: true} or {ok: false, error}. While a comms adapter is
active, all alert and report deliveries for your tenant go through it (per message:
a failed send is recorded against that recipient); disable or delete it to fall back to the
platform transports.
Choosing what a Flow uses
Each data tile carries a Data source parameter: generate (deterministic demo data, so you can build
and test flows before your data is connected) or adapter (your adapter for that facet). All
tiles that read the same facet in one flow must agree; a run that needs a facet in adapter mode
without an active adapter fails with a message pointing here.
The Growth Lab GUI exposes this as Settings → Adapters when you sign in as the tenant
(not as a grower). Env values are write-only; Test and (for targets) Sync sources are
on the same pane. Grower sessions cannot list or edit adapters.
Sample adapters
Four complete adapters for the Soiltech platform API are available from Soiltech on request,
together with a small registration script that installs any adapter through /v1/adapters:
- weather: forecasts via an upstream weather proxy (auth headers from
ctx.env, second→ms epochs, upstream-error handling,userKeyidentity) - series: sensor readings (
sourceKeys= device serials, wire-field → canonical-metric mapping, cumulative-rain handling, window clamping) - events: scout events (
sourceKeys= field ids, per-id fan-out with bounded concurrency, ISO→ms timestamps, lower-cased keys) - targets: catalog sync (fields → areas with boundary rings, devices → sensors with locations, keyed by field id / device serial so the data adapters get the right upstream ids automatically)
Endpoints (Available)
POST/GET/PATCH/DELETE /v1/adapters[/{id}] (one adapter per facet; env is write-only;
responses return envKeys only; PATCH status: disabled to turn one off) ·
POST /v1/adapters/{id}/test (run once with a sample request, returns the result or error).
Both: tenant gate, scope adapters:manage. Planned: GET /v1/adapters/{id}/runs, adapter.disabled
webhook. Outbound alerts and webhooks remain Engine-managed.