back to tutorials

Modernizing Legacy Applications with AI Widgets

Learn how to breathe new life into legacy applications by adding AI agents through lightweight ChatBotKit AI Widget integration, enabling modern conversational interfaces without complex rebuilds.

Legacy applications are the backbone of many organizations, containing decades of business logic and institutional knowledge. However, they often suffer from outdated user interfaces, limited accessibility, and poor user experience. Complete system rewrites are costly, risky, and time-consuming. What if you could modernize these applications incrementally by adding intelligent AI agents that provide a natural language interface to existing functionality?

In this tutorial, you'll learn how to use ChatBotKit AI Widgets to create a lightweight integration layer that modernizes legacy applications without touching the underlying codebase. This approach, sometimes called an "intelligent overlay," allows you to extend system lifespan while providing modern user experiences.

In This Tutorial

You'll Learn How To:

  • Understand the intelligent overlay approach to legacy modernization
  • Set up a ChatBotKit AI Widget as a conversational interface
  • Use client-side functions to bridge the widget with legacy application functionality
  • Connect to both client-side UI components and server-side APIs
  • Create a seamless user experience that masks the complexity of legacy systems
  • Implement this pattern without major infrastructure changes

By the end of this tutorial, you'll have a working example of an AI agent that can interact with a legacy application, demonstrating a practical approach to incremental modernization.

Why This Approach Works

The Legacy System Challenge

Enterprise organizations face a significant dilemma with their legacy systems:

  • Valuable but Outdated: These systems contain critical business logic but lack modern UX
  • High Maintenance Costs: According to industry research, approximately 74% of enterprise IT budgets are dedicated to maintaining existing systems rather than innovation
  • Risk of Complete Replacement: Traditional replatforming or refactoring approaches require significant investment and carry substantial business risk

The Intelligent Overlay Solution

Instead of replacing or rewriting, you can:

  1. Preserve existing investments in legacy systems
  2. Add a modern AI interface that users interact with naturally
  3. Bridge functionality using lightweight JavaScript integration
  4. Minimize disruption to current operations
  5. Modernize incrementally by adding capabilities over time

ChatBotKit AI Widgets provide the perfect foundation for this approach because they:

  • Embed easily with a single script tag
  • Run entirely in the browser (no backend changes required)
  • Support client-side functions to interact with existing code
  • Require minimal technical overhead

Before You Begin

Prerequisites

  • Active ChatBotKit account (sign up at chatbotkit.com)
  • Access to your legacy application's codebase (at least the frontend HTML/JavaScript)
  • Basic understanding of JavaScript and how your application works
  • Ability to add a script tag to your application pages

What You'll Need

  • 45-60 minutes of focused time
  • A web browser with developer tools (Chrome or Firefox recommended)
  • Text editor for modifying your application code
  • Basic knowledge of your application's functionality and data structures

Understanding Your Legacy Application

Before starting, identify:

  1. Key user workflows you want to AI-enable
  2. Existing JavaScript functions or APIs available
  3. UI elements the AI might need to interact with
  4. Data access patterns (client-side state, localStorage, API calls)
  5. Security considerations for exposing functionality

Step 1: Create Your AI Widget

First, let's set up the ChatBotKit AI Widget that will serve as the intelligent interface to your legacy application.

  1. Log into your ChatBotKit account
  2. From the dashboard, click "Integrations" in the sidebar
  3. Click "Widget" from the integration types
  4. Click the "Create Widget" button

Configure the Widget

  1. Name: Give it a descriptive name

    App Assistant
    
  2. Description: Explain its purpose

    AI agent that helps users interact with...
    
  3. Bot Configuration: Either create a new bot or select an existing one

Set Up the Bot's Backstory

This is crucial - your bot needs to understand its role as a bridge to your legacy system:

You are an intelligent assistant for our legacy enterprise application.

Key Responsibilities:
- Help users search for records and retrieve information
- Assist with creating, updating, and deleting data
- Guide users through complex multi-step processes
- Provide contextual help and documentation
- Translate user intent into appropriate system actions

Guidelines:
- Always confirm before destructive operations (update, delete)
- Provide clear explanations of what actions you're taking
- If you need more information, ask clarifying questions
- When showing results, format them in a readable way
- Help users understand the legacy system's limitations

Click "Create" or "Save" to create your widget

Get Your Widget ID

After creation, note your Widget ID - you'll need this for embedding. It looks like:

clchog8pw0001nwfirx0ymw3g

Step 2: Embed the Widget in Your Legacy Application

Now let's add the AI widget to your legacy application. This is surprisingly simple and non-invasive.

Add the Widget Script

In your legacy application's HTML, add this script tag before the closing </body> tag:

<!-- Add ChatBotKit AI Widget -->
<script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"
        data-widget="YOUR_WIDGET_ID">
</script>

Replace YOUR_WIDGET_ID with your actual widget ID.

Alternative: Manual Instantiation

For more control, you can initialize the widget manually:

<!-- Embed the Widget SDK -->
<script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"></script>

<!-- Instantiate widget within your application -->
<chatbotkit-widget
  widget="YOUR_WIDGET_ID"
/>

Step 3: Create Client-Side Functions

This is where the magic happens. Client-side functions allow your AI agent to interact with your legacy application's JavaScript code and APIs.

Understanding Client-Side Functions

Client-side functions are JavaScript functions you define that the AI can invoke to:

  • Query application state (read current user data, preferences, etc.)
  • Manipulate the UI (open modals, fill forms, navigate pages)
  • Call legacy APIs (CRUD operations on your backend)
  • Access browser storage (localStorage, sessionStorage, cookies)
  • Interact with third-party libraries (jQuery, legacy frameworks, etc.)

Basic Function Structure

Each client-side function has:

  • description: Explains what the function does (AI uses this to decide when to call it)
  • parameters: JSON Schema defining expected input parameters
  • handler or result: Either a function to execute or static data to return

Example 1: Read-Only Data Access

Let's start simple - allowing the AI to read current user information:

// Wait for widget to be ready
window.addEventListener('load', function() {
  // Get the widget instance
  const widgetInstance = window.chatbotkitWidget.instance;

  // Define client-side functions
  widgetInstance.functions = {
    getCurrentUser: {
      description: 'Get information about the currently logged-in user',
      parameters: {},
      result: {
        data: {
          userId: window.currentUser.id,
          username: window.currentUser.name,
          email: window.currentUser.email,
          role: window.currentUser.role,
          department: window.currentUser.department
        }
      }
    }
  };
});

Example 2: Search Legacy Database

Allow the AI to search your legacy database:

widgetInstance.functions = {
  ...widgetInstance.functions, // Keep existing functions

  searchRecords: {
    description: 'Search for records in the legacy database. Returns matching customer records.',
    parameters: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'Search query string to match against customer names, IDs, or emails'
        },
        limit: {
          type: 'number',
          description: 'Maximum number of results to return (default: 10)',
          default: 10
        }
      },
      required: ['query']
    },
    handler: async ({ query, limit = 10 }) => {
      try {
        const response = await fetch(`/api/legacy/search?q=${encodeURIComponent(query)}&limit=${limit}`);

        const data = await response.json();

        return {
          success: true,
          results: data.records,
          count: data.records.length
        };
      } catch (error) {
        return {
          success: false,
          error: 'Failed to search records: ' + error.message
        };
      }
    }
  }
};

Example 3: Update Legacy Data

Allow the AI to modify existing records (with proper validation):

widgetInstance.functions = {
  ...widgetInstance.functions,

  updateRecord: {
    description: 'Update an existing record in the legacy system. Use this after confirming with the user.',
    parameters: {
      type: 'object',
      properties: {
        recordId: {
          type: 'string',
          description: 'The unique identifier of the record to update'
        },
        fields: {
          type: 'object',
          description: 'Object containing field names and new values to update',
          properties: {
            name: { type: 'string' },
            email: { type: 'string' },
            phone: { type: 'string' },
            status: { type: 'string', enum: ['active', 'inactive', 'pending'] }
          }
        }
      },
      required: ['recordId', 'fields']
    },
    handler: async ({ recordId, fields }) => {
      try {
        // validate the update

        if (!recordId || !fields || Object.keys(fields).length === 0) {
          throw new Error('Invalid update parameters');
        }

        // call your legacy update API

        const response = await fetch(`/api/legacy/records/${recordId}`, {
          method: 'PUT',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
          },
          body: JSON.stringify(fields)
        });

        if (!response.ok) {
          throw new Error(`Update failed: ${response.statusText}`);
        }

        const result = await response.json();

        return {
          success: true,
          message: 'Record updated successfully',
          record: result
        };
      } catch (error) {
        return {
          success: false,
          error: error.message
        };
      }
    }
  }
};

Example 4: Navigate Legacy UI

Help users navigate complex legacy interfaces:

widgetInstance.functions = {
  ...widgetInstance.functions,

  navigateToPage: {
    description: 'Navigate the user to a specific page or section in the legacy application',
    parameters: {
      type: 'object',
      properties: {
        pageName: {
          type: 'string',
          description: 'The page to navigate to',
          enum: ['dashboard', 'customers', 'orders', 'reports', 'settings']
        },
        recordId: {
          type: 'string',
          description: 'Optional record ID to view a specific item'
        }
      },
      required: ['pageName']
    },
    handler: async ({ pageName, recordId }) => {
      // build the URL based on your legacy routing

      let url = `/legacy/${pageName}`;

      if (recordId) {
        url += `/${recordId}`;
      }

      // navigate to the page

      window.location.href = url;

      return {
        success: true,
        message: `Navigating to ${pageName}...`
      };
    }
  }
};

Example 5: Interact with Legacy UI Components

Manipulate existing UI elements (useful for filling forms):

widgetInstance.functions = {
  ...widgetInstance.functions,

  fillCustomerForm: {
    description: 'Pre-fill the customer creation form with provided data',
    parameters: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        email: { type: 'string' },
        phone: { type: 'string' },
        company: { type: 'string' }
      }
    },
    handler: async ({ name, email, phone, company }) => {
      try {
        // find and fill form fields (adjust selectors for your legacy app)

        if (name) document.getElementById('customer_name').value = name;
        if (email) document.getElementById('customer_email').value = email;
        if (phone) document.getElementById('customer_phone').value = phone;
        if (company) document.getElementById('customer_company').value = company;

        // trigger change events if needed for legacy validation

        document.getElementById('customer_name').dispatchEvent(new Event('change'));

        return {
          success: true,
          message: 'Form filled successfully'
        };
      } catch (error) {
        return {
          success: false,
          error: 'Failed to fill form: ' + error.message
        };
      }
    }
  }
};

Step 4: Complete Integration Example

Here's a complete example showing how to set up the AI widget with multiple functions in a legacy application:

<!DOCTYPE html>
<html>
<head>
  <title>Legacy Application</title>
  <meta name="csrf-token" content="your-csrf-token">
  <!-- Your existing legacy app CSS and scripts -->
</head>
<body>
  <!-- Your existing legacy application HTML -->
  <div id="legacy-app">
    <!-- Legacy application content -->
  </div>

  <!-- ChatBotKit AI Widget Integration -->
  <script src="<https://static.chatbotkit.com/integrations/widget/v2.js>"
          data-widget="YOUR_WIDGET_ID">
  </script>

  <script>
    // wait for both legacy app and widget to be ready
    window.addEventListener('load', function() {
      // ensure widget is loaded

      if (!window.chatbotkitWidget || !window.chatbotkitWidget.instance) {
        console.error('ChatBotKit widget not loaded');

        return;
      }

      const widgetInstance = window.chatbotkitWidget.instance;

      // define all client-side functions
      widgetInstance.functions = {
        // read current user information

        getCurrentUser: {
          description: 'Get information about the currently logged-in user',
          parameters: {},
          result: {
            data: {
              userId: window.currentUser?.id || 'unknown',
              username: window.currentUser?.name || 'Guest',
              email: window.currentUser?.email || '',
              role: window.currentUser?.role || 'user'
            }
          }
        },

        // search records
        searchRecords: {
          description: 'Search for customer records in the database',
          parameters: {
            type: 'object',
            properties: {
              query: {
                type: 'string',
                description: 'Search query'
              },
              limit: {
                type: 'number',
                description: 'Maximum results to return',
                default: 10
              }
            },
            required: ['query']
          },
          handler: async ({ query, limit = 10 }) => {
            try {
              const response = await fetch(
                `/api/legacy/search?q=${encodeURIComponent(query)}&limit=${limit}`
              );

              const data = await response.json();

              return {
                success: true,
                results: data.records,
                count: data.records.length
              };
            } catch (error) {
              return {
                success: false,
                error: error.message
              };
            }
          }
        },

        // fet record details

        getRecordDetails: {
          description: 'Retrieve detailed information about a specific record',
          parameters: {
            type: 'object',
            properties: {
              recordId: {
                type: 'string',
                description: 'The record ID to retrieve'
              }
            },
            required: ['recordId']
          },
          handler: async ({ recordId }) => {
            try {
              const response = await fetch(`/api/legacy/records/${recordId}`);

              const data = await response.json();

              return {
                success: true,
                record: data
              };
            } catch (error) {
              return {
                success: false,
                error: error.message
              };
            }
          }
        },

        // update record

        updateRecord: {
          description: 'Update an existing record. Always confirm with user first.',
          parameters: {
            type: 'object',
            properties: {
              recordId: { type: 'string' },
              fields: { type: 'object' }
            },
            required: ['recordId', 'fields']
          },
          handler: async ({ recordId, fields }) => {
            try {
              const response = await fetch(`/api/legacy/records/${recordId}`, {
                method: 'PUT',
                headers: {
                  'Content-Type': 'application/json',
                  'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
                },
                body: JSON.stringify(fields)
              });

              if (!response.ok) throw new Error('Update failed');

              const result = await response.json();

              return {
                success: true,
                message: 'Record updated successfully',
                record: result
              };
            } catch (error) {
              return {
                success: false,
                error: error.message
              };
            }
          }
        },

        // navigate to pages

        navigateToPage: {
          description: 'Navigate to a specific page in the application',
          parameters: {
            type: 'object',
            properties: {
              pageName: {
                type: 'string',
                enum: ['dashboard', 'customers', 'orders', 'reports']
              },
              recordId: { type: 'string' }
            },
            required: ['pageName']
          },
          handler: async ({ pageName, recordId }) => {
            let url = `/legacy/${pageName}`;

            if (recordId) url += `/${recordId}`;

            window.location.href = url;

            return {
              success: true,
              message: `Navigating to ${pageName}...`
            };
          }
        }
      };

      console.log('ChatBotKit AI Widget functions registered successfully');
    });
  </script>
</body>
</html>

Step 5: Test Your AI-Enabled Legacy Application

Now it's time to test the integration and see your AI agent interact with your legacy system.

Initial Testing

  1. Open your legacy application in a web browser
  2. Look for the widget button (usually bottom-right corner)
  3. Click to open the chat interface

Test Basic Interactions

Try these example conversations:

Example 1: Get User Information

User: Who am I logged in as?
AI: Let me check... You're logged in as John Smith (john.smith@company.com) with the role of Manager in the Sales department.

Example 2: Search for Records

User: Find all customers named Smith
AI: I found 3 customers matching "Smith":
1. John Smith - john@example.com - Active
2. Jane Smith - jane@example.com - Active
3. Bob Smith - bob@example.com - Inactive

Would you like details on any of these?

Example 3: Update a Record

User: Update customer 12345's email to newemail@example.com
AI: Just to confirm, you want to update customer 12345's email address to newemail@example.com. Is that correct?

User: Yes
AI: Record updated successfully! Customer 12345 now has the email address newemail@example.com.

Example 4: Navigate the App

User: Take me to the reports page
AI: Navigating to the reports page now...
[User is redirected to /legacy/reports]

Advanced Testing

Test error handling and edge cases:

  • Invalid input: "Update customer xyz" (should ask for clarification)
  • Permissions: Try actions the current user shouldn't be able to perform
  • Network errors: Test with network throttling enabled
  • Empty results: Search for non-existent records

Step 6: Enhance the User Experience

Now that the basic integration works, let's make it more sophisticated.

Add Contextual Help

Extend your bot's backstory to provide guidance:

When users seem confused or ask "how do I...", provide step-by-step guidance:

Example workflows:
- To create a new customer: Navigate to the customers page, click "Add New", fill in required fields (name, email), and click "Save"
- To generate a report: Go to the reports page, select date range, choose report type, and click "Generate"

If a user is struggling with a task, offer to help them complete it through our conversation instead.

Implement Confirmation Patterns

For destructive operations, always confirm:

deleteRecord: {
  description: 'Delete a record. MUST confirm with user first by asking "Are you sure you want to delete record [ID]? This cannot be undone."',
  parameters: {
    type: 'object',
    properties: {
      recordId: { type: 'string' },
      confirmed: {
        type: 'boolean',
        description: 'Set to true only after user explicitly confirms'
      }
    },
    required: ['recordId', 'confirmed']
  },
  handler: async ({ recordId, confirmed }) => {
    if (!confirmed) {
      return {
        success: false,
        error: 'Operation requires user confirmation'
      };
    }

    // proceed with deletion...
  }
}

Add Loading States

Provide feedback during long operations:

handler: async ({ query }) => {
  // send immediate feedback

  widgetInstance.sendMessage({
    text: 'Searching records...',
    hidden: true  // don't show in conversation history
  });

  const results = await searchDatabase(query);

  return {
    success: true,
    results: results
  };
}

Track Analytics

Monitor how users interact with the AI:

handler: async (params) => {
  // log usage

  try {
    await fetch('/api/analytics/ai-usage', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        function: 'searchRecords',
        params: params,
        timestamp: new Date().toISOString(),
        userId: window.currentUser?.id
      })
    });
  } catch (e) {
    // don't fail the actual operation if analytics fails
    console.warn('Analytics logging failed:', e);
  }

  // proceed with actual function logic
  // ...
}

Best Practices and Considerations

Security

  1. Never expose sensitive functions without proper authentication checks
  2. Validate all inputs on the server-side, not just in client functions
  3. Use CSRF tokens for state-changing operations
  4. Implement rate limiting to prevent abuse
  5. Log all AI-triggered actions for audit trails
handler: async ({ recordId, fields }) => {
  // validate authentication
  if (!window.currentUser?.authenticated) {
    return {
      success: false,
      error: 'Authentication required'
    };
  }

  // check permissions
  if (!window.currentUser.hasPermission('update_records')) {
    return {
      success: false,
      error: 'Insufficient permissions'
    };
  }

  // validate input
  if (!recordId || typeof recordId !== 'string') {
    return {
      success: false,
      error: 'Invalid record ID'
    };
  }

  // proceed with operation...
}

Error Handling

Provide helpful error messages:

handler: async (params) => {
  try {
    // operation logic

    const result = await performOperation(params);

    return { success: true, data: result };
  } catch (error) {
    // log error for debugging

    console.error('Function error:', error);

    // return user-friendly message

    if (error.message.includes('network')) {
      return {
        success: false,
        error: 'Network connection issue. Please check your connection and try again.'
      };
    } else if (error.message.includes('permission')) {
      return {
        success: false,
        error: 'You don\\'t have permission to perform this action. Please contact your administrator.'
      };
    } else {
      return {
        success: false,
        error: 'An unexpected error occurred. Please try again or contact support.'
      };
    }
  }
}

Gradual Migration Strategy

Don't try to AI-enable everything at once:

Phase 1: Read-Only Functions

  • User information retrieval
  • Search and browse functionality
  • Reporting and analytics viewing

Phase 2: Guided Navigation

  • Help users find features
  • Provide contextual documentation
  • Navigate to specific pages/sections

Phase 3: Data Modification

  • Simple updates (status changes, etc.)
  • Form pre-filling
  • Bulk operations with confirmation

Phase 4: Complex Workflows

  • Multi-step processes
  • Conditional logic
  • Integration with external systems

Troubleshooting

Widget Not Appearing

Problem: The chat widget doesn't show up on the page.

Solutions:

  1. Check browser console for JavaScript errors
  2. Verify the widget ID is correct
  3. Ensure the script tag is properly placed
  4. Check if your legacy app's Content Security Policy blocks the widget
  5. Try loading the page in an incognito window (rule out extensions)
// debug widget loading
console.log('ChatBotKit widget global:', window.chatbotkitWidget);
console.log('Widget instance:', window.chatbotkitWidget?.instance);

Functions Not Being Called

Problem: The AI doesn't invoke your client-side functions.

Solutions:

  1. Check function descriptions are clear and specific
  2. Verify parameters schema is correct
  3. Ensure functions are registered before user interactions
  4. Update bot backstory to mention available functions explicitly
  5. Test with direct function calls in browser console
// test function directly
window.chatbotkitWidget.instance.functions.searchRecords.handler({
  query: 'test',
  limit: 5
}).then(result => console.log('Function result:', result));

API Calls Failing

Problem: Legacy API calls from functions fail.

Solutions:

  1. Check CORS settings on your legacy backend
  2. Verify authentication tokens are being sent
  3. Ensure API endpoints are accessible from the browser
  4. Check for CSRF token requirements
  5. Look for SSL/HTTPS issues
// debug API calls
handler: async (params) => {
  console.log('Calling API with params:', params);

  try {
    const response = await fetch('/api/legacy/endpoint', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(params)
    });

    console.log('API response status:', response.status);
    console.log('API response headers:', response.headers);

    const data = await response.json();

    console.log('API response data:', data);

    return { success: true, data };
  } catch (error) {
    console.error('API call failed:', error);

    return { success: false, error: error.message };
  }
}

AI Misunderstanding User Intent

Problem: The AI calls wrong functions or misinterprets requests.

Solutions:

  1. Make function descriptions more specific
  2. Add examples to your bot's backstory
  3. Use parameter descriptions to guide the AI
  4. Consider adding a validation step before executing
  5. Refine the bot's backstory with clearer instructions

Next Steps

Congratulations! You've successfully created an AI overlay for your legacy application. Here's what to explore next:

Immediate Next Steps

  1. Expand function coverage: Add more client-side functions for additional features
  2. Refine bot behavior: Update the backstory based on user interactions
  3. Add analytics: Track which functions are most used
  4. Gather user feedback: Survey users about their AI experience

Advanced Capabilities

  • Multi-language support: Enable the widget in multiple languages
  • Voice interaction: Add voice input/output for hands-free operation
  • Mobile optimization: Ensure the widget works well on mobile devices
  • Integration with other tools: Connect to Slack, Teams, or other platforms

Learning Resources

Conclusion

Modernizing legacy applications doesn't have to mean complete rewrites or massive infrastructure changes. By using ChatBotKit AI Widgets with client-side functions, you can create an intelligent overlay that:

  • Improves user experience dramatically without touching legacy code
  • Reduces training costs by providing natural language interfaces
  • Extends system lifespan while planning long-term modernization
  • Minimizes risk through incremental, reversible changes
  • Delivers quick wins that demonstrate value to stakeholders

This approach works because it meets users where they are - providing a modern, conversational interface while leveraging the robust business logic already present in your legacy systems.

Start small, prove value, then expand. Your legacy applications may be old, but with AI, they can learn new tricks.

Frequently Asked Questions

Does this approach work with very old technologies (e.g., Java applets, Flash)?

Yes, as long as you can add JavaScript to the page. The AI widget communicates through JavaScript, so even if the legacy app uses outdated tech, you can still create functions that interact with it.

What if my legacy application is desktop-based (not web)?

You have a few options:

  1. Embed the widget frame in a WebView within the desktop app
  2. Create a companion web interface with the AI widget
  3. Use the ChatBotKit SDK to build a native chat interface
How do I handle authentication and session management?

The widget runs in the browser with the user's existing session. Your client-side functions can access the same cookies and tokens the legacy app uses. For additional security, implement server-side proxy endpoints.

Can multiple users interact with the AI simultaneously?

Yes, each user gets their own widget instance with their own session. The AI can access user-specific data through client-side functions.

What about GDPR and data privacy?

ChatBotKit is GDPR compliant. You control what data the AI accesses through your function definitions. See our privacy features guide.

How much does this cost?

ChatBotKit offers flexible pricing based on usage. You can start with a trial to test this approach.

What if my legacy system's API is slow?

Implement caching in your client-side functions, show loading states to users, and consider running heavy operations asynchronously with notifications when complete.