Compute
Server-side compute that extends your workspace's API — SQL-style views over your data and JSONata atomic functions that compose admin calls in one transaction.
The Compute tab is where you define server-side logic against your workspace. Two primitives:
- Views materialize a fold of events into a per-key state table — eagerly maintained, point-lookup reads. Good for "current state per entity" questions.
- Atomic functions compose admin-API calls inside a single database transaction, exposed at
POST /functions/{workspace}/{name}. Good for higher-level operations like issue-a-credit, recompute-quota, close-a-session.
Views
A view materializes a fold of events from one or more sources into a per-key state table. Unlike outputs (filter + reshape at read time), views maintain state eagerly: each ingested event runs through the view's reducer and updates a stored row, so reads are point lookups against an indexed table.
Views are good for "current state per entity" questions: latest reading per device, current session per user, running count per asset, most recent status per beacon. They are not for analytics (sum across many rows, percentiles, histograms); for that, query the raw event stream through an output.
How a view is defined
A view has:
- Sources: one or more sources the view materializes from. Only events arriving on these sources are considered.
- Key body: a JSONata expression evaluated against
{payload, metadata, received_at}for each incoming event. The result is coerced to a scalar string and used as the row key. If the expression returns null, missing, or a non-scalar value, the event is skipped — the key extractor doubles as a per-event filter, so a single view can apply to several event shapes by switching on payload fields. - Reducer: a JSONata expression evaluated against
{prev, event}that returns the new value to store under the key.previs the row's existing value (or the init expression's result if no row exists).eventis{payload, metadata, received_at, key}. - Init (optional): a JSONata expression evaluated when the first event arrives for a key. Falls back to
nullif absent.
The dashboard provides canned reducers as shorthand:
| Name | Effect |
|---|---|
last | Latest event wins. The most common choice. |
first | Keep the first event seen for this key; ignore subsequent events. |
count | Maintain a counter per key. Returns { value: N }. |
merge | Deep-merge each event's payload into the stored value. |
custom | Write your own JSONata expression. |
Creating a view
- Click + New view under the Views section.
- Enter a name.
- Pick one or more sources.
- Write the key body. The default
$.payload.idkeys events by theiridfield; adjust to whatever uniquely identifies an entity in your payload. - Pick a reducer, or write a custom JSONata expression.
- Optionally set an init expression and a required scope.
- Click Run test with a sample payload to confirm the key extraction and reducer behave as expected before saving.
- Click Create.
Requires Editor or Admin role.
Reading view data
Materialized rows are exposed at two URLs:
GET /views/{workspace_slug}/{view_name}/data?key=K
GET /views/{workspace_slug}/{view_name}/data?prefix=P&limit=N&cursor=C
GET /views/{workspace_slug}/{view_name}/events?key=Kis a point lookup. Returns the row or 404.?prefix=Pis a keyset-paginated range scan. The response includes anext_cursorfield; pass it ascursor=on the next request./eventsis a server-sent-events stream that emits one frame per row change.
If the view has a required scope set, callers must present a Bearer token with that scope in their scope claim.
If the view has a scope-field body, only rows whose value matches the caller's token subject (sub) are returned. This is the "end-user reads only their own rows" pattern: the scope-field expression is evaluated against each stored value, and rows are included only if its scalar result equals the token's sub. End-user tokens issued via PKCE carry the end-user's id as sub; the scope-field expression typically extracts the owning end-user id from the stored value (e.g., $.end_user).
Deleting a view
Click the delete button on a view row. Materialized rows for that view are removed.
Requires Admin role.
Errors
If a reducer or key body throws, that event is skipped for the affected view and the error is stamped on the view's row (last_error_at, last_error_message, and an incrementing error_count). The ingest itself is not blocked — other views and the underlying event remain durable. The dashboard surfaces the error count and most recent message in the view list.
Atomic functions
An atomic function is a JSONata expression that composes admin-API calls inside a single database transaction. Functions are how a workspace exposes higher-level operations (issue a credit, recompute a quota, close a session) without writing custom server code: every step the function performs hits the same admin routes the dashboard uses, and the whole sequence either commits together or rolls back.
Each function is exposed at POST /functions/{workspace}/{name} with the auth methods you choose (oauth2, hmac, none). Inside the body, $iotman_call(method, path, body) dispatches loopback HTTP — every call participates in the function's transaction — and $require / $fail produce structured errors with the HTTP status you specify.
See the Compute tab in the dashboard for the editor; the body is JSONata.
Worker identity
Every function is assigned a worker — an OAuth2 client in the workspace marked with is_worker = true. When the function runs, $iotman_call requests authenticate as that worker (Bearer auth), and scopes resolve from the client's iotman: scope grants. The function's privilege surface is exactly what the operator granted the worker; nothing more.
Workers are normal OAuth2 clients with one extra flag. Create them in the Access tab by checking Use as worker identity, then grant scopes on the client like any other. The same client can serve double duty — internal worker plus external machine caller — but the typical pattern is one worker per function (or per cluster of related functions) so privilege is bounded.
A function cannot be created or saved without a worker assignment. If no workers exist in the workspace, create one in the Access tab first.
Views also accept a worker assignment, but optionally. Today it is metadata-only — view materialization is a pure JSONata fold and makes no admin calls. The column exists so views can grow side effects (derived events, chained triggers) later without retrofitting identity.
Triggers
A trigger binds a function to a source: when an event arrives on that source and the trigger's condition matches, the function fires automatically. Triggers turn synchronous "POST and wait for the result" calls into event-driven workflows.
A trigger has:
- Function: which atomic function fires.
- Source: the source whose events evaluate the trigger condition.
- Trigger condition: a JSONata expression evaluated against
{payload, metadata, received_at}. Truthy result fires the function; null / false / empty skips the event. Same convention as a view's key body, sotruefires on every event and a path expression like$.payload.kind = "command"fires on a subset. - Reply-to source (optional): when set, every started invocation writes one result event to this source. The reply envelope carries
status: "ok"with the function's return value, orstatus: "error"with the status code and message. Consumers subscribe to the reply source like any other source — SSE, webhooks, exports — to get the function's outcome.
Triggers are evaluated after the ingest transaction commits. Each fired function opens its own transaction-session, so a slow or failing function never blocks ingest, and the inbound event remains durable even if the function rolls back.
Error treatment
Triggers and views diverge on errors:
- Views have a queryable row table, so reducer errors stamp the view's metadata and the change channel stays pure-success.
- Functions have no equivalent durable row. The reply channel is the audit log of every invocation, so both successful and failed runs post one event to
reply_to.
The split between the reply channel and the trigger row:
| Failure | Reply event? | Stamps trigger row? |
|---|---|---|
| Trigger condition parse / eval error | No — function never started | Yes |
$require / $fail throw | Yes (status from $fail) | No (user-intent) |
$iotman_call non-2xx | Yes (upstream status) | No |
| Function body parse error | Yes (500) | Yes |
| Runtime JSONata throw | Yes (500) | Yes |
User-intent errors ($require, $fail, deliberate non-2xx loopback calls) ride the reply channel only — they are the function's authored response. Authoring / runtime / infrastructure failures stamp the trigger row as well, so operators see the "this binding has been failing" indicator without subscribing.
If you create a trigger without a reply-to source, successes drop silently and only runtime errors stamp the row. That's the strict fire-and-forget mode — useful when the side effects are self-evident, otherwise prefer a reply target.
Creating a trigger
- Open the Compute tab.
- On the function row, click Triggers.
- Click + Add trigger.
- Pick the source whose events should evaluate the condition.
- Write the trigger condition. Default
truefires on every event. - Pick an optional reply-to source.
- Click Create.
Requires Editor or Admin role.