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:
- Widget Opens
- Before Submit
- Response Received
- Error (if any)
- Chat Clear (if triggered)
- 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.
Related Documentation
- 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