builderbot-code-skill

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

BuilderBot Code Skill

BuilderBot 编码规范

Operate

操作指南

  1. Identify the stack — confirm provider package, database adapter, and flow entrypoints before writing code.
  2. Type the callback signature correctly:
    • ctx: BotContext
      { body, from, name, host, ...platform fields }
    • methods: BotMethods
      { flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }
  3. Enforce flow-control semantics — these three MUST be
    return
    ed:
    • return gotoFlow(flow)
      ·
      return endFlow('msg')
      ·
      return fallBack('msg')
    • These MUST be
      await
      ed:
      await flowDynamic(...)
      ·
      await state.update(...)
      ·
      await globalState.update(...)
  4. Prefer small, composable flows — one responsibility per flow file.
  5. Keep secrets in env vars — never hardcode tokens, keys, or credentials.
  6. After writing any flow, run through the UX Review Checklist below — simulate the conversation as the end-user and verify every item before considering the flow done.
  7. Always validate lint before finishing — run
    eslint . --no-ignore
    and fix every error before delivering code.
  1. 确认技术栈 — 编写代码前先确认provider包、数据库适配器和流程入口。
  2. 正确编写回调函数签名
    • ctx: BotContext
      { body, from, name, host, ...平台专属字段 }
    • methods: BotMethods
      { flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }
  3. 严格遵守流程控制语义 — 以下三个语句必须加
    return
    • return gotoFlow(flow)
      ·
      return endFlow('msg')
      ·
      return fallBack('msg')
    • 以下语句必须加
      await
      await flowDynamic(...)
      ·
      await state.update(...)
      ·
      await globalState.update(...)
  4. 优先使用小型可组合流程 — 每个流程文件只承担单一职责。
  5. 密钥存放在环境变量中 — 永远不要硬编码token、密钥或凭证。
  6. 写完任何流程后,都要对照下方的UX检查清单走查 — 以终端用户视角模拟对话,确认所有项都满足后才算流程开发完成。
  7. 完成前务必检查lint规则 — 提交代码前执行
    eslint . --no-ignore
    并修复所有错误。

Critical Rules (common bugs)

关键规则(常见Bug)

BugWrongRight
Missing
return
on flow control
gotoFlow(flow)
return gotoFlow(flow)
Missing
await
on state/dynamic
state.update({...})
await state.update({...})
flowDynamic
with raw string array
flowDynamic(['a','b','c'])
— sends 3 separate messages
flowDynamic([{ body: ['a','b','c'].join('\n'), delay: rnd() }])
flowDynamic
+
endFlow
in same callback
await flowDynamic(...); return endFlow()
Split into two chained
addAction
— first sends
flowDynamic
, second calls
return endFlow(...)
Unnecessary dynamic import
await import('./flow')
when no circular dependency exists
Top-level
import { flow } from './flow'
— only use dynamic import when flow A → B AND B → A
Circular ESM importTop-level
import
between flows that reference each other
Dynamic
const { xFlow } = await import('./x.flow')
inside the callback
idle
without
capture
{ idle: 5000 }
{ capture: true, idle: 5000 }
Using
require()
in ESM
require('./flow')
await import('./flow')
— project is
"type": "module"
,
require
is not available
Using buttons
{ buttons: [...] }
Text-based numbered menu +
capture: true
Checking
idleFallBack
if (ctx?.idleFallBack) { ... }
Never use
idleFallBack
— just set
{ capture: true, idle: N }
and let the flow expire automatically
问题错误写法正确写法
流程控制语句缺少
return
gotoFlow(flow)
return gotoFlow(flow)
状态/动态消息操作缺少
await
state.update({...})
await state.update({...})
flowDynamic
传原始字符串数组
flowDynamic(['a','b','c'])
— 会发送3条独立消息
flowDynamic([{ body: ['a','b','c'].join('\n'), delay: rnd() }])
同一个回调中同时使用
flowDynamic
endFlow
await flowDynamic(...); return endFlow()
拆分为两个链式
addAction
— 第一个发送
flowDynamic
,第二个执行
return endFlow(...)
不必要的动态导入无循环依赖时使用
await import('./flow')
顶层导入
import { flow } from './flow'
— 仅当流程A调用B且流程B调用A时才使用动态导入
ESM循环导入互相引用的流程之间使用顶层
import
在回调内部使用动态导入
const { xFlow } = await import('./x.flow')
idle
配置未搭配
capture
{ idle: 5000 }
{ capture: true, idle: 5000 }
ESM项目中使用
require()
require('./flow')
await import('./flow')
— 项目是
"type": "module"
,不支持
require
使用按钮组件
{ buttons: [...] }
基于文本的编号菜单 +
capture: true
检查
idleFallBack
字段
if (ctx?.idleFallBack) { ... }
永远不要使用
idleFallBack
— 只需配置
{ capture: true, idle: N }
,流程会自动超时过期

Flow Chain

流程链

typescript
addKeyword(keywords, options?)        // ActionPropertiesKeyword: capture, idle, media, delay, regex, sensitive  ← NEVER use `buttons`
  .addAnswer(message, options?, cb?, childFlows?)
  .addAction(cb)
// cb: async (ctx: BotContext, methods: BotMethods) => void
typescript
addKeyword(keywords, options?)        // ActionPropertiesKeyword: capture, idle, media, delay, regex, sensitive  ← 永远不要使用`buttons`
  .addAnswer(message, options?, cb?, childFlows?)
  .addAction(cb)
// cb: async (ctx: BotContext, methods: BotMethods) => void

State Quick Reference

状态快速参考

typescript
// Per-user
await state.update({ key: value })
state.get('key')            // dot notation supported: 'user.profile.name'
state.getMyState()
state.clear()

// Global (shared across all users)
await globalState.update({ key: value })
globalState.get('key')
globalState.getAllState()
typescript
// 单用户状态
await state.update({ key: value })
state.get('key')            // 支持点语法: 'user.profile.name'
state.getMyState()
state.clear()

// 全局状态(所有用户共享)
await globalState.update({ key: value })
globalState.get('key')
globalState.getAllState()

UX Review Checklist

UX 检查清单

After building any flow, mentally walk through the conversation as the end-user and verify every point below. Fix anything that fails before delivering the code.
#QuestionWhat to check / fix
1Does every prompt tell the user exactly what to type?Each
addAnswer
with
capture: true
must state the expected input (e.g. "Reply with 1, 2, or 3").
2Are all invalid inputs handled?Every captured step must have a
fallBack('...')
branch for bad input.
3Is there an idle timeout on long captures?Add
{ capture: true, idle: 60000 }
— the flow will automatically expire after the timeout.
4Can the user always exit?Provide a cancel keyword (e.g. "cancel", "salir") or honour it inside captures and call
return endFlow('...')
.
5Are messages short, mobile-friendly, and max 3-4 bubbles?No wall-of-text AND no bubble spam. Group lines with
\n
inside one
FlowDynamicMessage.body
. Each string in a
flowDynamic([...])
array = a separate WhatsApp message — never spread long lists. Add random delay per bubble.
6Is the user's name used where natural?Greetings and confirmations should reference
ctx.name
when available.
7Are menus numbered text lists (never
buttons
)?
Use
['Option 1', '1. Foo', '2. Bar']
+
capture: true
+
fallBack
.
8Does each multi-step flow confirm before committing?Before irreversible actions (order, payment, delete) show a summary and ask "confirm? yes / no".
9Does the flow end with a clear closing message?The final step must tell the user what happened and what to do next (or say goodbye).
10Are error messages actionable?Never say just "error". Say what went wrong and how to fix it:
return fallBack('That doesn\'t look like a valid email. Try again:')
完成任意流程开发后,以终端用户身份在脑海中走一遍完整对话,验证下方所有项,提交代码前修复所有不满足的问题。
序号检查项校验/修复规则
1每个提示是否都明确告知用户需要输入的内容?所有配置了
capture: true
addAnswer
都必须说明期望的输入格式(例如:"请回复 123")。
2所有非法输入是否都有处理逻辑?每个捕获输入的步骤都必须配置
fallBack('...')
分支处理错误输入。
3长等待的捕获步骤是否配置了 idle 超时?添加
{ capture: true, idle: 60000 }
— 超时后流程会自动过期。
4用户是否随时可以退出流程?提供取消关键词(例如:"cancel"、"salir"),在捕获步骤中识别该关键词后调用
return endFlow('...')
5消息是否简短、适配移动端,且最多3-4个消息气泡?不要发送大段文本,也不要一次性发送过多气泡。用
\n
把多行内容合并到一个
FlowDynamicMessage.body
中。
flowDynamic([...])
数组中的每个字符串对应一条独立的WhatsApp消息 — 永远不要拆分长列表。每个气泡添加随机延迟。
6合适的场景下是否使用了用户姓名?问候和确认类消息有
ctx.name
时要引用用户名。
7菜单是否是编号文本列表(永远不要用
buttons
)?
使用
['选项1', '1. 选项A', '2. 选项B']
+
capture: true
+
fallBack
的实现方式。
8多步流程在执行不可逆操作前是否有确认步骤?执行不可逆操作(下单、支付、删除)前要展示摘要,询问用户"确认?yes / no"。
9流程结束时是否有清晰的收尾消息?最后一步要告知用户操作结果,以及后续需要做什么(或者道别)。
10错误提示是否可行动?不要只说"出错了",要说明问题原因和修复方式:
return fallBack('这看起来不是有效的邮箱地址,请重试:')

WhatsApp Text Formatting

WhatsApp 文本格式

WhatsApp uses its own markdown — always apply it in message strings.
StyleSyntaxExample
Bold
*text*
*Pepperoni*
Italic
_text_
_Tomate y mozzarella_
Strikethrough
~text~
~$15~
Monospace
```text```
```CODE123```
Bullet list
or
-
• Opción 1
WhatsApp有专属的markdown语法,所有消息字符串都要遵循该规范。
样式语法示例
粗体
*text*
*Pepperoni*
斜体
_text_
_Tomate y mozzarella_
删除线
~text~
~$15~
等宽字体
```text```
```CODE123```
无序列表
-
• Opción 1

Rules

格式规则

  • Use
    *bold*
    for product names, totals, section headers, and actions the user must take.
  • Use
    _italic_
    for descriptions, hints, and secondary info.
  • Use
    (not
    -
    ) for list items — renders cleaner on mobile.
  • Use
    ━━━━━━━
    (or
    ---
    ) as a visual divider between sections inside one bubble.
  • Never use HTML tags (
    <b>
    ,
    <br>
    , etc.) — WhatsApp ignores them.
  • Never use standard markdown (
    **bold**
    ,
    ## heading
    ) — not supported.
  • 产品名、总价、章节标题、用户需要执行的操作使用
    *粗体*
  • 描述、提示、次要信息使用
    _斜体_
  • 列表项使用
    (不要用
    -
    )— 移动端显示效果更好。
  • 同一个气泡内的不同章节用
    ━━━━━━━
    (或
    ---
    )作为视觉分隔线。
  • 永远不要使用HTML标签(
    <b>
    <br>
    等)— WhatsApp会忽略这类标签。
  • 永远不要使用标准markdown(
    **粗体**
    ## 标题
    )— 不支持。

Example — well-formatted bubble

示例 — 格式规范的气泡内容

typescript
const body = [
    '*🍕 Tu pedido*',
    '━━━━━━━━━━━━━━',
    `• Pizza: *Pepperoni*`,
    `• Tamaño: *Mediana (30 cm)*`,
    `• Cantidad: *2*`,
    '',
    `💰 Total: *$26 USD*`,
    '',
    '_Responde *sí* para confirmar o *no* para cancelar._',
].join('\n')
typescript
const body = [
    '*🍕 Tu pedido*',
    '━━━━━━━━━━━━━━',
    `• Pizza: *Pepperoni*`,
    `• Tamaño: *Mediana (30 cm)*`,
    `• Cantidad: *2*`,
    '',
    `💰 Total: *$26 USD*`,
    '',
    '_Responde *sí* para confirmar o *no* para cancelar._',
].join('\n')

WhatsApp Messaging Norms

WhatsApp 消息发送规范

These rules apply to every flow. Violating them makes the bot feel like spam.
Critical distinction —
flowDynamic
vs
addAnswer
with arrays:
CallArray behavior
flowDynamic(['a','b','c'])
⚠️ Each string = separate WhatsApp message
addAnswer(['a','b','c'])
✅ Joined into one message with line breaks
Always use
FlowDynamicMessage[]
with
.join('\n')
in
body
when calling
flowDynamic
. Never pass raw string arrays.
RuleWrongRight
Max 3-4 bubbles per turn
flowDynamic(['line1','line2','line3',...])
→ each string = 1 bubble
flowDynamic([{ body: ['line1','line2','line3'].join('\n'), delay: rnd() }])
Always use random delayNo delay between bubbles
delay: Math.floor(Math.random() * 800) + 500
on each bubble
Never spread item lists
flowDynamic([...items.map(...)])
flowDynamic([{ body: items.map(...).join('\n'), delay: rnd() }])
所有流程都要遵守以下规则,违反会让机器人看起来像垃圾消息。
关键区别 — 数组参数在
flowDynamic
addAnswer
中的表现:
调用方式数组行为
flowDynamic(['a','b','c'])
⚠️ 每个字符串对应一条独立的WhatsApp消息
addAnswer(['a','b','c'])
✅ 所有字符串用换行拼接成一条消息
调用
flowDynamic
时永远使用
FlowDynamicMessage[]
格式,把内容用
.join('\n')
合并到
body
中,不要直接传原始字符串数组。
规则错误写法正确写法
每个轮次最多3-4个气泡
flowDynamic(['line1','line2','line3',...])
→ 每个字符串对应1个气泡
flowDynamic([{ body: ['line1','line2','line3'].join('\n'), delay: rnd() }])
所有气泡都要加随机延迟气泡之间没有延迟每个气泡配置
delay: Math.floor(Math.random() * 800) + 500
不要拆分列表内容
flowDynamic([...items.map(...)])
flowDynamic([{ body: items.map(...).join('\n'), delay: rnd() }])

Random delay helper (define once per file)

随机延迟工具函数(每个文件定义一次即可)

typescript
const rnd = () => Math.floor(Math.random() * 800) + 500
typescript
const rnd = () => Math.floor(Math.random() * 800) + 500

Correct pattern — 3 bubbles max

正确示例 — 最多3个气泡

typescript
await flowDynamic([
    {
        body: [
            '*Título*',
            '',
            items.map(i => `${i.name} — $${i.price}`).join('\n'),
        ].join('\n'),
        delay: rnd(),
    },
    {
        body: '*Sección 2*\nLínea A\nLínea B',
        delay: rnd(),
    },
])
// addAnswer o addAction siguiente = bubble 3
typescript
await flowDynamic([
    {
        body: [
            '*Título*',
            '',
            items.map(i => `${i.name} — $${i.price}`).join('\n'),
        ].join('\n'),
        delay: rnd(),
    },
    {
        body: '*Sección 2*\nLínea A\nLínea B',
        delay: rnd(),
    },
])
// 后续的addAnswer或addAction = 第3个气泡

Presence Update (Baileys / Sherpa only)

在线状态更新(仅支持Baileys / Sherpa)

Simulates "typing..." or "recording..." before sending a message. Makes the bot feel human. Only available with
BaileysProvider
and
SherpaProvider
.
typescript
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = typing bubble   recording = audio bubble
发送消息前模拟「输入中...」或「录制中...」状态,让机器人更拟人。仅
BaileysProvider
SherpaProvider
支持该功能。
typescript
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = 输入中提示   recording = 录制音频提示

Pattern — typing indicator before each message

示例 — 每条消息前显示输入中提示

typescript
const waitT = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

.addAction(async (ctx, { provider, flowDynamic }) => {
    await provider.vendor.sendPresenceUpdate('composing', ctx.key.remoteJid)
    await waitT(1500)
    await flowDynamic([{ body: 'Mensaje que parece escrito por humano', delay: rnd() }])
    await provider.vendor.sendPresenceUpdate('paused', ctx.key.remoteJid)
})
typescript
const waitT = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

.addAction(async (ctx, { provider, flowDynamic }) => {
    await provider.vendor.sendPresenceUpdate('composing', ctx.key.remoteJid)
    await waitT(1500)
    await flowDynamic([{ body: 'Mensaje que parece escrito por humano', delay: rnd() }])
    await provider.vendor.sendPresenceUpdate('paused', ctx.key.remoteJid)
})

Rules

状态更新规则

  • Always call
    sendPresenceUpdate('paused', ...)
    after sending — clears the indicator.
  • Combine with
    rnd()
    delays: presence update → wait →
    flowDynamic
    → paused.
  • Use
    composing
    for text replies,
    recording
    for audio context.
  • Never use on non-Baileys/Sherpa providers — will throw at runtime.
  • 发送消息后必须调用
    sendPresenceUpdate('paused', ...)
    — 清除状态提示。
  • 配合
    rnd()
    延迟使用:更新状态 → 等待 → 发送
    flowDynamic
    → 清除状态。
  • 文本回复使用
    composing
    ,音频场景使用
    recording
  • 非Baileys/Sherpa服务商永远不要使用该功能 — 运行时会抛出错误。

Debug Checklist

调试检查清单

  • Flow not switching →
    return gotoFlow(...)
  • Session not ending →
    return endFlow(...)
  • Fallback not repeating →
    return fallBack(...)
  • State not saved →
    await state.update(...)
  • Idle not firing → add
    capture: true
    alongside
    idle
    (never check
    idleFallBack
    )
  • EVENTS flow not triggering → verify provider maps the event payload to
    EVENTS.*
  • Circular import crash → use dynamic
    await import()
    inside the callback (never
    require()
    )
  • Language server shows "Cannot find module" on dynamic imports → check if a circular dep actually exists; if not, replace with static top-level
    import
  • ESLint errors after changes → run
    eslint . --no-ignore
    and fix before finishing
  • builderbot/func-prefix-endflow-flowdynamic
    error →
    flowDynamic
    and
    endFlow
    are in the same callback; split into two chained
    addAction
    :
    typescript
    .addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) })
    .addAction(async (_, { endFlow }) => { return endFlow('bye') })
  • 流程不跳转 → 检查是否加了
    return gotoFlow(...)
  • 会话不结束 → 检查是否加了
    return endFlow(...)
  • Fallback不重复触发 → 检查是否加了
    return fallBack(...)
  • 状态不保存 → 检查是否加了
    await state.update(...)
  • Idle不触发 → 检查
    idle
    是否搭配了
    capture: true
    (永远不要检查
    idleFallBack
  • EVENTS流程不触发 → 检查provider是否把事件 payload 映射到了
    EVENTS.*
  • 循环导入崩溃 → 在回调内部使用动态
    await import()
    (永远不要用
    require()
  • 动态导入时语言服务提示「找不到模块」→ 检查是否真的存在循环依赖,如果没有,换成顶层静态
    import
  • 修改后出现ESLint错误 → 执行
    eslint . --no-ignore
    修复后再提交
  • 出现
    builderbot/func-prefix-endflow-flowdynamic
    错误 →
    flowDynamic
    endFlow
    放在了同一个回调中,拆分为两个链式
    addAction
    typescript
    .addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) })
    .addAction(async (_, { endFlow }) => { return endFlow('bye') })

Module System

模块系统

This project uses ESM (
"type": "module"
in package.json,
"module": "ES2022"
in tsconfig).
  • NEVER use
    require()
    — it is not available in ESM.
  • Default: use static top-level imports. Dynamic imports cause TypeScript language server errors when no circular dependency exists.
  • Before using
    await import()
    , draw the dependency graph. Dynamic import is only justified when flow A calls flow B and flow B calls flow A (a real cycle).
✅ Static (no cycle):  welcome → menu → order → payment
   import { orderFlow } from './order.flow'   ← top of file

✅ Dynamic (real cycle): welcome ↔ order
   const { orderFlow } = await import('./order.flow')  ← inside callback
When flows reference each other (circular), use dynamic
import()
inside the callback:
typescript
.addAction(async (ctx, { gotoFlow }) => {
    const { targetFlow } = await import('./target.flow')
    return gotoFlow(targetFlow)
})
本项目使用ESM(package.json中配置
"type": "module"
,tsconfig中配置
"module": "ES2022"
)。
  • 永远不要使用
    require()
    — ESM不支持该语法。
  • 默认使用顶层静态导入:没有循环依赖时使用动态导入会导致TypeScript语言服务报错。
  • 使用
    await import()
    先梳理依赖关系,仅当流程A调用流程B流程B调用流程A(真实循环依赖)时才需要使用动态导入。
✅ 静态导入(无循环): 欢迎页 → 菜单 → 下单 → 支付
   import { orderFlow } from './order.flow'   ← 写在文件顶部

✅ 动态导入(真实循环): 欢迎页 ↔ 下单页
   const { orderFlow } = await import('./order.flow')  ← 写在回调内部
当流程之间互相引用(循环依赖)时,在回调内部使用动态
import()
typescript
.addAction(async (ctx, { gotoFlow }) => {
    const { targetFlow } = await import('./target.flow')
    return gotoFlow(targetFlow)
})

Modular Structure (recommended)

推荐模块化结构

Organize flows in a
src/flows/
directory with a barrel
index.ts
that exports the assembled flow:
src/
├── app.ts
├── flows/
│   ├── index.ts            # createFlow([...all flows])
│   ├── welcome.flow.ts
│   └── order.flow.ts
└── services/
typescript
// src/flows/index.ts
import { createFlow } from '@builderbot/bot'
import { welcomeFlow } from './welcome.flow'
import { orderFlow } from './order.flow'

export const flow = createFlow([welcomeFlow, orderFlow])
typescript
// src/app.ts
import { createBot, createProvider } from '@builderbot/bot'
import { MemoryDB as Database } from '@builderbot/bot'
import { BaileysProvider as Provider } from '@builderbot/provider-baileys'
import { flow } from './flows'

const main = async () => {
    const provider = createProvider(Provider)
    const database = new Database()
    await createBot({ flow, provider, database })
    provider.initHttpServer(+(process.env.PORT ?? 3008))
}
main()
把流程放在
src/flows/
目录下,通过桶文件
index.ts
导出组装好的总流程:
src/
├── app.ts
├── flows/
│   ├── index.ts            # createFlow([...所有子流程])
│   ├── welcome.flow.ts
│   └── order.flow.ts
└── services/
typescript
// src/flows/index.ts
import { createFlow } from '@builderbot/bot'
import { welcomeFlow } from './welcome.flow'
import { orderFlow } from './order.flow'

export const flow = createFlow([welcomeFlow, orderFlow])
typescript
// src/app.ts
import { createBot, createProvider } from '@builderbot/bot'
import { MemoryDB as Database } from '@builderbot/bot'
import { BaileysProvider as Provider } from '@builderbot/provider-baileys'
import { flow } from './flows'

const main = async () => {
    const provider = createProvider(Provider)
    const database = new Database()
    await createBot({ flow, provider, database })
    provider.initHttpServer(+(process.env.PORT ?? 3008))
}
main()

References

参考资料

  • Code patterns (scaffold, capture, gotoFlow, media, idle, flowDynamic, fallBack, REST, EVENTS, UX patterns): patterns.md
  • Provider configs, database configs, TypeScript types: providers.md
  • 代码模式(脚手架、输入捕获、gotoFlow、媒体资源、idle、flowDynamic、fallBack、REST、EVENTS、UX模式):patterns.md
  • Provider配置、数据库配置、TypeScript类型定义:providers.md