# State Management in Builder.io Agent Native: SQL-Backed Architecture Explained

> Discover how Builder.io Agent Native manages state with a SQL-backed architecture and a type-safe API for seamless UI synchronization. Learn to read and write app state efficiently.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: architecture
- Published: 2026-07-18

---

**Builder.io Agent Native stores all transient UI state in an `application_state` SQL table and provides a type-safe API with `readAppState()` and `writeAppState()` functions to abstract raw SQL operations, using a polling mechanism to sync changes between the agent and UI.**

State management in Builder.io Agent Native follows a database-centric architecture that ensures ACID compliance and real-time synchronization between AI agents and the user interface. Unlike traditional in-memory state solutions, the framework persists ephemeral UI data in a dedicated `application_state` table managed by Drizzle, providing durability across page reloads while maintaining type safety through a minimal abstraction layer. This approach enables seamless coordination between LLM tools and React components in the BuilderIO/agent-native repository.

## Core Architecture: SQL-Backed Storage

Builder.io Agent Native treats transient UI state as a first-class database concern. All ephemeral data lives in the **`application_state`** SQL table alongside other Drizzle-managed tables like `settings` and `tools`.

This design provides several advantages:

- **ACID guarantees** through standard SQL transactions
- **Automatic indexing** and cleanup via database migrations
- **Durability** across browser refreshes and server restarts
- **Explicit exclusion of large payloads**—video chunks and binary blobs are stored in dedicated file services, not in this table

The framework deliberately distinguishes between persistent application data and transient UI state. Only data that must survive page reloads or be shared between the agent and client belongs in `application_state`.

## The Type-Safe State API

Located in [[`packages/core/src/application-state/index.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/application-state/index.ts)](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/application-state/index.ts), the public API abstracts raw SQL calls into four core operations:

**`readAppState(key)`** retrieves values for the current session. UI components use this to access drafts, selections, or navigation state.

**`writeAppState(key, value)`** persists values while automatically updating the row's `updated_at` timestamp. Agents invoke this to change screens, update selections, or store temporary computation results.

**`readAppStateForCurrentTab(key)`** provides tab-scoped isolation by prefixing keys with namespace identifiers like `slides-` or `forms-`. This prevents collisions when multiple editors run simultaneously. The implementation delegates to `readAppState` after applying the prefix.

**`refreshScreen()`** triggers a manual poll that forces the UI to fetch the latest `application_state` rows, typically called by agents after batch write operations.

## Tab-Scoped State Isolation

To support heterogeneous editors (Slides, Forms, Mail), the framework implements namespacing through helper functions in [[`templates/slides/actions/_tab-state.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/_tab-state.ts)](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/_tab-state.ts).

When a component calls `readAppStateForCurrentTab("slide-fit-check")`, the utility automatically prefixes the key with the tab identifier before querying the database. This ensures that the Slides editor's fit-check flag never collides with the Forms editor's navigation state, enabling multiple editors to coexist within the same application session.

## Real-Time Synchronization Mechanism

State changes propagate automatically through a polling architecture defined in [[`packages/core/src/server/poll.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/poll.ts)](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/poll.ts).

The server-side poll handler monitors the `application_state.updated_at` column. When any row modification occurs, the `useDbSync()` hook on the client side receives the update, ensuring the UI instantly reflects writes performed by either user actions or the LLM agent.

This mechanism eliminates the need for complex WebSocket management while maintaining near-real-time consistency between the AI agent's view and the rendered interface.

## Practical Implementation Examples

### Reading UI State in Actions

The following pattern from [[`templates/slides/actions/view-screen.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/view-screen.ts)](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/view-screen.ts) demonstrates reading a simple boolean flag:

```typescript
import { readAppState } from "@agent-native/core/application-state";

async function isSidebarOpen() {
  const flag = await readAppState("sidebar-open");
  return !!flag;
}

```

### Writing Draft Documents

Agents persist temporary documents using `writeAppState`:

```typescript
import { writeAppState } from "@agent-native/core/application-state";

async function saveDraft(draftId: string, content: string) {
  await writeAppState(`draft-${draftId}`, content);
}

```

### Accessing Tab-Scoped State

For per-tab data like slide-fit-check flags, use the tab-scoped helper:

```typescript
import { readAppStateForCurrentTab } from "./_tab-state";

async function getFitCheck() {
  const raw = await readAppStateForCurrentTab("slide-fit-check");
  return raw ? JSON.parse(raw) : null;
}

```

*Source:* [[`templates/slides/actions/_await-fit-check.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/_await-fit-check.ts)](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/_await-fit-check.ts)

### Triggering UI Refreshes from Agents

After updating navigation state, agents force synchronization:

```typescript
await writeAppState("navigation", { view: "forms" });
await refreshScreen(); // Forces immediate UI update

```

The `refreshScreen` tool is defined in the system prompt at [[`packages/core/src/server/prompts/framework-core.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/prompts/framework-core.ts)](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/prompts/framework-core.ts), where the LLM receives explicit instructions to use only `readAppState` and `writeAppState` for state access, preventing accidental writes to other tables.

### Client-Side State Subscription

React components consume state through the synchronization hook:

```tsx
import { useDbSync } from "@agent-native/client";

function App() {
  const { data: appState } = useDbSync("application_state");
  // Automatically updates when any row's updated_at changes
}

```

## Summary

- **All transient UI data resides in the `application_state` SQL table**, providing ACID guarantees and automatic indexing through Drizzle migrations.

- **Type-safe helpers** (`readAppState`, `writeAppState`, `readAppStateForCurrentTab`) abstract raw SQL operations while maintaining strict session boundaries.

- **Tab namespacing** prevents key collisions across different editors by automatically prefixing keys with identifiers like `slides-` or `forms-`.

- **The polling mechanism** watches `updated_at` timestamps to push changes to the client via `useDbSync()`, keeping the AI agent and UI synchronized without WebSockets.

- **Large binary data is explicitly excluded** from SQL storage and routed to dedicated file services, keeping the database lightweight.

## Frequently Asked Questions

### Where is the application state physically stored in Builder.io Agent Native?

All transient UI state lives in the **`application_state`** SQL table managed alongside other Drizzle ORM tables. This provides ACID guarantees, automatic indexing, and durability across page reloads, distinguishing it from in-memory state solutions that vanish on server restart.

### How does the framework prevent state collisions between different editor tabs?

The framework implements **automatic namespacing** through `readAppStateForCurrentTab()` helpers defined in files like [`templates/slides/actions/_tab-state.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/_tab-state.ts). These functions prefix keys with tab-specific identifiers (e.g., `slides-` or `forms-`) before delegating to the core `readAppState` function, ensuring complete isolation between simultaneous editor instances.

### What triggers the UI to update when an AI agent modifies state?

The **polling mechanism** in [`packages/core/src/server/poll.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/poll.ts) monitors the `updated_at` column of the `application_state` table. When the agent calls `writeAppState()`, the timestamp updates, causing the server to push changes to connected clients through the `useDbSync()` hook. Agents can also force immediate synchronization by calling the `refreshScreen()` tool.

### Why does the framework restrict LLM access to specific state methods?

According to the system prompt in [`packages/core/src/server/prompts/framework-core.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/prompts/framework-core.ts), the LLM is explicitly instructed to use only `readAppState` and `writeAppState` for state access. This **read-only constraint for other tables** prevents the AI agent from accidentally modifying persistent data like user settings or tool configurations, ensuring state mutations remain confined to transient UI data.