Data Overview

Structured, queryable storage scoped to each app

Most chat platforms treat integrations as notification pipes — data flows in, gets buried in a timeline, and becomes unsearchable. Start.chat takes a different approach: apps store structured data directly in your workspace, where it stays queryable, displayable, and actionable.

What this section is for

Read the Data docs after Apps if you need durable state, searchable records, dashboards, or structured context for AI. This section is about storage and query patterns inside start.chat, not external SDK or REST access.

Data is a key-value storage system that apps use to sync external information, store state, and share data across your server. Every entry has a key, a value, and rich metadata like title, kind, tags, and status — making it easy to filter, sort, and display in app UIs.

Two kinds of data output

Apps produce data in two distinct ways. Understanding the difference is key:

Action return data

Actions return AppResultDataItem[] as their primary structured output via the data field on the result object. These items have well-defined fields the UI knows how to render automatically — title, status, kind, url, tags, externalId, and a flexible value field for domain-specific payloads.

// an action returns structured data items
return {
summary: '25 payments totaling $3,420',
data: result.items, // AppResultDataItem[] — auto-rendered by blocks
}

Returning data does not automatically insert anything into the data table. The items flow through the pipeline and blocks can render them, but they are a structured “view” of what the action produced — not persisted records.

Persisted data

To store data permanently, apps explicitly call tools.data.public.set(), tools.data.public.stat(), or tools.data.public.series(). Persisted data lives in the data table, is searchable, and can be queried by blocks and the AI.

// persist a stat — overwrites on each sync
await tools.data.public.stat('stripe-mrr', { value: 48200 })
// persist a series data point — auto-aggregated over time
await tools.data.public.series('stripe-daily-revenue', { value: 1420 })

Many actions do both: return data items for the pipeline/UI, and persist key metrics via stat/series for dashboards.

How data gets created

Data typically comes from app backends that sync on a schedule:

export const backend = {
runsEvery: '15 minutes',
async run({ tools }) {
const customers = await fetchCustomers()
for (const customer of customers) {
await tools.data.public.set(`customer:${customer.id}`, JSON.stringify(customer), {
title: customer.name,
kind: 'customer',
tags: [customer.plan],
})
}
},
}

Built-in apps like GitHub, Stripe, and Linear sync their data automatically when connected. You can also import data by dragging CSV or JSON files into the Data view.

AppResultDataItem

The standardized item shape returned from actions. The UI auto-renders these fields as tables, lists, badges, and links without needing app-specific code:

FieldTypeDescription
typestringitem type (e.g., payment, issue)
keystring?unique key (e.g., stripe-payment:ch_abc)
valuestring or objectdomain-specific payload
titlestring?human-readable display title
statusstring?status indicator
kindstring?broad classification for grouping
urlstring?link to external resource
tagsstring[]?tags for filtering
externalIdstring?ID in the external system
weightnumber?sort priority

Because these fields are well-defined, the UI can:

  • render tables with title, status, and tags as columns
  • show lists with title + status badges
  • make links clickable from url
  • group and filter by kind
  • work without knowing anything about stripe vs github vs linear

Permissions

Data has two permission levels:

PermissionVisibilityUse case
publicAll server membersSynced records, shared dashboards, triggers
privateOnly the app install that created itSync cursors, tokens, internal state

Write access is granted to the data creator, the owner, any role with canEditData, or a server admin.

Scope

Scope controls who the data is relevant to:

ScopeMeaning
serverAvailable server-wide (default)
channelScoped to a specific channel
userScoped to a specific user

AI visibility

The aiVisibility field controls how the AI assistant interacts with data:

ValueBehavior
searchableDiscoverable via search (default)
contextAutomatically included in AI conversation context
hiddenNot visible to AI

For example, the built-in Memory app stores memories with aiVisibility: 'context' so they are always available to the assistant.

Querying data

App UIs query data with the useData hook, which returns live results that update in real time:

const [customers] = useData({
kindFilter: 'customer',
sortColumn: 'createdAt',
sortDirection: 'desc',
limit: 20,
})

The DataTable component provides a full-featured browsing UI with filtering, sorting, and bulk operations out of the box.

Data fields

FieldTypeDescription
keystringLookup key (unique per scope)
valuestringStored value (typically JSON)
titlestringHuman-readable display title
kindstringData kind (e.g., customer, payment)
tagsstring[]Tags for grouping and filtering
statusstringStatus indicator
weightnumberPriority or importance score
formatstringValue format: text, json, or markdown
urlstringLink to external resource
extraobjectArbitrary additional metadata
aiVisibilitystringAI access level (see above)
scopestringData reach: server, channel, or user
permissionsstringpublic or private
startsAtnumberStart time (for events)
endsAtnumberEnd time (for events)
externalIdstringID in the external system
syncedAtnumberLast sync timestamp

Next steps