bot sdk complete - nself-org/nchat GitHub Wiki
Version: 1.0.0 (v0.7.0) Last Updated: January 31, 2026
Complete guide to building intelligent bots for nself-chat using the Bot SDK.
- Getting Started
- SDK API Reference
- Event System
- Context API Reference
- Response API Reference
- State Management
- Bot Lifecycle
- Debugging Bots
- Best Practices
- Examples
Required:
- Node.js >= 20.0.0
- TypeScript >= 5.0.0
- nself-chat development environment
Recommended:
- VS Code with TypeScript extension
- Basic understanding of async/await
- Familiarity with React (for UI components)
The Bot SDK is included with nself-chat. No additional installation needed.
# If setting up a new project
npm install @nself/bot-sdk
# Or use the included SDK in nself-chat
# Located at: src/lib/bots/bot-sdk.tsCreate a simple bot in 5 minutes:
// src/lib/bots/my-first-bot.ts
import { bot, text, command } from '@/lib/bots/bot-sdk'
export const myFirstBot = bot('my-first-bot')
.name('My First Bot')
.description('A simple bot that says hello')
.icon('👋')
// Add a command
.command('hello', 'Say hello', (ctx) => {
return text(`Hello, ${ctx.user.displayName}! 👋`)
})
// Respond to mentions
.onMention((ctx) => {
return text(`You called? I'm ${ctx.bot.name}!`)
})
.build()Register your bot:
// src/lib/bots/index.ts
import { myFirstBot } from './my-first-bot'
export function initializeBots() {
// Bot is automatically registered when .build() is called
// Just import to ensure it's loaded
}Test it:
- Start the dev server:
pnpm dev - Open nself-chat
- Type
/helloin any channel - See your bot respond!
Two approaches to building bots:
Fluent API, concise syntax:
const myBot = bot('my-bot-id')
.name('My Bot')
.command('hello', 'Say hello', (ctx) => text('Hello!'))
.build()Pros:
- ✅ Less boilerplate
- ✅ Easier to read
- ✅ Type-safe
- ✅ Chainable
Best for: Most bots, quick prototypes, simple to medium complexity
Traditional OOP approach:
import { BaseBot, Command } from '@/lib/bots/bot-sdk'
class MyBot extends BaseBot {
constructor() {
super('my-bot-id', 'My Bot', 'Description')
}
@Command('hello')
async handleHello(ctx: CommandContext) {
return text('Hello!')
}
}
const myBot = new MyBot()Pros:
- ✅ Familiar OOP patterns
- ✅ Better for complex state
- ✅ Easier testing (dependency injection)
- ✅ Decorator support
Best for: Complex bots, team projects, when you need inheritance
import { bot } from '@/lib/bots/bot-sdk'
const builder = bot(id: string)Parameters:
-
id- Unique bot identifier (e.g., 'weather-bot')
Returns: BotBuilder instance
.name(name: string): BotBuilderSet bot display name.
.description(description: string): BotBuilderSet bot description (shown in bot list).
.version(version: string): BotBuilderSet semantic version (default: '1.0.0').
.author(author: string): BotBuilderSet bot author name.
.icon(icon: string): BotBuilderSet bot icon (emoji or image URL).
.permissions(...permissions: BotPermission[]): BotBuilderSet required permissions.
Available Permissions:
-
'read_messages'- Read messages in channels -
'send_messages'- Send messages -
'manage_messages'- Edit/delete messages -
'read_users'- Access user information -
'manage_channels'- Modify channel settings -
'admin'- Full admin access
.addPermission(permission: BotPermission): BotBuilderAdd a single permission.
.channels(...channelIds: ChannelId[]): BotBuilderRestrict bot to specific channels.
.settings(settings: Record<string, unknown>): BotBuilderSet bot configuration settings.
.command(
name: string,
description: string,
handler: CommandHandler
): BotBuilderAdd a slash command.
Parameters:
-
name- Command name (without/) -
description- Help text -
handler- Function to handle the command
Example:
.command('hello', 'Say hello', (ctx) => {
return text(`Hello, ${ctx.user.displayName}!`)
}).command(
cmd: CommandBuilder,
handler: CommandHandler
): BotBuilderAdd a command with advanced options.
Example:
import { command } from '@/lib/bots/bot-sdk'
.command(
command('weather')
.description('Get weather forecast')
.option('location', 'City name', { required: true })
.option('units', 'Temperature units', {
choices: ['celsius', 'fahrenheit'],
default: 'celsius'
}),
async (ctx) => {
const location = ctx.args.location
const units = ctx.args.units
// Fetch and return weather
}
).onMessage(handler: MessageHandler): BotBuilderHandle all messages in channels the bot is in.
Example:
.onMessage((ctx) => {
if (ctx.message.content.includes('help')) {
return text('Need help? Try /help')
}
}).onMention(handler: MessageHandler): BotBuilderHandle messages that mention the bot.
Example:
.onMention((ctx) => {
return text(`You mentioned me! How can I help?`)
}).onKeyword(keywords: string[], handler: MessageHandler): BotBuilderTrigger on specific keywords.
Example:
.onKeyword(['hello', 'hi', 'hey'], (ctx) => {
return text('Hello! 👋')
}).onPattern(patterns: string[], handler: MessageHandler): BotBuilderTrigger on regex patterns.
Example:
.onPattern(['/bug-\\d+/', '/issue-\\d+/'], (ctx) => {
// Extract issue number and fetch details
const match = ctx.message.content.match(/(?:bug|issue)-(\d+)/)
const issueId = match[1]
return text(`Fetching issue #${issueId}...`)
}).onUserJoin(handler: UserEventHandler): BotBuilderHandle user joining a channel.
.onUserLeave(handler: UserEventHandler): BotBuilderHandle user leaving a channel.
.onReaction(handler: ReactionHandler): BotBuilderHandle reactions added to messages.
.onInit(handler: (bot, api) => void | Promise<void>): BotBuilderRun code when bot initializes.
Example:
.onInit(async (bot, api) => {
console.log(`${bot.manifest.name} initialized`)
// Load saved state
const state = await api.getStorage('config')
// Schedule periodic tasks
}).build(): BotInstanceBuild and register the bot. Call this as the last step.
Returns: BotInstance - The active bot instance
Create complex commands with options and arguments:
import { command } from '@/lib/bots/bot-sdk'
const cmd = command(name: string)
.description(desc: string)
.option(name, description, options?)
.build().option(
name: string,
description: string,
options?: {
type?: 'string' | 'number' | 'boolean'
required?: boolean
default?: any
choices?: any[]
}
): CommandBuilderExample:
command('create-poll')
.description('Create a poll')
.option('question', 'Poll question', { required: true })
.option('duration', 'Duration in minutes', {
type: 'number',
default: 60,
})
.option('anonymous', 'Anonymous voting', {
type: 'boolean',
default: false,
})Bots can subscribe to various events:
| Event | Trigger | Context Type | Common Use Cases |
|---|---|---|---|
| Command | User types /command
|
CommandContext |
Execute actions, fetch data |
| Message | Any message sent | MessageContext |
Monitor keywords, auto-respond |
| Mention | Bot is @mentioned | MessageContext |
Help requests, Q&A |
| User Join | User joins channel | UserContext |
Welcome messages, onboarding |
| User Leave | User leaves channel | UserContext |
Goodbye messages, cleanup |
| Reaction | Reaction added/removed | ReactionContext |
Polls, bookmarks, votes |
.onMessage((ctx) => {
// Handle synchronously
return text('Response')
}).onMessage(async (ctx) => {
// Async operations
const data = await fetchData()
return text(`Result: ${data}`)
}).onMessage((ctx) => {
if (ctx.message.content.startsWith('!')) {
return text('Command detected')
}
// Return nothing to ignore
}).onMessage(handler1)
.onMessage(handler2)
.onMessage(handler3)Execution: All handlers run in order. First non-null response is returned.
import { matchesKeyword, matchesPattern, parseDuration, formatDuration } from '@/lib/bots/bot-sdk'matchesKeyword(text: string, keywords: string[]): booleanCheck if text contains any keyword (case-insensitive).
Example:
if (matchesKeyword(ctx.message.content, ['help', 'support'])) {
return text('How can I help you?')
}matchesPattern(text: string, patterns: string[]): booleanCheck if text matches any regex pattern.
Example:
if (matchesPattern(ctx.message.content, ['/bug-\\d+/', '/issue-\\d+/'])) {
// Handle bug/issue reference
}parseDuration(text: string): numberParse natural language duration to milliseconds.
Examples:
-
'30 minutes'→ 1800000 -
'2 hours'→ 7200000 -
'1 day'→ 86400000
formatDuration(ms: number): stringFormat milliseconds to human-readable duration.
Example:
formatDuration(90000) // '1 minute 30 seconds'Provided to command handlers:
interface CommandContext {
// Command info
command: string // Command name (without /)
args: {
[key: string]: any // Parsed arguments
_raw: string // Raw argument string
}
// Message info
message: {
id: MessageId
content: string
channelId: ChannelId
threadId?: string
createdAt: Date
}
// User info
user: {
id: UserId
displayName: string
email: string
avatarUrl?: string
role: UserRole
}
// Channel info
channel: {
id: ChannelId
name: string
type: 'public' | 'private' | 'dm' | 'group_dm'
}
// Bot info
bot: {
id: string
name: string
manifest: BotManifest
}
// Helpers
isMention: boolean // Is bot mentioned?
isDM: boolean // Is this a DM?
}Provided to message handlers:
interface MessageContext {
// Same as CommandContext, plus:
message: {
// ... base fields
attachments: Attachment[]
mentions: User[]
reactions: Reaction[]
isEdited: boolean
editedAt?: Date
replyTo?: MessageId
}
// Thread info (if in thread)
thread?: {
id: string
parentMessageId: MessageId
participantCount: number
}
}Provided to user event handlers (join/leave):
interface UserContext {
// User who joined/left
user: {
id: UserId
displayName: string
email: string
avatarUrl?: string
role: UserRole
joinedAt: Date // When they joined the workspace
}
// Channel context
channel: {
id: ChannelId
name: string
type: ChannelType
memberCount: number
}
// Event type
eventType: 'join' | 'leave'
}Provided to reaction handlers:
interface ReactionContext {
// Reaction details
reaction: {
emoji: string
userId: UserId
messageId: MessageId
createdAt: Date
}
// User who reacted
user: {
id: UserId
displayName: string
avatarUrl?: string
}
// Message that was reacted to
message: {
id: MessageId
content: string
authorId: UserId
channelId: ChannelId
}
// Channel context
channel: {
id: ChannelId
name: string
}
}Import response builders:
import {
text,
embed,
error,
success,
info,
warning,
confirm,
list,
code,
quote,
button,
select,
} from '@/lib/bots/bot-sdk'text(content: string): BotResponseSimple text response.
Example:
return text('Hello, world!')error(message: string): BotResponseError message (red styling).
return error('Command failed: Invalid arguments')success(message: string): BotResponseSuccess message (green styling).
return success('Poll created successfully!')info(message: string): BotResponseInfo message (blue styling).
return info('Reminder: Meeting in 30 minutes')warning(message: string): BotResponseWarning message (yellow styling).
return warning('This action cannot be undone')embed(options: {
title?: string
description?: string
color?: string
fields?: Array<{ name: string; value: string; inline?: boolean }>
footer?: string
timestamp?: string | Date
image?: string
thumbnail?: string
author?: {
name: string
icon?: string
url?: string
}
}): BotResponseRich embed with formatted content.
Example:
return embed({
title: '📊 Poll Results',
description: 'Final results for: "What should we have for lunch?"',
color: '#6366f1',
fields: [
{ name: '🍕 Pizza', value: '45% (9 votes)', inline: true },
{ name: '🌮 Tacos', value: '55% (11 votes)', inline: true },
],
footer: 'Poll closed',
timestamp: new Date(),
})list(items: string[], title?: string): BotResponseBulleted list.
Example:
return list(
['Install dependencies', 'Configure environment', 'Run tests', 'Deploy to production'],
'📋 Deployment Checklist'
)code(content: string, language?: string): BotResponseCode block with syntax highlighting.
Example:
return code(
`
function greet(name) {
return \`Hello, \${name}!\`
}
`,
'javascript'
)quote(text: string, author?: string): BotResponseBlockquote formatting.
Example:
return quote('The best way to predict the future is to invent it.', 'Alan Kay')button(options: {
label: string
action: string
style?: 'primary' | 'secondary' | 'success' | 'danger'
disabled?: boolean
}): ButtonComponentCreate a button.
Example:
import { response, button } from '@/lib/bots/bot-sdk'
return response({
content: 'Choose an option:',
components: [
button({ label: 'Approve', action: 'approve', style: 'success' }),
button({ label: 'Reject', action: 'reject', style: 'danger' }),
],
})select(options: {
placeholder?: string
options: Array<{ label: string; value: string }>
action: string
}): SelectComponentCreate a dropdown select.
Example:
return response({
content: 'Select a priority:',
components: [
select({
placeholder: 'Choose priority...',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
{ label: 'Critical', value: 'critical' },
],
action: 'set_priority',
}),
],
})confirm(message: string, options?: {
confirmText?: string
cancelText?: string
confirmAction?: string
cancelAction?: string
}): BotResponseConfirmation dialog with yes/no buttons.
Example:
return confirm('Are you sure you want to delete this poll?', {
confirmText: 'Yes, delete',
cancelText: 'Cancel',
confirmAction: 'confirm_delete',
cancelAction: 'cancel_delete',
})All response functions accept additional options:
text('Message', {
ephemeral: true, // Only visible to user who triggered
threadId: 'thread-123', // Reply in thread
mentionUser: 'user-456', // Mention a user
deleteAfter: 5000, // Auto-delete after 5 seconds
})Bots can store persistent data:
// In any handler
const api = ctx.api // or passed to handler
// Store data
await api.setStorage('key', value)
// Retrieve data
const value = await api.getStorage<Type>('key')
// Delete data
await api.deleteStorage('key').command('setlang', 'Set your preferred language', async (ctx, api) => {
const lang = ctx.args._raw
// Store user preference
await api.setStorage(`user:${ctx.user.id}:lang`, lang)
return success(`Language set to ${lang}`)
})
.command('hello', 'Say hello in your language', async (ctx, api) => {
// Retrieve user preference
const lang = await api.getStorage<string>(`user:${ctx.user.id}:lang`) || 'en'
const greetings = {
en: 'Hello',
es: 'Hola',
fr: 'Bonjour'
}
return text(greetings[lang] || greetings.en)
}).onReaction(async (ctx, api) => {
if (ctx.reaction.emoji === '⭐') {
// Increment star count for message author
const key = `stars:${ctx.message.authorId}`
const current = await api.getStorage<number>(key) || 0
await api.setStorage(key, current + 1)
}
})
.command('leaderboard', 'Show star leaderboard', async (ctx, api) => {
// Fetch all star counts
// Note: In production, use proper database queries
// This is simplified for example
return embed({
title: '⭐ Star Leaderboard',
description: 'Top contributors this month',
fields: [
{ name: 'Alice', value: '47 stars' },
{ name: 'Bob', value: '32 stars' },
{ name: 'Charlie', value: '28 stars' }
]
})
}).onInit(async (bot, api) => {
// Load config on startup
const config = await api.getStorage<BotConfig>('config')
if (!config) {
// Set defaults
await api.setStorage('config', {
enabled: true,
prefix: '/',
maxPollDuration: 604800000 // 7 days
})
}
})
.command('config', 'View bot configuration', async (ctx, api) => {
const config = await api.getStorage<BotConfig>('config')
return code(JSON.stringify(config, null, 2), 'json')
})-
Namespace your keys: Use prefixes like
user:${id}:preforpoll:${id}:votes -
Type your data: Use TypeScript generics:
getStorage<Type>(key) - Handle missing data: Always provide defaults
- Clean up: Delete old data periodically
- Don't abuse storage: Use for configuration and state, not large datasets
┌─────────────┐
│ Created │ Constructor/Builder called
└──────┬──────┘
│
▼
┌─────────────┐
│ Registered │ .build() called, bot added to registry
└──────┬──────┘
│
▼
┌─────────────┐
│ Starting │ .start() called (auto or manual)
└──────┬──────┘
│
▼
┌─────────────┐
│ Running │ Handlers active, receiving events
└──────┬──────┘
│
▼
┌─────────────┐
│ Stopping │ .stop() called
└──────┬──────┘
│
▼
┌─────────────┐
│ Stopped │ Handlers inactive
└─────────────┘
.onInit((bot, api) => {
// Called when bot starts
console.log(`${bot.manifest.name} starting...`)
// Load state, set up timers, etc.
})
.onStop(() => {
// Called when bot stops
console.log('Bot stopping...')
// Clean up resources, clear timers
})const myBot = bot('my-bot')
.name('My Bot')
// ... configuration
.build()
// Bot auto-starts by default
// Manual control
myBot.stop() // Stop the bot
myBot.start() // Restart the bot.command('risky', 'A command that might fail', async (ctx) => {
try {
const result = await riskyOperation()
return success(`Result: ${result}`)
} catch (error) {
console.error('Command failed:', error)
return error(`Operation failed: ${error.message}`)
}
})
// Global error handler
.onInit((bot, api) => {
// Catch unhandled errors
process.on('unhandledRejection', (error) => {
console.error(`[${bot.manifest.name}] Unhandled error:`, error)
})
}).onMessage((ctx) => {
console.log('[MyBot] Message received:', {
content: ctx.message.content,
from: ctx.user.displayName,
channel: ctx.channel.name
})
})Enable verbose logging:
const DEBUG =
process.env.BOT_DEBUG ===
'true'.onMessage((ctx) => {
if (DEBUG) {
console.log('Full context:', JSON.stringify(ctx, null, 2))
}
})Use the test endpoint:
curl -X POST http://localhost:3000/api/bots/test \
-H "Content-Type: application/json" \
-d '{
"botId": "my-bot",
"command": "hello",
"args": {},
"userId": "user-1"
}'Check:
- Bot is registered: Check
initializeBots()is called - Bot is started:
myBot.start()called - Bot has permission: Check
manifest.permissions - Command syntax correct: Use
/helpto verify
Check:
- Storage service configured
- Using correct key format
- Handling async properly (await)
- Type casting correct
Check:
- Bot is in the channel
- Bot has
read_messagespermission - Event handler registered before
.build() - Not returning early from handler
✅ Do:
- Validate all user input
- Use typed arguments
- Check permissions before actions
- Sanitize output
- Rate limit commands
❌ Don't:
- Trust user input blindly
- Store sensitive data in bot storage
- Grant excessive permissions
- Expose internal errors to users
Example:
.command('admin', 'Admin command', (ctx) => {
// Check permissions
if (ctx.user.role !== 'admin' && ctx.user.role !== 'owner') {
return error('You do not have permission to use this command')
}
// Validate input
const action = ctx.args.action
if (!['backup', 'restore', 'status'].includes(action)) {
return error('Invalid action')
}
// Execute safely
return success(`${action} completed`)
})✅ Do:
- Cache expensive operations
- Use async/await properly
- Implement timeouts
- Batch API calls
- Clean up resources
❌ Don't:
- Block the event loop
- Make synchronous network calls
- Store large data in memory
- Create memory leaks
Example:
// Cache expensive data
const cache = new Map<string, { data: any; expires: number }>().command(
'fetch',
'Fetch data',
async (ctx) => {
const key = ctx.args.key
// Check cache first
const cached = cache.get(key)
if (cached && cached.expires > Date.now()) {
return text(`Cached: ${cached.data}`)
}
// Fetch with timeout
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch(`/api/data/${key}`, {
signal: controller.signal,
})
const data = await response.json()
// Cache for 5 minutes
cache.set(key, {
data,
expires: Date.now() + 300000,
})
return text(`Fresh: ${data}`)
} catch (error) {
return error('Fetch timeout or failed')
} finally {
clearTimeout(timeout)
}
}
)✅ Do:
- Provide clear error messages
- Use progress indicators for slow operations
- Confirm destructive actions
- Provide helpful usage examples
- Use consistent formatting
❌ Don't:
- Show technical errors to users
- Make users wait without feedback
- Use jargon in messages
- Spam channels
Example:
.command('delete', 'Delete data', async (ctx) => {
const id = ctx.args.id
// 1. Validate
if (!id) {
return error('Usage: /delete <id>\nExample: /delete 123')
}
// 2. Confirm
const confirmed = await confirm(`Delete item ${id}?`, {
confirmText: 'Yes, delete it',
cancelText: 'Cancel'
})
// 3. Show progress
await ctx.api.sendMessage(ctx.channel.id, info('Deleting...'))
// 4. Execute
try {
await deleteItem(id)
return success(`Item ${id} deleted successfully`)
} catch (error) {
return error(`Failed to delete item ${id}. Please try again.`)
}
})✅ Do:
- One bot per file
- Group related commands
- Extract complex logic to functions
- Use TypeScript types
- Document your code
Example Structure:
src/lib/bots/
├── weather-bot/
│ ├── index.ts # Bot registration
│ ├── commands/
│ │ ├── forecast.ts # /forecast command
│ │ ├── current.ts # /current command
│ │ └── alerts.ts # /alerts command
│ ├── services/
│ │ └── weather-api.ts # External API client
│ └── types.ts # TypeScript interfaces
└── index.ts # Register all bots
See Bot Templates Guide for ready-to-use templates and Bots.md for detailed examples of:
- HelloBot - Greetings and jokes
- PollBot - Polls and voting
- ReminderBot - Reminders and scheduling
- WelcomeBot - Welcome messages
- SearchBot - Semantic search
- SummaryBot - AI summaries
- Build your first bot using this guide
- Explore templates in Bot Templates Guide
- Review examples in the codebase
- Test thoroughly before deploying
- Share with the community on the Bot Marketplace
- Bot Templates Guide - Pre-built bot templates
- Bot API Implementation - Low-level API docs
- Bots Feature Guide - User-facing bot documentation
- AI Features Guide - AI integration
- Documentation: This guide
-
Examples:
src/lib/bots/examples/ - Community: community.nself.org/bot-development
- Issues: github.com/nself/nself-chat/issues
Last Updated: January 31, 2026 Version: v0.7.0 SDK Version: 1.0.0