State management lets you remember information about users across messages. BuilderBot provides two types of state: user state (per conversation) and global state (shared across all conversations).
User State
User state stores data specific to each conversation, identified by the user’s phone number.
Updating State
Store data using state.update():
const registerFlow = addKeyword ( 'register' )
. addAnswer (
'What is your name?' ,
{ capture: true },
async ( ctx , { state }) => {
await state . update ({ name: ctx . body })
}
)
. addAnswer (
'What is your age?' ,
{ capture: true },
async ( ctx , { state }) => {
await state . update ({ age: ctx . body })
}
)
state.update() merges new data with existing state. It doesn’t replace the entire state.
Reading State
Retrieve stored data using state.get():
. addAction ( async ( ctx , { flowDynamic , state }) => {
const name = state . get ( 'name' )
const age = state . get ( 'age' )
await flowDynamic ( ` ${ name } , thanks for your information! Your age: ${ age } ` )
})
Getting Nested Properties
Access nested object properties with dot notation:
await state . update ({
user: {
profile: {
email: '[email protected] ' ,
phone: '123456789'
}
}
})
// Access nested property
const email = state . get ( 'user.profile.email' )
// Returns: '[email protected] '
Getting All State
Get the entire state object:
. addAction ( async ( ctx , { flowDynamic , state }) => {
const allState = state . getMyState ()
console . log ( 'Complete state:' , allState )
})
Clearing State
Clear all state for a user:
const logoutFlow = addKeyword ( 'logout' )
. addAction ( async ( ctx , { flowDynamic , state }) => {
state . clear ()
await flowDynamic ( 'Your session has been cleared.' )
})
Complete User State Example
Create a multi-step registration flow
import { addKeyword } from '@builderbot/bot'
const registerFlow = addKeyword ( 'register' )
. addAnswer (
'What is your name?' ,
{ capture: true },
async ( ctx , { state }) => {
await state . update ({ name: ctx . body })
}
)
Collect additional information
. addAnswer (
'What is your email?' ,
{ capture: true },
async ( ctx , { state , fallBack }) => {
// Validate email
if ( ! ctx . body . includes ( '@' )) {
return fallBack ( 'Please enter a valid email' )
}
await state . update ({ email: ctx . body })
}
)
. addAnswer (
'What is your age?' ,
{ capture: true },
async ( ctx , { state , fallBack }) => {
const age = parseInt ( ctx . body )
if ( isNaN ( age ) || age < 1 ) {
return fallBack ( 'Please enter a valid age' )
}
await state . update ({ age })
}
)
Display collected data
. addAction ( async ( ctx , { flowDynamic , state }) => {
const name = state . get ( 'name' )
const email = state . get ( 'email' )
const age = state . get ( 'age' )
await flowDynamic ([
'✅ Registration complete!' ,
`Name: ${ name } ` ,
`Email: ${ email } ` ,
`Age: ${ age } `
]. join ( ' \n ' ))
// Save to database
await saveToDatabase ({ name , email , age })
})
Global State
Global state is shared across all conversations. Use it for configuration, counters, or shared resources.
Initializing Global State
Set initial global state when creating the bot:
const { httpServer } = await createBot (
{
flow: adapterFlow ,
provider: adapterProvider ,
database: adapterDB ,
},
{
globalState: {
appName: 'MyBot' ,
version: '1.0.0' ,
totalUsers: 0
}
}
)
Reading Global State
Access global state in any flow:
const infoFlow = addKeyword ( 'info' )
. addAction ( async ( ctx , { flowDynamic , globalState }) => {
const appName = globalState . get ( 'appName' )
const version = globalState . get ( 'version' )
await flowDynamic ( ` ${ appName } v ${ version } ` )
})
Updating Global State
Modify global state from any flow:
const welcomeFlow = addKeyword ([ 'hi' , 'hello' ])
. addAction ( async ( ctx , { flowDynamic , globalState }) => {
const totalUsers = globalState . get ( 'totalUsers' ) || 0
await globalState . update ({ totalUsers: totalUsers + 1 })
await flowDynamic ( `Welcome! You are user # ${ totalUsers + 1 } ` )
})
Getting All Global State
. addAction ( async ( ctx , { globalState }) => {
const allGlobalState = globalState . getAllState ()
console . log ( 'Global state:' , allGlobalState )
})
Clearing Global State
Reset all global state:
const resetFlow = addKeyword ( 'reset-global' )
. addAction ( async ( ctx , { flowDynamic , globalState }) => {
globalState . clear ()
await flowDynamic ( 'Global state cleared' )
})
State in HTTP Endpoints
Access state in custom HTTP endpoints:
User State via API
adapterProvider . server . get (
'/v1/user/:number/state' ,
handleCtx ( async ( bot , req , res ) => {
const { number } = req . params
const userState = bot . state ( number ). getMyState ()
res . writeHead ( 200 , { 'Content-Type' : 'application/json' })
return res . end ( JSON . stringify ( userState ))
})
)
Global State via API
adapterProvider . server . get (
'/v1/global-state' ,
handleCtx ( async ( bot , req , res ) => {
const state = bot . globalState (). getAllState ()
res . writeHead ( 200 , { 'Content-Type' : 'application/json' })
return res . end ( JSON . stringify ( state ))
})
)
Updating State via API
adapterProvider . server . post (
'/v1/user/:number/state' ,
handleCtx ( async ( bot , req , res ) => {
const { number } = req . params
const { key , value } = req . body
await bot . state ( number ). update ({ [key]: value })
res . writeHead ( 200 , { 'Content-Type' : 'application/json' })
return res . end ( JSON . stringify ({ status: 'ok' }))
})
)
Practical Examples
const addToCartFlow = addKeyword ( 'add' )
. addAnswer (
'Which product? (Enter product ID)' ,
{ capture: true },
async ( ctx , { state , flowDynamic }) => {
const productId = ctx . body
const cart = state . get ( 'cart' ) || []
cart . push ({ id: productId , quantity: 1 })
await state . update ({ cart })
await flowDynamic ( `Added to cart! Total items: ${ cart . length } ` )
}
)
const viewCartFlow = addKeyword ( 'cart' )
. addAction ( async ( ctx , { state , flowDynamic }) => {
const cart = state . get ( 'cart' ) || []
if ( cart . length === 0 ) {
await flowDynamic ( 'Your cart is empty' )
return
}
const items = cart . map (( item , i ) =>
` ${ i + 1 } . Product ${ item . id } (Qty: ${ item . quantity } )`
). join ( ' \n ' )
await flowDynamic ([ 'Your cart:' , items ]. join ( ' \n ' ))
})
User Preferences
const setLanguageFlow = addKeyword ( 'language' )
. addAnswer (
[ 'Choose language:' , '1. English' , '2. Spanish' , '3. French' ]. join ( ' \n ' ),
{ capture: true },
async ( ctx , { state , flowDynamic , fallBack }) => {
const languages = { '1' : 'en' , '2' : 'es' , '3' : 'fr' }
const choice = ctx . body . trim ()
if ( ! languages [ choice ]) {
return fallBack ( 'Please choose 1, 2, or 3' )
}
await state . update ({ language: languages [ choice ] })
await flowDynamic ( `Language set to ${ languages [ choice ] } ` )
}
)
const greetFlow = addKeyword ([ 'hi' , 'hello' ])
. addAction ( async ( ctx , { state , flowDynamic }) => {
const language = state . get ( 'language' ) || 'en'
const greetings = {
en: 'Hello!' ,
es: '¡Hola!' ,
fr: 'Bonjour!'
}
await flowDynamic ( greetings [ language ])
})
Session Counter
const counterFlow = addKeyword ( 'count' )
. addAction ( async ( ctx , { state , flowDynamic }) => {
const count = state . get ( 'messageCount' ) || 0
const newCount = count + 1
await state . update ({ messageCount: newCount })
await flowDynamic ( `You've sent ${ newCount } messages to this bot` )
})
State Architecture
Understanding how state works internally:
State is stored in memory by default
User state is stored in a Map structure, keyed by the user’s phone number: private STATE : Map < string , StateValue > = new Map ()
// Example: Map { '1234567890' => { name: 'John', age: 30 } }
State persists during bot runtime
State remains in memory while the bot is running. When the bot restarts, state is lost unless you persist it to a database.
Each user has isolated state
State for user 1234567890 is completely separate from state for user 0987654321.
Best Practices
Use descriptive state keys
// Good
await state . update ({ userEmail: email , isSubscribed: true })
// Avoid
await state . update ({ e: email , s: true })
. addAnswer ( 'Enter email:' , { capture: true }, async ( ctx , { state , fallBack }) => {
const email = ctx . body . trim ()
if ( ! email . includes ( '@' )) {
return fallBack ( 'Invalid email format' )
}
await state . update ({ email })
})
Clear state when sessions end
const logoutFlow = addKeyword ( 'logout' )
. addAction ( async ( ctx , { state , flowDynamic }) => {
state . clear ()
await flowDynamic ( 'Session ended. All data cleared.' )
})
Use global state sparingly
Global state is shared across all users. Use it only for truly global data like configuration or counters, not user-specific data.
Persist important state to database
For data that must survive bot restarts, save to your database: . addAction ( async ( ctx , { state , database }) => {
const userData = state . getMyState ()
await database . save ({ from: ctx . from , ... userData })
})
Common Patterns
const surveyFlow = addKeyword ( 'survey' )
. addAnswer ( 'Question 1: How old are you?' , { capture: true },
async ( ctx , { state }) => {
await state . update ({ q1: ctx . body })
})
. addAnswer ( 'Question 2: Where are you from?' , { capture: true },
async ( ctx , { state }) => {
await state . update ({ q2: ctx . body })
})
. addAction ( async ( ctx , { state , flowDynamic }) => {
const answers = {
age: state . get ( 'q1' ),
location: state . get ( 'q2' )
}
await saveSurveyResults ( answers )
await flowDynamic ( 'Thank you for completing the survey!' )
})
Conditional Flow Based on State
const menuFlow = addKeyword ( 'menu' )
. addAction ( async ( ctx , { state , flowDynamic , gotoFlow }) => {
const isRegistered = state . get ( 'isRegistered' )
if ( ! isRegistered ) {
await flowDynamic ( 'Please register first' )
return gotoFlow ( registerFlow )
}
await flowDynamic ( 'Welcome back! Here \' s your menu...' )
})
Next Steps