state-machine-architecture
How to architect web applications where AI agents can observe state, discover available actions, and execute them safely.
Purpose
Define an architecture pattern that enables AI agents to:
- Understand what the app does
- Observe current state
- Discover available actions given the state
- Execute actions to modify state
Core Insight: Redux + Finite State Machines
Redux captures the mutation pattern (actions create state changes), but doesn’t capture validity (which actions make sense when).
Finite State Machines (FSM) explicitly define:
- All possible states
- Valid transitions between states
- Guards (conditions for transitions)
The combination gives us:
- Single source of truth (Redux)
- Predictable mutations (reducers)
- Explicit valid actions per state (FSM)
- Discoverable action catalog (for AI)
Architecture Overview
+-------------------------------------------------------------------------+| AI Agent (Claude) || Receives: { state, availableActions, appDescription } || Sends: { action, payload } |+-------------------------------------+-----------------------------------+ | v+-------------------------------------------------------------------------+| Agent Bridge Layer || - Serializes state for AI consumption || - Computes available actions from current state || - Validates incoming actions || - Dispatches valid actions to store |+-------------------------------------+-----------------------------------+ | +-------------------------+-------------------------+ v v v+-------------------+ +-------------------+ +-------------------+| State Machine | | Redux Store | | Action Registry || (XState) | | | | (JSON Schemas) || | | - UI state | | || - Flow states | | - Data state | | - All actions || - Valid actions |<--->| - Derived state |<--->| - Input schemas || - Guards | | | | - Descriptions |+-------------------+ +-------------------+ +-------------------+The Four Requirements
1. App Description (What the App Does)
Static metadata the AI receives once:
interface AppDescription { name: string; purpose: string; capabilities: string[];
// Entity types the app manages entities: { name: string; description: string; fields: Record<string, FieldSchema>; }[];
// High-level flows/features flows: { name: string; description: string; states: string[]; }[];}Example:
const appDescription: AppDescription = { name: "E-Commerce Store", purpose: "Online shopping platform for browsing and purchasing products", capabilities: [ "Browse product catalog", "Search products", "Manage shopping cart", "Complete checkout with shipping and payment" ], entities: [ { name: "Product", description: "Item available for purchase", fields: { id: { type: "string" }, name: { type: "string" }, price: { type: "number" }, inStock: { type: "boolean" } } }, { name: "CartItem", description: "Product in shopping cart with quantity", fields: { productId: { type: "string" }, quantity: { type: "number" } } } ], flows: [ { name: "Checkout", description: "Purchase flow from cart to order confirmation", states: ["cart", "shipping", "payment", "confirmation"] } ]};2. Observable State (Current State)
What AI sees on every turn:
interface ObservableState { // Current state machine state(s) machineState: { current: string; // e.g., "checkout.shipping" context: Record<string, any>; };
// Relevant data state (filtered/summarized) data: { user: { id: string; name: string; } | null; cart: { items: number; total: number; }; currentView: string; };
// UI state ui: { modals: string[]; // Open modals loading: string[]; // Loading indicators errors: string[]; // Active errors focused: string | null; // Current focus };}Key principle: Don’t expose raw Redux state. Serialize a summarized, AI-friendly view that contains what’s relevant for decision-making.
3. Available Actions (State-Dependent)
Computed dynamically from current state:
interface AvailableAction { name: string; description: string; inputSchema: JSONSchema; // For payload validation reason?: string; // Why this action is available effects?: string[]; // What this action will do}
function getAvailableActions(state: ObservableState): AvailableAction[] { const actions: AvailableAction[] = [];
// Always available (global actions) actions.push({ name: 'navigate', description: 'Navigate to a different page', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Route path like /dashboard' } }, required: ['path'] } });
// State-dependent actions if (state.machineState.current === 'checkout.cart') { if (state.data.cart.items > 0) { actions.push({ name: 'proceed_to_shipping', description: 'Continue to shipping information', inputSchema: { type: 'object', properties: {} }, reason: 'Cart has items', effects: ['Will transition to shipping form'] }); }
actions.push({ name: 'update_cart_item', description: 'Change quantity of an item in cart', inputSchema: { type: 'object', properties: { itemId: { type: 'string' }, quantity: { type: 'number', minimum: 0 } }, required: ['itemId', 'quantity'] }, effects: ['Updates cart total', 'quantity=0 removes item'] }); }
if (state.machineState.current === 'checkout.shipping') { actions.push({ name: 'submit_shipping', description: 'Submit shipping address and continue', inputSchema: { type: 'object', properties: { address: { type: 'string' }, city: { type: 'string' }, state: { type: 'string' }, zip: { type: 'string' } }, required: ['address', 'city', 'state', 'zip'] }, effects: ['Validates address', 'Transitions to payment'] });
actions.push({ name: 'go_back_to_cart', description: 'Return to cart to modify items', inputSchema: { type: 'object' }, effects: ['Returns to cart view'] }); }
return actions;}Why this matters for AI:
- AI knows exactly what it CAN do (no guessing)
- JSON schemas enable structured payload generation
- Reasons/effects help AI make informed decisions
4. Action Execution
interface ActionResult { success: boolean; newState: ObservableState; newAvailableActions: AvailableAction[]; changes?: { type: 'state_transition' | 'data_update' | 'ui_change'; description: string; }[]; error?: string;}
async function executeAction( actionName: string, payload: any): Promise<ActionResult> { const oldState = getCurrentState(); const available = getAvailableActions(oldState);
// 1. Validate action is available const action = available.find(a => a.name === actionName); if (!action) { return { success: false, error: `Action "${actionName}" not available in current state`, newState: oldState, newAvailableActions: available }; }
// 2. Validate payload against schema const validation = validateSchema(payload, action.inputSchema); if (!validation.valid) { return { success: false, error: `Invalid payload: ${validation.errors.join(', ')}`, newState: oldState, newAvailableActions: available }; }
// 3. Execute: dispatch to Redux + State Machine try { store.dispatch({ type: actionName, payload }); stateMachine.send({ type: actionName, ...payload });
// 4. Wait for async effects await waitForEffects();
// 5. Compute new state const newState = getCurrentState(); return { success: true, newState, newAvailableActions: getAvailableActions(newState), changes: computeChanges(oldState, newState) }; } catch (err) { return { success: false, error: err.message, newState: getCurrentState(), newAvailableActions: getAvailableActions(getCurrentState()) }; }}AI Tool Interface
What Claude receives as tools:
const tools = [ { name: "get_app_info", description: "Get description of what this app does and its capabilities", input_schema: { type: "object", properties: {} } }, { name: "get_current_state", description: "Get current state and list of available actions", input_schema: { type: "object", properties: {} } }, { name: "execute_action", description: "Execute an action from the available actions list", input_schema: { type: "object", properties: { action: { type: "string", description: "Action name (must be in available actions)" }, payload: { type: "object", description: "Action payload matching the action's input schema" } }, required: ["action"] } }];Example Interaction Flow
User: "Add the blue shirt to my cart and checkout"
AI: [calls get_current_state]-> { machineState: { current: "browsing" }, data: { cart: { items: 0, total: 0 } }, availableActions: ["search", "view_product", "add_to_cart", "navigate"] }
AI: [calls execute_action { action: "add_to_cart", payload: { productId: "blue-shirt-123" } }]-> { success: true, changes: [{ type: "data_update", description: "Added 1 item to cart" }], newState: { data: { cart: { items: 1, total: 29.99 } } } }
AI: [calls execute_action { action: "navigate", payload: { path: "/checkout" } }]-> { success: true, machineState: { current: "checkout.cart" }, availableActions: ["proceed_to_shipping", "update_cart_item", "remove_item"] }
AI: [calls execute_action { action: "proceed_to_shipping", payload: {} }]-> { success: true, machineState: { current: "checkout.shipping" }, availableActions: ["submit_shipping", "go_back_to_cart"] }
AI: "I've added the blue shirt ($29.99) to your cart and started checkout. I'm now at the shipping step. Would you like me to fill in your shipping address, or would you like to enter it yourself?"Architecture Patterns Comparison
| Approach | Pros | Cons |
|---|---|---|
| Redux only | Simple, familiar | Actions always “available”, ad-hoc validation |
| XState/FSM only | Explicit transitions | Complex for data-heavy apps |
| Redux + FSM hybrid | Best of both | Two systems to synchronize |
| Actor Model | Distributed, message-based | Harder to observe globally |
Recommendation: Redux + FSM hybrid for most cases.
Security Considerations
Action Allowlisting
Only expose safe actions. Never expose:
- Arbitrary code execution
- Direct DOM manipulation
- Unrestricted API calls
- Admin operations without confirmation
User Confirmation for Sensitive Actions
const sensitiveActions = ['delete_account', 'place_order', 'send_payment'];
async function executeWithConfirmation(action: string, payload: any) { if (sensitiveActions.includes(action)) { const confirmed = await showConfirmDialog( `AI wants to: ${action}`, `Payload: ${JSON.stringify(payload)}` ); if (!confirmed) { return { success: false, error: 'User cancelled' }; } } return executeAction(action, payload);}Rate Limiting
const rateLimiter = new RateLimiter({ maxCalls: 10, windowMs: 1000});
async function executeWithRateLimit(action: string, payload: any) { if (!rateLimiter.allow()) { return { success: false, error: 'Rate limited', retryAfterMs: rateLimiter.retryAfter() }; } return executeAction(action, payload);}Implementation with XState
Example XState machine for checkout flow:
import { createMachine, assign } from 'xstate';
const checkoutMachine = createMachine({ id: 'checkout', initial: 'cart', context: { items: [], shipping: null, payment: null }, states: { cart: { on: { UPDATE_ITEM: { actions: assign({ items: (ctx, event) => updateItem(ctx.items, event.itemId, event.quantity) }) }, PROCEED_TO_SHIPPING: { target: 'shipping', guard: 'hasItems' } } }, shipping: { on: { SUBMIT_SHIPPING: { target: 'payment', actions: assign({ shipping: (_, event) => event.address }), guard: 'validAddress' }, GO_BACK_TO_CART: 'cart' } }, payment: { on: { SUBMIT_PAYMENT: { target: 'confirmation', actions: assign({ payment: (_, event) => event.paymentMethod }), guard: 'validPayment' }, GO_BACK_TO_SHIPPING: 'shipping' } }, confirmation: { type: 'final' } }}, { guards: { hasItems: (ctx) => ctx.items.length > 0, validAddress: (_, event) => validateAddress(event.address), validPayment: (_, event) => validatePayment(event.paymentMethod) }});Benefits of This Architecture
- Discoverability - AI always knows what actions are possible
- Safety - Invalid actions rejected before execution
- Transparency - AI understands why actions are/aren’t available
- Determinism - Same state + action = same result
- Debuggability - Full action history, state snapshots
- Human oversight - Confirmation for sensitive actions
- Testability - State machines are highly testable
Related Patterns
- Claude Frontend Control - Basic tool use patterns for UI control
- Computer Use - Screenshot-based UI control (heavier)
- MCP - Protocol for connecting AI to external tools/data
Sources
- XState Documentation
- Redux Style Guide
- State Machines in UI - David Khourshid
- Finite State Machines in Game AI
Created: 2025-12-13