.agents/skills/storing-data/SKILL.md
How to store application data in agent-native apps. All data lives in SQL. Use when adding data models, deciding where to store data, or reading/writing application data.
npx skillsauth add BuilderIO/agent-native storing-dataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
All application data lives in SQL (SQLite locally, cloud database in production). The agent and UI share the same database. There is no filesystem dependency for data.
Agent-native apps use SQLite via Drizzle ORM + @libsql/client. This works locally out of the box and upgrades seamlessly to cloud databases (Turso, Neon, Supabase, D1) by setting DATABASE_URL. Local and production behave identically.
| Store | Purpose | Access |
| ------------------- | ---------------------------------------------------- | ------------------------------------------ |
| application_state | Ephemeral UI state (compose windows, navigation) | readAppState() / writeAppState() |
| settings | Persistent KV config (preferences, app settings) | getSetting() / setSetting() |
| oauth_tokens | OAuth credentials | @agent-native/core/oauth-tokens |
| sessions | Auth sessions | @agent-native/core/server |
Define schema with Drizzle ORM in server/db/schema.ts. Get a database instance with const db = getDb() from server/db/index.ts. All queries are async.
| Template | Tables | | ------------ | --------------------------------------------- | | Mail | emails, labels (+ Gmail API when connected) | | Calendar | events, bookings | | Forms | forms, responses | | Content | documents | | Slides | decks (JSON stored in SQL) | | Videos | compositions in registry + localStorage |
The agent uses actions to read/write the database:
pnpm action db-schema — Show all tables, columns, typespnpm action db-query --sql "SELECT * FROM forms" — Run SELECT queriespnpm action db-exec --sql "INSERT INTO ..." — Run INSERT / UPDATE / DELETE. Use for short columns, multi-column writes, computed updates.pnpm action db-patch --table <t> --column <c> --where "<clause>" --find "<old>" --replace "<new>" — Surgical search/replace on a large text column. Sends the diff instead of re-transmitting the whole value, so it's dramatically more token-efficient than db-exec UPDATE when editing multi-kilobyte documents, slide HTML, dashboard/form JSON, etc. Targets exactly one row per call — narrow --where by primary key. Supports --edits '[{find,replace},...]' for batch edits and --all to replace every occurrence.db-patch is the generic fallback for tables without a dedicated edit action.How to choose between db-exec UPDATE and db-patch:
| Scenario | Use |
| -------------------------------------------------------------- | ------------ |
| SET status = 'published' on one row | db-exec |
| SET calories = calories + 50 | db-exec |
| Updating several columns at once | db-exec |
| Fixing a typo in a 50KB markdown document's content column | db-patch |
| Changing a single key in a dashboard's JSON blob | db-patch |
| Tweaking one paragraph of slide HTML stored in decks.data | db-patch |
| Any edit where you'd otherwise re-send thousands of characters | db-patch |
All of these honor the per-user / per-org data scoping — you can't read or write rows outside the current user's data, regardless of which tool you choose.
The frontend calls actions via their auto-mounted HTTP endpoints using React Query hooks:
import { useActionQuery, useActionMutation } from "@agent-native/core/client";
// Read data (calls GET /_agent-native/actions/list-meals)
const { data } = useActionQuery<Meal[]>("list-meals", { date: "2025-01-01" });
// Write data (calls POST /_agent-native/actions/log-meal)
const { mutate } = useActionMutation<Meal>("log-meal");
Actions are the preferred way for the frontend to access data. You rarely need custom /api/ routes — only for file uploads, streaming, webhooks, or OAuth callbacks.
Local SQLite works out of the box. To deploy to production with a cloud database:
DATABASE_URL (e.g. libsql://your-db.turso.io)DATABASE_AUTH_TOKEN for auth@libsql/client handles both local and remotePolling streams database changes to the UI. When the agent writes to the database via scripts, the UI updates automatically via useDbSync() which invalidates React Query caches.
settings store for app configuration and user preferencesapplication-state for ephemeral UI state that the agent and UI shareoauth-tokens for OAuth credentialsdb-schema, db-query, db-exec, db-patch) for ad-hoc database operationsdb-patch instead of db-exec UPDATE whenever you're making a small change to a large text/JSON column — it's much cheaper on tokensWhen storing app-state, include navigation state — the agent needs to know what the user is looking at. The application_state table holds ephemeral UI state that both the agent and UI share. Key patterns:
navigation key — the UI writes current view and selection on every route change. The agent reads this before acting.navigate key — the agent writes one-shot commands to navigate the UI. The UI processes and deletes them.compose-{id}) — bidirectional state for features like email drafts.When adding a new data model or feature, also consider what navigation and selection state needs to be exposed via application-state. See the context-awareness skill for the full pattern.
defineAction to query the database (auto-exposed as HTTP endpoints)tools
Public booking flow — the state machine, animations, and URL/app-state sync.
tools
Trigger-based automations — reminders, follow-ups, webhooks — across the booking lifecycle.
tools
Team event types, round-robin assignment, collective bookings, host weights, and no-show calibration.
development
The pure `computeAvailableSlots` function — inputs, outputs, invariants, and debugging guide.