Docs
Event Callbacks

Event Callbacks

Learn how to listen to widget lifecycle events and execute custom functions during different stages of the chat widget's behavior.

The chat widget provides a comprehensive event callback system that allows you to hook into various stages of the widget's behavior. This enables you to implement custom analytics, error handling, business logic, and user experience enhancements.

Overview

Event callbacks are triggered at specific moments during the widget's operation, following this typical sequence:

  1. Widget Opens
  2. Before Submit
  3. Response Received
  4. Error (if any)
  5. Chat Clear (if triggered)
  6. Widget Close

Our callback registry approach works by creating a global JavaScript object containing your event handler functions, then referencing that object in your widget configuration. The widget will automatically call your functions when the corresponding events occur.

Setting Up Event Callbacks

Step 1: Create Your Callback Registry Object

First, create a global JavaScript object containing your event handler functions:

<script>
  // Create your callback registry object (must be defined before widget initialization)
  window.myChatCallbacks = {
    // Called when widget opens
    onWidgetOpen: function (data) {
      console.log("Widget opened:", data);
    },
 
    // Called before each message submission
    beforeSubmit: function (data) {
      console.log("Before submit:", data);
      // Modify metadata if needed
      data.metadata.customField = "Modified by beforeSubmit";
      return data; // Always return the data object
    },
 
    // Called after receiving response from backend
    onResponseReceived: function (data) {
      console.log("Response received:", data);
    },
 
    // Called when any error occurs
    onError: function (data) {
      console.error("Error occurred:", data);
    },
 
    // Called when chat is cleared
    onChatClear: function (data) {
      console.log("Chat cleared:", data);
    },
 
    // Called when widget closes
    onWidgetClose: function (data) {
      console.log("Widget closed:", data);
    },
  };
</script>

Step 2: Initialize & Reference Your Callback Registry in Widget Configuration

Then, specify the name of your callback registry in your widget embed code:

<script type="module">
  import Chatbot from "https://cdn.n8nchatui.com/v1/embed.js";
 
  Chatbot.init({
    n8nChatUrl: "http://yourdomain.com/webhook/your-webhook-id/chat",
    callbackRegistry: "myChatCallbacks", // Name of your callback registry object
    // ...other properties
    theme: {
      button: {
        backgroundColor: "#ffc8b8",
        right: 20,
        bottom: 20,
        size: 50,
        iconColor: "#373434",
        customIconSrc: "https://www.svgrepo.com/show/339963/chat-bot.svg",
        customIconSize: 60,
        customIconBorderRadius: 15,
        autoWindowOpen: {
          autoOpen: true,
          openDelay: 3,
        },
        borderRadius: "rounded",
      },
      chatWindow: {
        borderRadiusStyle: "rounded",
        avatarBorderRadius: 25,
        messageBorderRadius: 6,
        showTitle: true,
        title: "N8N Chat UI Bot",
        titleAvatarSrc: "https://www.svgrepo.com/show/339963/chat-bot.svg",
        avatarSize: 40,
        welcomeMessage: "Hello! This is the default welcome message",
        errorMessage: "Please connect me to n8n first",
        backgroundColor: "#ffffff",
        height: 600,
        width: 400,
        fontSize: 16,
        showScrollbar: false,
        botMessage: {
          backgroundColor: "#f36539",
          textColor: "#fafafa",
          showAvatar: true,
          avatarSrc: "https://www.svgrepo.com/show/334455/bot.svg",
        },
        userMessage: {
          backgroundColor: "#fff6f3",
          textColor: "#050505",
          showAvatar: true,
          avatarSrc: "https://www.svgrepo.com/show/532363/user-alt-1.svg",
        },
        textInput: {
          placeholder: "Type your query",
          backgroundColor: "#ffffff",
          textColor: "#1e1e1f",
          sendButtonColor: "#f36539",
          maxChars: 50,
          maxCharsWarningMessage:
            "You exceeded the characters limit. Please input less than 50 characters.",
          autoFocus: false,
          borderRadius: 6,
          sendButtonBorderRadius: 50,
        },
      },
    },
  });
</script>

Step 3: Test Your Implementation

All callback functions support both synchronous and asynchronous execution. Open your browser's developer console to see the event logs when interacting with the widget.

Setting Up Event Callbacks for Hosted Widgets

For hosted widgets, you can register event callbacks by simply adding the callbackRegistry:

<script>
  n8nChatUiWidget.load({
    callbackRegistry: "myChatCallbacks", // Reference to our callback registry
    // ...other overriding properties
  });
</script>

Then create your callback registry object as usual by following Step 2 above.

Available Event Callbacks

1. onWidgetOpen

When triggered: Called when the chat widget opens.

Input: Function parameter structure you'll receive:

{
  timestamp: string; // ISO timestamp when widget opened
  openMethod: "user_click" | "programmatic" | "auto_open"; // How widget was opened
  sessionId: string; // Current chat session ID
}

Output: No return value expected (void).

Example (supports async):

onWidgetOpen: async function(data) {
  console.log('Widget opened via:', data.openMethod);
 
  // Async example: Track analytics
  await fetch('/api/track-widget-open', {
    method: 'POST',
    body: JSON.stringify({ sessionId: data.sessionId, method: data.openMethod })
  });
}

2. beforeSubmit

When triggered: Called before each message submission, allowing you to modify the metadata before it's sent to your backend.

Use cases:

  • Adding authentication tokens
  • Enriching metadata with user context
  • Adding custom tracking data
  • Implementing validation logic

Important: It's the only callback that allows you to modify the metadata that will be sent to your backend. You cannot modify the user's input text, only the metadata object.

Input: Function parameter structure you'll receive:

{
  input: string; // The user's message text (read-only)
  uploads: IUploads; // Any uploaded files (read-only)
  metadata: Record<string, unknown>; // Current metadata object (modifiable)
  sessionId: string; // Current chat session ID
  timestamp: string; // ISO timestamp when event triggered
}

Output: You must return the modified data object. The metadata property is the only modifiable field.

Example (supports async):

beforeSubmit: async function(data) {
  console.log('Message about to be sent:', data.input);
 
  // Async example: Add user context
  try {
    const userProfile = await fetch('/api/user-profile').then(r => r.json());
    data.metadata.userId = userProfile.id;
  } catch (error) {
    console.log('Could not fetch user profile');
  }
 
  // Add page context
  data.metadata.currentPage = window.location.pathname;
 
  return data; // Always return the data object
}

3. onResponseReceived

When triggered: Called after receiving a successful response from your backend, before displaying it to the user.

Input: Function parameter structure you'll receive:

{
  response: Record<string, unknown>; // Full response from backend
  metadata: Record<string, unknown>; // Metadata that was sent with request
  sessionId: string; // Current chat session ID
  timestamp: string; // ISO timestamp when response received
}

Output: No return value expected (void).

Example (supports async):

onResponseReceived: async function(data) {
  console.log('Response received:', data.response);
 
  // Async example: Track response analytics
  await fetch('/api/track-response', {
    method: 'POST',
    body: JSON.stringify({
      sessionId: data.sessionId,
      responseLength: JSON.stringify(data.response).length
    })
  });
}

4. onError

When triggered: Called when any error occurs while submitting the chat input or in the response.

Input: Function parameter structure you'll receive:

{
  error: Error; // The actual error object
  errorType: 'network' | 'validation' | 'server' | 'callback' | 'unknown';
  context: 'message_submission' | 'widget_open' | 'file_upload' | 'voice_recording' | 'chat_clear' | 'response_processing';
  userMessage?: string; // User's message when error occurred (if applicable)
  sessionId: string; // Current chat session ID
  timestamp: string; // ISO timestamp when error occurred
}

Output: No return value expected (void).

Example (supports async):

onError: async function(data) {
  console.error('Widget error:', data.errorType, data.context, data.error);
 
  // Async example: Log error to backend
  await fetch('/api/log-error', {
    method: 'POST',
    body: JSON.stringify({
      error: data.error.message,
      errorType: data.errorType,
      context: data.context,
      sessionId: data.sessionId
    })
  });
}

5. onChatClear

When triggered: Called when the chat history is cleared.

Input: Function parameter structure you'll receive:

{
  sessionId: string; // The session ID that was cleared
  timestamp: string; // ISO timestamp when chat was cleared
}

Output: No return value expected (void).

Example (supports async):

onChatClear: async function(data) {
  console.log('Chat cleared for session:', data.sessionId);
 
  // Async example: Save conversation history
  await fetch('/api/save-conversation', {
    method: 'POST',
    body: JSON.stringify({
      sessionId: data.sessionId,
      clearedAt: data.timestamp
    })
  });
}

6. onWidgetClose

When triggered: Called when the chat widget closes.

This event is not available for In-Page widget types.

Input: Function parameter structure you'll receive:

{
  timestamp: string; // ISO timestamp when widget closed
  closeMethod: "user_click" | "programmatic" | "auto_close"; // How widget was closed
  sessionId: string; // Current chat session ID
}

Output: No return value expected (void).

Example (supports async):

onWidgetClose: async function(data) {
  console.log('Widget closed via:', data.closeMethod);
 
  // Async example: Track session analytics
  await fetch('/api/track-session', {
    method: 'POST',
    body: JSON.stringify({
      sessionId: data.sessionId,
      closeMethod: data.closeMethod
    })
  });
}

Best Practices

Async/Await Support

All callback functions support both synchronous and asynchronous execution:

// Synchronous callback
beforeSubmit: function(data) {
  data.metadata.timestamp = Date.now();
  return data;
},
 
// Asynchronous callback
beforeSubmit: async function(data) {
  try {
    const userProfile = await fetch('/api/user-profile').then(r => r.json());
    data.metadata.userId = userProfile.id;
  } catch (error) {
    console.log('Failed to fetch user profile');
  }
  return data;
}

Error Handling

  • Always wrap your callback logic in try-catch blocks to prevent breaking the widget
  • Log errors appropriately but don't show technical details to end users
  • Implement graceful fallbacks when third-party services are unavailable
  • For async callbacks, handle promise rejections properly
onError: async function(data) {
  try {
    // Your async error handling logic
    await fetch('/api/log-error', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  } catch (loggingError) {
    console.error('Failed to log error:', loggingError);
    // Don't throw - let the widget continue functioning
  }
}

Performance

  • Keep callback functions lightweight and fast-executing
  • For async operations, avoid blocking the UI thread unnecessarily
  • Use Promise.allSettled() for multiple parallel async operations
  • Consider using timeouts for external API calls
beforeSubmit: async function(data) {
  try {
    // Set a timeout for external API calls
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 3000);
 
    const userContext = await fetch('/api/user-context', {
      signal: controller.signal
    }).then(r => r.json());
 
    clearTimeout(timeoutId);
    data.metadata.userContext = userContext;
  } catch (error) {
    // Continue without user context if API fails
    console.log('User context unavailable');
  }
  return data;
}

Troubleshooting

Callbacks Not Triggering

  • Use console.log statements during development to understand event flow
  • Ensure the callback registry object is defined before the widget initialization
  • Check that the callbackRegistry property in widget config matches your registry object name
  • Verify that your callback functions are properly defined

TypeScript Support

If you're using TypeScript, you can define the types manually for better development experience

// Define the callback types manually
interface BeforeSubmitData {
  input: string;
  uploads: any;
  metadata: Record<string, unknown>;
  sessionId: string;
  timestamp: string;
}
 
interface WidgetOpenData {
  timestamp: string;
  openMethod: "user_click" | "programmatic" | "auto_open";
  sessionId: string;
}
 
// Define callback function types
type BeforeSubmitCallback = (
  data: BeforeSubmitData,
) => BeforeSubmitData | Promise<BeforeSubmitData>;
type WidgetOpenCallback = (data: WidgetOpenData) => void | Promise<void>;
 
const callbacks: {
  beforeSubmit: BeforeSubmitCallback;
  onWidgetOpen: WidgetOpenCallback;
} = {
  beforeSubmit: (data) => {
    // TypeScript will provide full type checking here
    return data;
  },
  // ... other callbacks
};

For the remaining callbacks, refer to the data structure examples provided in each callback section above and define similar interfaces based on those types.

  • Metadata Configuration - Learn about sending custom data to your n8n workflows and how it integrates with event callbacks
  • Session Management - Understand how to manage separate chat conversations across different contexts
  • Backend Integration - Connect to your backend (n8n or custom) and learn about response formats