Writing Data
Store and manage data from app backends
App backends write data using tools.data. There are two storage scopes —
public for data shared across the server, and private for data only the app
install can see.
Operations
set(key, value, meta?)
Store or update a key-value pair. The value is always a string; use
JSON.stringify for complex data.
get(key)
Retrieve a single entry by key. Returns null if not found.
getAll()
List all entries for the current scope.
delete(key)
Remove an entry.
stat(key, value, meta?)
Store a point-in-time metric. Calling stat with the same key overwrites the
previous value — use it for current counts, gauges, and snapshots that reflect
the latest state.
Stats use the {app}-{metric} naming convention:
Under the hood, stat creates a row with type: "stat" and the value as a JSON
object. Blocks query it directly:
For multi-field stats, pass an object with all the metrics:
series(name, value, options?)
Store a time-series data point with automatic aggregation. Each call creates a
period-keyed entry and updates running aggregates (sum, count, min, max, avg)
plus a rolling points array (up to 30 values) on the aggregate row.
Series use the same {app}-{metric} naming convention as stats:
How series storage works
Each series() call creates two rows:
- Aggregate row:
key=name,type="series-agg"— running sum, count, min, max, avg, and apointsarray - Period row:
key=name:suffix,type="series",category=name— the individual data point
The period option controls bucketing: 'hourly', 'daily' (default),
'weekly', or 'monthly'. Within each period, updating the same series
replaces the value and recalculates aggregates.
Querying series in blocks
Multi-field series
For metrics with multiple values per point:
Metadata
The optional meta parameter on set accepts:
| Field | Type | Description |
|---|---|---|
title | string | Human-readable title |
kind | string | Data kind for filtering (e.g., customer) |
tags | string[] | Tags for grouping |
status | string | Current status |
weight | number | Priority or importance score |
format | string | Value format: text, json, or markdown |
url | string | Link to external resource |
extra | object | Arbitrary additional metadata |
aiVisibility | string | context, searchable (default), or hidden |
scope | string | server (default), channel, or user |
startsAt | number | Event start time |
endsAt | number | Event end time |
externalId | string | ID in the external system |
syncedAt | number | Last sync timestamp |
Public vs private
Public data is visible to all server members. Use it for synced records, shared dashboards, and anything that should trigger flows.
Private data is scoped to the app install. Use it for sync cursors, OAuth tokens, and internal state.
AI visibility
Control how the AI assistant sees your data:
searchable(default) — discoverable when the assistant searchescontext— automatically loaded into every AI conversationhidden— invisible to the assistant entirely
Key naming conventions
Use the {app}-{metric} convention for stat and series keys. For individual
records, use {app}-{type}:{external-id}:
This convention ensures blocks can query known keys without configuration and keeps keys predictable across apps.
Triggering flows
When public data is created, it can trigger event-based
flows. Flows configured with on app.data run
automatically: