State Management in Builder.io Agent Native: SQL-Backed Architecture Explained
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), 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).
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).
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) demonstrates reading a simple boolean flag:
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:
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:
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)
Triggering UI Refreshes from Agents
After updating navigation state, agents force synchronization:
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), 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:
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_stateSQL 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-orforms-. -
The polling mechanism watches
updated_attimestamps to push changes to the client viauseDbSync(), 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. 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 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →