Skip to main content
Shared state management in CopilotKit allows your React application and AI agents to maintain synchronized state, enabling seamless collaboration and reactive user interfaces. This is essential for building complex, stateful AI applications.

Overview

The State Machine Copilot example demonstrates a multi-stage car sales flow where state transitions are managed through AI interactions. It showcases:
  • Complex state machines with multiple stages
  • Bidirectional state sync between UI and agents
  • Stage-specific AI behavior with context switching
  • Visual state representation using React Flow
  • Generative UI for each stage
  • Form validation and data collection
State Machine Copilot Live Demo: https://copilotkit.ai/examples/state-machine-copilot Source Code: GitHub

What You’ll Learn

This example teaches you how to:
  • Use useAgent for bidirectional state synchronization
  • Build multi-stage state machines with AI
  • Implement stage-specific actions and prompts
  • Create state-driven generative UI
  • Visualize state transitions
  • Manage complex application state with TypeScript

Prerequisites

Quick Start

1

Clone the Repository

git clone https://github.com/CopilotKit/CopilotKit.git
cd CopilotKit/examples/v1/state-machine
2

Install Dependencies

pnpm install
3

Configure API Key

Create a .env file:
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=your_api_key_here
4

Start Development Server

pnpm dev
5

Open Application

Application Flow

The car sales application progresses through these stages:

Stages

  1. Get Contact Info - Collect customer name, email, phone
  2. Build Car - Configure car model, color, features
  3. Sell Financing - Present financing options
  4. Get Financing Info - Collect financing details (if accepted)
  5. Get Payment Info - Process direct payment (if financing declined)
  6. Confirm Order - Review and confirm the order

Core Implementation

1. State Definition

File: lib/types.ts
export interface Car {
  make: string;
  model: string;
  year: number;
  color: string;
  price: number;
  features: string[];
  image?: {
    src: string;
    alt: string;
  };
}

export interface ContactInfo {
  name: string;
  email: string;
  phone: string;
}

export interface FinancingInfo {
  term: number;      // months
  downPayment: number;
  creditScore: number;
}

export interface PaymentInfo {
  cardNumber: string;
  expiryDate: string;
  cvv: string;
  billingAddress: string;
}

export type Stage =
  | "getContactInfo"
  | "buildCar"
  | "sellFinancing"
  | "getFinancingInfo"
  | "getPaymentInfo"
  | "confirmOrder";

export interface AppState {
  stage: Stage;
  contactInfo?: ContactInfo;
  car?: Car;
  financing?: {
    accepted: boolean;
    info?: FinancingInfo;
  };
  payment?: PaymentInfo;
  orderConfirmed: boolean;
}

2. Global State Hook with useAgent

File: lib/stages/use-global-state.tsx
import { useAgent } from "@copilotkit/react";
import { AppState } from "@/lib/types";

export function useGlobalState() {
  const { agent } = useAgent({ agentId: "car_sales" });
  const state = agent.state as AppState;
  const setState = (newState: AppState) => agent.setState(newState);

  // Helper to transition stages
  const setStage = (newStage: AppState["stage"]) => {
    setState({ ...state, stage: newStage });
  };

  // Helper to update contact info
  const setContactInfo = (info: ContactInfo) => {
    setState({ ...state, contactInfo: info });
  };

  // Helper to update car configuration
  const setCar = (car: Car) => {
    setState({ ...state, car });
  };

  // Helper to update financing
  const setFinancing = (accepted: boolean, info?: FinancingInfo) => {
    setState({
      ...state,
      financing: { accepted, info },
    });
  };

  return {
    state,
    setState,
    setStage,
    setContactInfo,
    setCar,
    setFinancing,
  };
}
Key features:
  • useAgent provides bidirectional state sync
  • State changes in UI automatically propagate to agent
  • Agent state updates trigger UI re-renders
  • Type-safe state management with TypeScript
  • Helper functions for common operations

3. Stage-Specific Actions

Each stage has its own hook with stage-specific actions and prompts: File: lib/stages/use-stage-get-contact-info.tsx
import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";
import { ContactInfo } from "@/lib/types";

export function useStageGetContactInfo(
  contactInfo: ContactInfo | undefined,
  setContactInfo: (info: ContactInfo) => void,
  setStage: (stage: string) => void
) {
  useFrontendTool({
    name: "collectContactInfo",
    description: "Collect customer contact information",
    parameters: z.object({
      name: z.string().describe("Customer's full name"),
      email: z.string().describe("Customer's email address"),
      phone: z.string().describe("Customer's phone number"),
    }),
    execute: async ({ name, email, phone }) => {
      setContactInfo({ name, email, phone });
      setStage("buildCar");
    },
  });

  return {
    instructions: `You are a friendly car sales representative. 
                   Greet the customer and collect their contact information.
                   Ask for their name, email, and phone number in a natural conversation.
                   Once you have all the information, call collectContactInfo.`,
  };
}
File: lib/stages/use-stage-build-car.tsx
import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";
import { ShowCars } from "@/components/generative-ui/show-car";

export function useStageBuildCar(
  car: Car | undefined,
  setCar: (car: Car) => void,
  setStage: (stage: string) => void
) {
  useFrontendTool({
    name: "showCarOptions",
    description: "Show car options to the customer",
    parameters: z.object({
      cars: z.array(z.object({
        make: z.string(),
        model: z.string(),
        year: z.number(),
        color: z.string(),
        price: z.number(),
      })).describe("Array of car options matching customer preferences"),
    }),
    render: ({ args, status }) => {
      return (
        <ShowCars
          cars={args.cars || []}
          status={status}
          onSelect={(selectedCar) => {
            setCar(selectedCar);
            setStage("sellFinancing");
          }}
        />
      );
    },
  });

  return {
    instructions: `Help the customer build their dream car.
                   Ask about their preferences for make, model, color, and features.
                   Then show them matching options using showCarOptions.
                   Once they select a car, move to financing.`,
  };
}

4. Orchestrating Stage Hooks

File: components/car-sales-chat.tsx
import { useMemo } from "react";
import { CopilotSidebar } from "@copilotkit/react";
import { useGlobalState } from "@/lib/stages/use-global-state";
import { useStageGetContactInfo } from "@/lib/stages/use-stage-get-contact-info";
import { useStageBuildCar } from "@/lib/stages/use-stage-build-car";
import { useStageSellFinancing } from "@/lib/stages/use-stage-sell-financing";
import { useStageGetFinancingInfo } from "@/lib/stages/use-stage-get-financing-info";
import { useStageGetPaymentInfo } from "@/lib/stages/use-stage-get-payment-info";
import { useStageConfirmOrder } from "@/lib/stages/use-stage-confirm-order";

export function CarSalesChat() {
  const { state, setStage, setContactInfo, setCar, setFinancing } = useGlobalState();

  // Get instructions for current stage
  const stageInstructions = useMemo(() => {
    switch (state.stage) {
      case "getContactInfo":
        return useStageGetContactInfo(state.contactInfo, setContactInfo, setStage);
      case "buildCar":
        return useStageBuildCar(state.car, setCar, setStage);
      case "sellFinancing":
        return useStageSellFinancing(setFinancing, setStage);
      case "getFinancingInfo":
        return useStageGetFinancingInfo(state.financing?.info, setFinancing, setStage);
      case "getPaymentInfo":
        return useStageGetPaymentInfo(setStage);
      case "confirmOrder":
        return useStageConfirmOrder(state);
      default:
        return { instructions: "" };
    }
  }, [state.stage]);

  return (
    <div className="flex h-screen">
      <div className="flex-1">
        <StateVisualizer currentStage={state.stage} />
      </div>
      
      <CopilotSidebar
        instructions={stageInstructions.instructions}
        labels={{
          title: "Car Sales Assistant",
          initial: "Hi! I'm here to help you find your perfect car. Let's get started!",
        }}
      />
    </div>
  );
}

5. State Visualization

File: components/state-visualizer.tsx
import ReactFlow, { Node, Edge } from 'reactflow';
import 'reactflow/dist/style.css';

interface StateVisualizerProps {
  currentStage: string;
}

export function StateVisualizer({ currentStage }: StateVisualizerProps) {
  const nodes: Node[] = [
    {
      id: 'getContactInfo',
      data: { label: 'Contact Info' },
      position: { x: 0, y: 0 },
      className: currentStage === 'getContactInfo' ? 'active' : '',
    },
    {
      id: 'buildCar',
      data: { label: 'Build Car' },
      position: { x: 200, y: 0 },
      className: currentStage === 'buildCar' ? 'active' : '',
    },
    {
      id: 'sellFinancing',
      data: { label: 'Financing' },
      position: { x: 400, y: 0 },
      className: currentStage === 'sellFinancing' ? 'active' : '',
    },
    // ... more nodes
  ];

  const edges: Edge[] = [
    { id: 'e1', source: 'getContactInfo', target: 'buildCar' },
    { id: 'e2', source: 'buildCar', target: 'sellFinancing' },
    // ... more edges
  ];

  return (
    <div className="h-full">
      <ReactFlow
        nodes={nodes}
        edges={edges}
        fitView
      />
    </div>
  );
}

6. Stage-Specific Generative UI

File: components/generative-ui/contact-info.tsx
import { ToolCallStatus } from "@copilotkit/react";

interface ContactInfoFormProps {
  args: { name: string; email: string; phone: string };
  handler: (result: any) => void;
  status: ToolCallStatus;
}

export function ContactInfoForm({ args, handler, status }: ContactInfoFormProps) {
  if (status === ToolCallStatus.Complete) {
    return (
      <div className="bg-green-50 p-3 rounded-lg">
        <p className="text-green-800">Contact information saved!</p>
      </div>
    );
  }

  return (
    <div className="bg-white p-4 rounded-lg shadow-md">
      <h3 className="font-semibold mb-3">Confirm Your Information</h3>
      
      <div className="space-y-2 mb-4">
        <div>
          <span className="text-gray-600">Name:</span>
          <span className="ml-2 font-medium">{args.name}</span>
        </div>
        <div>
          <span className="text-gray-600">Email:</span>
          <span className="ml-2 font-medium">{args.email}</span>
        </div>
        <div>
          <span className="text-gray-600">Phone:</span>
          <span className="ml-2 font-medium">{args.phone}</span>
        </div>
      </div>

      <div className="flex gap-2">
        <button
          className="flex-1 bg-blue-600 text-white px-4 py-2 rounded"
          onClick={() => handler(args)}
        >
          Confirm
        </button>
        <button
          className="flex-1 bg-gray-200 px-4 py-2 rounded"
          onClick={() => handler(null)}
        >
          Edit
        </button>
      </div>
    </div>
  );
}

Advanced Patterns

Conditional Stage Transitions

const handleFinancingDecision = (accepted: boolean) => {
  setFinancing(accepted);
  
  // Branch based on decision
  if (accepted) {
    setStage("getFinancingInfo");
  } else {
    setStage("getPaymentInfo");
  }
};

State Validation

const canProgressToNextStage = () => {
  switch (state.stage) {
    case "getContactInfo":
      return state.contactInfo?.name && 
             state.contactInfo?.email && 
             state.contactInfo?.phone;
    case "buildCar":
      return state.car !== undefined;
    case "sellFinancing":
      return state.financing !== undefined;
    default:
      return true;
  }
};

State Persistence

import { useEffect } from "react";

export function useGlobalState() {
  const { agent } = useAgent({ agentId: "car_sales" });
  const state = agent.state as AppState || loadStateFromLocalStorage() || defaultState;
  const setState = (newState: AppState) => agent.setState(newState);

  // Persist state to localStorage
  useEffect(() => {
    localStorage.setItem("car_sales_state", JSON.stringify(state));
  }, [state]);

  return { state, setState };
}

function loadStateFromLocalStorage(): AppState | null {
  try {
    const saved = localStorage.getItem("car_sales_state");
    return saved ? JSON.parse(saved) : null;
  } catch {
    return null;
  }
}

Undo/Redo Functionality

export function useGlobalStateWithHistory() {
  const { agent } = useAgent({ agentId: "car_sales" });
  const state = agent.state as AppState || defaultState;

  const [history, setHistory] = useState<AppState[]>([state]);
  const [currentIndex, setCurrentIndex] = useState(0);

  const setStateWithHistory = (newState: AppState) => {
    const newHistory = history.slice(0, currentIndex + 1);
    newHistory.push(newState);
    setHistory(newHistory);
    setCurrentIndex(newHistory.length - 1);
    agent.setState(newState);
  };

  const undo = () => {
    if (currentIndex > 0) {
      const prevState = history[currentIndex - 1];
      setCurrentIndex(currentIndex - 1);
      agent.setState(prevState);
    }
  };

  const redo = () => {
    if (currentIndex < history.length - 1) {
      const nextState = history[currentIndex + 1];
      setCurrentIndex(currentIndex + 1);
      agent.setState(nextState);
    }
  };

  return {
    state,
    setState: setStateWithHistory,
    undo,
    redo,
    canUndo: currentIndex > 0,
    canRedo: currentIndex < history.length - 1,
  };
}

Real-World Use Cases

Form Wizards

Multi-step forms with AI assistance at each step:
  • Onboarding flows
  • Application processes
  • Survey builders
  • Checkout flows

Game State Management

Turn-based games or interactive stories:
  • Player inventory and stats
  • Game progression and levels
  • Decision trees
  • Quest tracking

Workflow Automation

Business process automation:
  • Approval workflows
  • Document processing
  • Customer support tickets
  • Project management

Educational Applications

Learning platforms with progress tracking:
  • Quiz progression
  • Learning paths
  • Skill assessments
  • Personalized tutoring

Best Practices

1. Keep State Flat

Avoid deeply nested state:
// ❌ Bad - deeply nested
interface BadState {
  user: {
    profile: {
      contact: {
        email: string;
      };
    };
  };
}

// ✅ Good - flat structure
interface GoodState {
  userEmail: string;
  userName: string;
  userPhone: string;
}

2. Use TypeScript

Enforce type safety across state updates:
interface AppState {
  stage: "step1" | "step2" | "step3";  // Union type
  data: StepData;
}

// TypeScript will error if you use invalid stage
setState({ ...state, stage: "step4" });  // Error!

3. Separate Concerns

Keep state logic separate from UI:
// ✅ Good
const { state, actions } = useGlobalState();

return (
  <div>
    <StageComponent state={state} actions={actions} />
  </div>
);

4. Memoize Expensive Computations

const sortedItems = useMemo(() => {
  return state.items.sort((a, b) => a.price - b.price);
}, [state.items]);

5. Document State Transitions

/**
 * State transition flow:
 * 
 * START
 *   ↓
 * getContactInfo (collect user details)
 *   ↓
 * buildCar (configure vehicle)
 *   ↓
 * sellFinancing (offer financing)
 *   ↓ accepted          ↓ declined
 * getFinancingInfo   getPaymentInfo
 *   ↓                   ↓
 * confirmOrder ← ← ← ← ←
 *   ↓
 * END
 */

Debugging Tips

Log State Changes

useEffect(() => {
  console.log("State changed:", state);
}, [state]);

React DevTools

Use React DevTools to inspect state:
  1. Install React DevTools browser extension
  2. Open DevTools
  3. Navigate to Components tab
  4. Find your component
  5. Inspect hooks → CoAgent state

State Snapshots

const takeSnapshot = () => {
  console.log("State snapshot:", JSON.stringify(state, null, 2));
  navigator.clipboard.writeText(JSON.stringify(state, null, 2));
};

<button onClick={takeSnapshot}>Copy State</button>

Troubleshooting

Verify:
  1. useAgent is called with correct agent ID
  2. Agent name matches in backend configuration
  3. State types are consistent
  4. CopilotKit provider wraps the app
Check:
  1. You’re calling setState with a new object reference
  2. State updates aren’t being batched incorrectly
  3. Components are properly subscribed to state changes
  4. No memoization preventing re-renders
Ensure:
  1. Stage names are consistent across the application
  2. Validation logic allows progression
  3. All required data is collected before transition
  4. No async operations blocking the transition

Next Steps

Multi-Agent

Combine shared state with multi-agent systems

Generative UI

Create state-driven generative UI

useAgent

Complete API reference

Human-in-the-Loop

Add approval workflows to state transitions