back to tutorials

How to Use Channel-Based Function Calling with Stream

Learn how to implement channel-based function calling using the complete().stream() method for inline conversation execution. This tutorial covers real-time event handling and function execution in interactive chat scenarios.

This tutorial demonstrates how to implement channel-based function calling using the complete().stream() method in ChatBotKit. This approach is ideal for interactive conversations where you need real-time control over the conversation flow.

Learning Objectives

By the end of this tutorial, you will be able to:

  • Use the complete().stream() method for inline conversation processing
  • Handle streaming events directly in your code
  • Implement static and channel-based function results
  • Process the waitForChannelMessageBegin event for dynamic function execution
  • Display real-time tokens as they arrive

Prerequisites

  • Node.js 18+ installed
  • A ChatBotKit account with an API secret
  • Basic understanding of async iterators and streams

Estimated time: 20 minutes

Understanding Stream vs Dispatch

Aspectcomplete().stream()dispatch()
ExecutionInline, blockingBackground, async
Event sourceDirect from stream iteratorVia channel.subscribe()
Use caseInteractive chatBackground tasks
ControlDirect in your loopRequires separate subscription

The complete().stream() method processes a conversation inline, streaming events directly to your code as they occur.

Step 1: Set Up Your Project

Create a new Node.js project and install the ChatBotKit SDK:

mkdir function-stream-example
cd function-stream-example
npm init -y
npm install @chatbotkit/sdk dotenv

Create a .env file with your API secret:

CHATBOTKIT_API_SECRET=your_api_secret_here

Step 2: Create the Basic Structure

Create a file called index.js:

import * as dotenv from 'dotenv'
import { ChatBotKit } from '@chatbotkit/sdk/index.js'
import { randomBytes } from 'node:crypto'

dotenv.config()

async function main() {
  const client = new ChatBotKit({
    secret: process.env.CHATBOTKIT_API_SECRET,
  })

  // Generate unique channel ID for dynamic functions
  const weatherChannelId = `weather-${randomBytes(16).toString('hex')}`

  console.log('Starting conversation stream...')
}

main().catch(console.error)

Step 3: Define Your Functions

Define both static and channel-based functions:

const functions = [
  // Static result - returns immediately
  {
    name: 'get_current_time',
    description: 'Get the current time for a specified timezone',
    parameters: {
      type: 'object',
      properties: {
        timezone: {
          type: 'string',
          description: 'The timezone, e.g. America/New_York',
        },
      },
      required: ['timezone'],
    },
    result: {
      data: {
        time: '10:30 AM',
        date: 'Monday, January 26, 2026',
      },
    },
  },

  // Channel-based result - you execute and publish
  {
    name: 'get_weather',
    description: 'Get the current weather for a location',
    parameters: {
      type: 'object',
      properties: {
        location: {
          type: 'string',
          description: 'The city name',
        },
      },
      required: ['location'],
    },
    result: {
      channel: weatherChannelId,
    },
  },
]

Step 4: Start the Conversation Stream

Use complete().stream() to get a stream of events:

const stream = client.conversation
  .complete(null, {
    model: 'claude-4.5-sonnet',
    backstory: 'You are a helpful assistant that provides weather and time information.',
    messages: [
      {
        type: 'user',
        text: 'What time is it in New York, and what is the weather like there?',
      },
    ],
    functions,
  })
  .stream()

Step 5: Process Stream Events

Iterate over the stream and handle different event types:

for await (const event of stream) {
  switch (event.type) {
    case 'token':
      // Real-time token for display
      process.stdout.write(event.data.token)
      break

    case 'waitForChannelMessageBegin':
      // Function execution required
      await handleChannelFunction(client, event.data)
      break

    case 'message':
      // Complete message (bot response or activity)
      handleMessage(event.data)
      break

    case 'result':
      // Conversation completed
      console.log('Conversation finished')
      break
  }
}

Step 6: Handle Channel Function Calls

When you receive a waitForChannelMessageBegin event, execute the function and publish the result:

async function handleChannelFunction(client, data) {
  const { function: fn, channel } = data
  const functionName = fn.name
  const functionArgs = fn.args

  console.log(`\nExecuting function: ${functionName}`)
  console.log(`Arguments: ${JSON.stringify(functionArgs)}`)

  // Execute your function logic
  let result
  if (functionName === 'get_weather') {
    result = await fetchWeatherData(functionArgs.location)
  } else {
    result = { error: `Unknown function: ${functionName}` }
  }

  // Publish the result back to the channel
  await client.channel.publish(channel, {
    message: result,
  })

  console.log('Result published to channel')
}

async function fetchWeatherData(location) {
  // In production, call a real weather API
  return {
    temperature: 42,
    conditions: 'partly cloudy',
    humidity: 65,
    location: location,
  }
}

Complete Example

Here's the complete working example:

import * as dotenv from 'dotenv'
import { ChatBotKit } from '@chatbotkit/sdk/index.js'
import { randomBytes } from 'node:crypto'

dotenv.config()

async function main() {
  const client = new ChatBotKit({
    secret: process.env.CHATBOTKIT_API_SECRET,
  })

  const weatherChannelId = `weather-${randomBytes(16).toString('hex')}`

  console.log('Starting conversation stream...\n')

  const stream = client.conversation
    .complete(null, {
      model: 'claude-4.5-sonnet',
      backstory: 'You are a helpful assistant that provides weather and time information.',
      messages: [
        {
          type: 'user',
          text: 'What time is it in New York, and what is the weather like there?',
        },
      ],
      functions: [
        {
          name: 'get_current_time',
          description: 'Get the current time for a specified timezone',
          parameters: {
            type: 'object',
            properties: {
              timezone: { type: 'string', description: 'The timezone' },
            },
            required: ['timezone'],
          },
          result: {
            data: { time: '10:30 AM', date: 'Monday, January 26, 2026' },
          },
        },
        {
          name: 'get_weather',
          description: 'Get the current weather for a location',
          parameters: {
            type: 'object',
            properties: {
              location: { type: 'string', description: 'The city name' },
            },
            required: ['location'],
          },
          result: { channel: weatherChannelId },
        },
      ],
    })
    .stream()

  for await (const event of stream) {
    if (event.type === 'waitForChannelMessageBegin') {
      const { function: fn, channel } = event.data

      console.log(`\nExecuting: ${fn.name}`)

      const result = { temperature: 42, conditions: 'partly cloudy' }
      await client.channel.publish(channel, { message: result })

      console.log('Result published\n')
    }

    if (event.type === 'message') {
      const messageData = event.data

      if (messageData.type === 'activity') {
        const activity = messageData.meta?.activity
        if (activity?.type === 'request') {
          console.log(`Function call: ${activity.function?.name}`)
        }
      }

      if (messageData.type === 'bot' && messageData.text) {
        console.log(`Bot: ${messageData.text}`)
      }
    }

    if (event.type === 'result') {
      console.log('\nConversation completed')
    }
  }
}

main().catch(console.error)

Key Differences from Dispatch

  1. Direct event access: Events come directly from the stream iterator, not wrapped in a message envelope
  2. Inline execution: Your code blocks while processing the stream
  3. Token streaming: You can display tokens in real-time with event.type === 'token'
  4. Simpler event handling: Event types are directly on event.type

Displaying Real-Time Tokens

To show tokens as they arrive (for chat-like interfaces):

for await (const event of stream) {
  if (event.type === 'token') {
    process.stdout.write(event.data.token)
  }
  // ... handle other events
}

Troubleshooting

Stream Closes Early

Ensure you handle all events before the result event. Don't break out of the loop prematurely.

Channel Timeout

If you don't publish to the channel quickly enough, the conversation may timeout. Execute functions efficiently.

Missing Results

Always check event.type carefully. The stream includes many event types - filter for what you need.

Next Steps