← Back to blog
TUTORIAL14 min read

Building an Agentic Chatbot with NoLag

HB
Henco Burger
May 19, 2026

This tutorial walks through building a customer support chatbot that combines two NoLag Blueprints: @nolag/chat for the user-facing interface and @nolag/agents for AI-powered response generation. The result is a production-ready chatbot with typing indicators, conversation history, sentiment-based escalation, and full observability.

Architecture Overview

The system has three components:

  1. Chat frontend - Users interact with a standard chat room powered by @nolag/chat. They see typing indicators, message history, and presence.
  2. Bridge service - A server-side process that connects to both the chat app and the agents app. It forwards user messages to the agent workflow and sends responses back to chat.
  3. AI workers - Agent processes that receive tasks via the Handoff pattern, call an LLM, and return responses. They can be scaled horizontally.

Step 1: Install Dependencies

npm install @nolag/chat @nolag/agents @nolag/js-sdk

Step 2: Set Up Apps in the Portal

Create two apps in your NoLag project:

  1. A Chat Blueprint app (slug: support-chat)
  2. An Agents Blueprint app (slug: support-agents)

Create three actors:

  • Bridge actor - granted access to both apps
  • Worker actor - granted access to the agents app
  • User actors - granted access to the chat app (one per user, or use dynamic tokens)

Step 3: Build the Bridge

The bridge is the key piece. It connects to both apps simultaneously and translates between chat messages and agent tasks:

import { NoLagChat } from "@nolag/chat";
import { NoLagAgents } from "@nolag/agents";

// The bridge actor connects to both apps
const chat = new NoLagChat(BRIDGE_CHAT_TOKEN, {
  user: { id: "bot", name: "Support Bot", avatar: "/bot.png" },
});
const agents = new NoLagAgents(BRIDGE_AGENT_TOKEN);

await Promise.all([chat.connect(), agents.connect()]);

const chatRoom = chat.joinRoom("support");
const agentRoom = agents.joinRoom("support-workflow");

// Forward user messages to the agent workflow
chatRoom.on("message", async (msg) => {
  // Don't process our own messages
  if (msg.sender.id === "bot") return;

  // Show typing indicator while agent works
  chatRoom.startTyping();

  await agentRoom.handoff.dispatch({
    type: "respond",
    payload: {
      userMessage: msg.text,
      userId: msg.sender.id,
      history: chatRoom.getMessages().slice(-10),
    },
    capabilities: ["customer-support"],
  });
});

// Forward agent responses back to chat
agentRoom.handoff.onResult(async (result) => {
  chatRoom.stopTyping();
  await chatRoom.sendMessage(result.data.response);
});

The bridge shows typing indicators while the agent is processing, giving users a natural chat experience. Message history is passed to the agent so it has conversation context.

Step 4: Build the AI Worker

Workers register their capabilities and process incoming tasks. You can use any LLM - OpenAI, Anthropic, a local model, or a custom pipeline:

import { NoLagAgents } from "@nolag/agents";

const agents = new NoLagAgents(WORKER_TOKEN);
await agents.connect();

const room = agents.joinRoom("support-workflow");

room.handoff.register({
  capabilities: ["customer-support"],
  concurrency: 5,
});

room.handoff.onTask(async (task) => {
  const { userMessage, history } = task.payload;

  // Call your LLM of choice
  const response = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [
      { role: "system", content: "You are a helpful customer support agent." },
      ...history.map((m: any) => ({
        role: m.sender.id === "bot" ? "assistant" : "user",
        content: m.text,
      })),
      { role: "user", content: userMessage },
    ],
  });

  await task.complete({
    response: response.choices[0].message.content,
  });
});

Workers are stateless and horizontally scalable. Run multiple instances and they'll automatically load-balance tasks via the Handoff pattern.

Step 5: Add Escalation

Use the Approve pattern to escalate sensitive conversations to a human agent. The worker detects negative sentiment and requests approval before responding:

// In the worker, detect when to escalate
room.handoff.onTask(async (task) => {
  const { userMessage } = task.payload;
  const sentiment = await analyzeSentiment(userMessage);

  if (sentiment.score < -0.7) {
    // Escalate: request human approval before responding
    const approval = await room.approve.request({
      action: "escalate-to-human",
      description: "Negative sentiment detected. Review before responding.",
      payload: { userMessage, suggestedResponse: "..." },
    });

    if (approval.approved) {
      // Human may have modified the response
      await task.complete({
        response: approval.modifications?.response || approval.payload.suggestedResponse,
      });
    }
  } else {
    // Normal LLM response
    const response = await generateResponse(task.payload);
    await task.complete({ response });
  }
});

Step 6: Monitor Everything

The Observe pattern gives you full visibility into the chatbot's performance:

const room = agents.joinRoom("support-workflow");

// Track metrics
let totalQueries = 0;
let avgResponseTime = 0;

room.observe.on("task:completed", (event) => {
  totalQueries++;
  avgResponseTime = (avgResponseTime * (totalQueries - 1) + event.data.duration) / totalQueries;
  console.log(`Avg response: ${avgResponseTime}ms | Total: ${totalQueries}`);
});

room.observe.on("task:failed", (event) => {
  console.error("Agent failed:", event.data.error);
});

// Monitor agent health
const onlineAgents = room.getPresence();
console.log("Workers online:", onlineAgents.length);

What You Get

  • A chat interface with typing indicators and presence - users don't know they're talking to an AI
  • Horizontally scalable AI workers with automatic load balancing
  • Human escalation for sensitive conversations
  • Full observability: response times, error rates, agent health
  • Persistent message history with replay on reconnect
  • Per-topic access control separating user traffic from agent traffic

The entire system runs on NoLag's real-time infrastructure. No Redis queues, no custom WebSocket servers, no separate monitoring stack.

Get started with @nolag/agents →