Skip to Content
Chat WidgetMessage Interceptor

Message Interceptor

The Message Interceptor API allows you to intercept, modify, or handle chat messages before they reach the Nexus backend. This enables custom logic, message routing, enrichment, validation, and more.

Overview

The message interceptor is a powerful feature that gives you complete control over message flow:

  • Intercept messages before they reach the backend
  • Handle messages locally without backend calls
  • Enrich messages with additional metadata
  • Route messages based on content or conditions
  • Validate input and provide immediate feedback
  • Implement custom logic for specific use cases

Basic Usage

Configure the message interceptor in the advanced section:

const widget = window.NexusChatWidget.init({
  experienceId: "your-experience-id",
  apiUrl: "https://nexus-api.uat.knowbl.com/api/v2",
  advanced: {
    messageInterceptor: async (message, context) => {
      // Your custom logic here
      console.log("User message:", message.text);

      // Return null to let Nexus handle it
      return null;
    },
  },
});

API Reference

Interceptor Function

async function messageInterceptor(
  message: Message,
  context: Context,
): Promise<InterceptorResult | null>;

Parameters

message Object

The user’s message with the following properties:

interface Message {
  text: string; // The message text
  metadata?: Record<string, any>; // Optional metadata
  role: "user"; // Always "user" for intercepted messages
}

context Object

Contextual information about the current session:

interface Context {
  /** Current session ID (null if no session started yet) */
  sessionId: string | null;

  /** Experience ID from widget config */
  experienceId: string;

  /** Optional user ID from widget config */
  userId?: string;

  /** Direct access to chat store */
  store: {
    events: {
      value: Event[]; // Array of all session events
    };
    addEvent: (event: Event) => void; // Add custom events
  };

  /** Optional metadata from widget config */
  metadata?: Record<string, any>;

  /** Whether the message is being sent during an agent handover */
  inAgentHandover: boolean;
}

Return Values

The interceptor can return one of several values:

1. Return null - Pass Through

Let Nexus handle the message normally:

return null;

2. Return Modified Message

Enrich or modify the message before sending to Nexus:

return {
  handled: false, // false = send to Knowbl
  message: {
    text: message.text,
    metadata: {
      customField: "value",
      timestamp: new Date().toISOString(),
    },
  },
};

3. Return Custom Response

Handle the message locally without backend call:

return {
  handled: true, // true = don't send to Knowbl
  response: {
    text: "Your custom response",
    role: "assistant",
    format: "markdown", // optional: "text" or "markdown"
    metadata: {
      // optional metadata
      source: "interceptor",
    },
  },
};

4. Return Error

Return an error message to the user:

return {
  handled: true,
  error: "Unable to process your request",
  format: "markdown", // optional
};

Configuration Options

Interceptor Timeout

Set a timeout for the interceptor function (default: 5000ms):

{
  advanced: {
    messageInterceptor: async (message, context) => {
      // Your logic
    },
    interceptorTimeout: 10000, // 10 seconds
  },
}

If the interceptor exceeds the timeout, the widget will show an error and the message will not be sent to Nexus.

Common Patterns

Pattern 1: No Interceptor

Default behavior - all messages go directly to Nexus:

window.NexusChatWidget.init({
  experienceId: "demo-experience",
  apiUrl: "https://nexus-api.uat.knowbl.com/api/v2",
  // No messageInterceptor configured
});

Pattern 2: Custom Response

Handle all messages locally without backend calls:

{
  advanced: {
    messageInterceptor: async (message, context) => {
      console.log("[Interceptor] Handling:", message.text);

      // Simulate thinking delay
      await new Promise((resolve) => setTimeout(resolve, 500));

      // Return custom response (no backend call)
      return {
        handled: true,
        response: {
          text: `Custom response to: "${message.text}"`,
          role: "assistant",
          metadata: {
            source: "custom-interceptor",
            timestamp: new Date().toISOString(),
          },
        },
      };
    },
  },
}

Pattern 3: Message Enrichment

Add metadata to messages before sending to Nexus:

{
  advanced: {
    messageInterceptor: async (message, context) => {
      console.log("[Interceptor] Enriching:", message.text);

      // Add metadata and continue to Nexus
      return {
        handled: false,
        message: {
          text: message.text,
          metadata: {
            ...message.metadata,
            pageUrl: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString(),
            sessionLength: context.store.events.value.length,
          },
        },
      };
    },
  },
}

Pattern 4: Conditional Routing

Route messages based on content:

{
  advanced: {
    messageInterceptor: async (message, context) => {
      // Handle "help" messages locally
      if (message.text.toLowerCase().includes("help")) {
        return {
          handled: true,
          response: {
            text:
              "**Help Menu**\n\n" +
              '- Type "help" for this menu\n' +
              "- Type anything else to talk to Nexus AI\n" +
              `- Current session has ${context.store.events.value.length} events`,
            role: "assistant",
            format: "markdown",
          },
        };
      }

      // All other messages go to Nexus
      return null;
    },
  },
}

Pattern 5: Error Handling

Demonstrate various error scenarios:

{
  advanced: {
    messageInterceptor: async (message, context) => {
      let messageCount = 0;
      messageCount++;

      // First message: Simulate timeout (caught after 5s)
      if (messageCount === 1) {
        await new Promise((resolve) => setTimeout(resolve, 10000));
      }

      // Second message: Throw error
      if (messageCount === 2) {
        throw new Error("Simulated interceptor error!");
      }

      // Third message: Return error with custom format
      if (messageCount === 3) {
        return {
          handled: true,
          error:
            "**Custom Error**: Unable to process your request.\n\n" +
            "Please try again or [contact support](https://example.com/support).",
          format: "markdown",
        };
      }

      // Fourth+ messages: Normal behavior
      return {
        handled: true,
        response: {
          text: "All error scenarios demonstrated.",
          role: "assistant",
        },
      };
    },
    interceptorTimeout: 5000, // 5 second timeout
  },
}

Use Cases

Input Validation

Validate user input before processing:

messageInterceptor: async (message) => {
  // Check for profanity or inappropriate content
  if (containsProfanity(message.text)) {
    return {
      handled: true,
      error: "Please keep the conversation professional.",
    };
  }

  // Check message length
  if (message.text.length > 1000) {
    return {
      handled: true,
      error: "Message too long. Please keep it under 1000 characters.",
    };
  }

  // Pass through to Nexus
  return null;
};

FAQ Handling

Handle frequently asked questions locally:

const faqs = {
  hours: "We're open Monday-Friday, 9am-5pm EST.",
  pricing: "Visit our pricing page at https://example.com/pricing",
  contact: "Email us at support@example.com or call 1-800-555-0123",
};

messageInterceptor: async (message) => {
  const lowerText = message.text.toLowerCase();

  // Check for FAQ keywords
  for (const [keyword, answer] of Object.entries(faqs)) {
    if (lowerText.includes(keyword)) {
      return {
        handled: true,
        response: {
          text: answer,
          role: "assistant",
          metadata: { source: "faq" },
        },
      };
    }
  }

  // Not a FAQ, send to Knowbl
  return null;
};

User Context Enrichment

Add user information to messages:

messageInterceptor: async (message, context) => {
  // Get current user info from your app
  const user = getCurrentUser();

  return {
    handled: false,
    message: {
      text: message.text,
      metadata: {
        userId: user.id,
        userEmail: user.email,
        userTier: user.subscriptionTier,
        sessionLength: context.store.events.value.length,
        pageContext: {
          url: window.location.href,
          title: document.title,
        },
      },
    },
  };
};

Rate Limiting

Implement client-side rate limiting:

let messageTimestamps = [];
const MAX_MESSAGES_PER_MINUTE = 10;

messageInterceptor: async (message) => {
  const now = Date.now();
  const oneMinuteAgo = now - 60000;

  // Remove old timestamps
  messageTimestamps = messageTimestamps.filter((t) => t > oneMinuteAgo);

  // Check rate limit
  if (messageTimestamps.length >= MAX_MESSAGES_PER_MINUTE) {
    return {
      handled: true,
      error: "You're sending messages too quickly. Please slow down.",
    };
  }

  // Add current timestamp
  messageTimestamps.push(now);

  // Continue to Nexus
  return null;
};

A/B Testing

Route messages to different backends for testing:

messageInterceptor: async (message, context) => {
  const experimentGroup = getExperimentGroup(context.sessionId);

  return {
    handled: false,
    message: {
      text: message.text,
      metadata: {
        experiment: "model-comparison",
        group: experimentGroup, // "control" or "treatment"
        modelVersion: experimentGroup === "control" ? "v1" : "v2",
      },
    },
  };
};

Analytics Tracking

Track message patterns before sending:

messageInterceptor: async (message, context) => {
  // Send analytics event
  analytics.track("Chat Message Sent", {
    messageLength: message.text.length,
    wordCount: message.text.split(" ").length,
    sessionLength: context.store.events.value.length,
    sessionId: context.sessionId,
  });

  // Continue to Nexus
  return null;
};

Action Buttons with Confirmations

Return messages with action buttons that show confirmation messages after being clicked:

messageInterceptor: async (message, context) => {
  return {
    handled: true,
    response: {
      text: "How would you rate your experience?",
      role: "assistant",
      actions: {
        position: "bottom", // "top" | "bottom"
        alignment: "center", // "left" | "center" | "right"
        actions: [
          {
            id: "excellent",
            label: "Excellent",
            action: {
              type: "send_event",
              eventName: "survey_response",
              eventData: { rating: 5 },
            },
            behavior: {
              postClickBehavior: {
                removeOnClick: true, // Remove buttons after click
                confirmationMessage: "Thank you for your feedback!",
                confirmationMessageFormat: "markdown", // "markdown" | "html" | "text"
                confirmationMessageAlignment: "center", // "left" | "center" | "right"
                confirmationMessageColor: "#10b981", // CSS color value
                confirmationIcon: {
                  type: "emoji", // "emoji" | "svg" | "url"
                  content: "✓",
                },
                showIcon: true, // Show icon (default: true)
              },
            },
          },
        ],
      },
    },
  };
};

PostClickBehavior Options:

Inline Confirmation (appears below button):

  • confirmationMessage - Message to display after click (supports markdown/HTML)
  • confirmationMessageFormat - Format of the message: "markdown", "html", or "text" (default: "markdown")
  • confirmationMessageAlignment - Alignment: "left", "center", or "right" (default: "center")
  • confirmationMessageColor - CSS color value for the text (default: green)
  • confirmationIcon - Icon to display (emoji, SVG string, or image URL)
  • showIcon - Whether to show the icon (default: true if icon provided)

System Event (appears in chat history):

  • showSystemEvent - Show a system event in the chat history (default: false)
  • systemEventMessage - Custom message for the system event (defaults to action-specific message)
  • systemEventType - Type of system event: "info", "success", "warning", or "error" (default: "info")

Button Behavior:

  • removeOnClick - Remove all action buttons after this action is clicked

For complete documentation of ActionBehavior and PostClickBehavior properties, see the Actions Configuration reference page.

Action Types:

  • send_event - Emit a custom event (listen via widget events)
  • send_query - Send a query on behalf of the user
  • open_url - Open a URL (e.g., { type: "open_url", url: "https://...", target: "_blank" })
  • trigger_handover - Trigger agent handover

See the Action Button Confirmations Demo for 8 interactive examples.

Handling Custom Action Events

When action buttons use the send_event action type, you can listen for these events and handle them in your application:

// Initialize widget and capture the API
const widget = window.NexusChatWidget.init({
  experienceId: "your-exp-id",
  apiUrl: "https://api.example.com",
  messages: {
    welcomeMessage: "👋 Hello! What would you like to do?",
    welcomeMessageDisplay: "bubble",
    welcomeActions: {
      actions: [
        {
          id: "show-animation",
          label: "Party Time!",
          action: {
            type: "send_event",
            eventName: "show_animation",
            eventData: { source: "welcome_message" },
          },
        },
      ],
    },
  },
});

// Listen for custom action events
widget.on("customAction", (event) => {
console.log("Custom action triggered:", event);

  // event contains:
  // - eventName: string (from action.eventName)
  // - eventData: object (from action.eventData)
  // - actionId: string (button ID)
  // - timestamp: string (ISO timestamp)

  if (event.eventName === "show_animation") {
    // Handle the custom action
    showConfettiAnimation();
  }

});

function showConfettiAnimation() {
// Your custom animation logic
const confetti = document.getElementById("confetti");
confetti.classList.add("active");
setTimeout(() => confetti.classList.remove("active"), 3000);
}

Event Object Properties:

  • eventName - The name of the event (from action.eventName)
  • eventData - Optional data object (from action.eventData)
  • actionId - The ID of the action button that triggered the event
  • timestamp - ISO timestamp of when the event was triggered

Common Use Cases:

  • Show/hide UI elements (modals, animations, notifications)
  • Update application state (Redux, Vuex, etc.)
  • Trigger analytics or tracking events
  • Navigate to different sections of your app
  • Open custom forms or dialogs

See the Welcome Message Demo for a live example with a confetti animation triggered by a custom action.

Error Handling

Timeout Errors

If the interceptor exceeds interceptorTimeout, the widget will:

  1. Cancel the interceptor execution
  2. Show an error message to the user
  3. Emit an error event with type: "interceptor"
widget.on("error", (error) => {
  if (error.type === "interceptor") {
    console.error("Interceptor timed out:", error.message);
  }
});

Thrown Errors

If the interceptor throws an error:

  1. The error will be caught
  2. An error message will be shown to the user
  3. An error event will be emitted
messageInterceptor: async (message) => {
  try {
    // Your logic that might fail
    const result = await riskyOperation(message.text);
    return result;
  } catch (error) {
    console.error("Interceptor error:", error);
    return {
      handled: true,
      error: "Something went wrong. Please try again.",
    };
  }
};

Custom Error Messages

Return custom formatted error messages:

return {
  handled: true,
  error:
    "**Error**: Unable to process your request.\n\n" +
    "Please check:\n" +
    "- Your message is not empty\n" +
    "- You are not sending too many messages\n" +
    "- You have a stable internet connection\n\n" +
    "[Contact Support](https://example.com/support)",
  format: "markdown",
};

Best Practices

1. Keep It Fast

The interceptor blocks message sending, so keep it fast:

// Good - quick validation
messageInterceptor: async (message) => {
  if (message.text.length > 500) {
    return { handled: true, error: "Message too long" };
  }
  return null;
};

// Avoid - slow external API call
messageInterceptor: async (message) => {
  // This will make the chat feel slow
  const result = await fetch("https://slow-api.com/validate");
  return null;
};

2. Handle Errors Gracefully

Always catch errors and provide user-friendly messages:

messageInterceptor: async (message) => {
  try {
    // Your logic
  } catch (error) {
    console.error("Interceptor error:", error);
    return {
      handled: true,
      error: "Something went wrong. Please try again.",
    };
  }
};

3. Use Appropriate Timeouts

Set realistic timeouts based on your interceptor’s complexity:

{
  advanced: {
    messageInterceptor: async (message) => {
      // Quick validation - 2 second timeout is fine
    },
    interceptorTimeout: 2000,
  },
}

4. Log for Debugging

Use console logs to debug interceptor behavior:

messageInterceptor: async (message, context) => {
  console.log("[Interceptor] Received:", message.text);
  console.log(
    "[Interceptor] Session events:",
    context.store.events.value.length,
  );

  const result = await yourLogic(message);

  console.log("[Interceptor] Returning:", result);
  return result;
};

5. Return Consistent Types

Be consistent with your return types:

// Good - consistent structure
messageInterceptor: async (message) => {
  if (condition) {
    return { handled: true, response: { text: "...", role: "assistant" } };
  }
  return null;
};

// Avoid - mixing different return patterns
messageInterceptor: async (message) => {
  if (condition) {
    return { handled: true, response: { text: "..." } };
  }
  if (otherCondition) {
    return "some string"; // Wrong!
  }
  return undefined; // Should return null
};

Interactive Demo

See the Message Interceptor Demo to experiment with all 5 patterns and see the interceptor in action.

Next Steps