builderbot-code-skill について
BuilderBot Code Skillは、BuilderBotでチャットボットのフローを作る開発者向けの実装ガイドです。フロー制御、状態管理、モジュールのインポート、WhatsAppでの会話UXを扱います。
主な機能
- 01フロー制御におけるreturnとawaitの規則
- 02状態とコールバック型のリファレンス
- 03モジュールのインポートと循環依存の指針
- 04会話UXのレビューチェックリスト
- 05WhatsAppの書式とメッセージ規則
活用シーン
- 01入力を受け取る複数段階のフローを実装
- 02フロー遷移や状態更新の問題を調査
- 03組み合わせ可能なフローモジュールを構成
- 04WhatsAppメッセージの読みやすさを確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ leifermendez/ skill-builderbot-code --skill builderbot-code-skillインストール後、Claude Code で/builderbot-code-skillを入力して読み込まれたか確認します。
同梱ファイルpatterns.mdproviders.md
詳細
BuilderBot Code スキル
操作
- スタックを特定する — コードを書く前に、プロバイダーパッケージ、データベースアダプター、フローのエントリーポイントを確認します。
- コールバックのシグネチャーを正しく記述する:
ctx: BotContext→{ body, from, name, host, ...platform fields }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(...)
- 小さく、組み合わせ可能なフローを優先する — フローファイルごとに責務を1つにします。
- シークレットは環境変数に保存する — トークン、キー、認証情報をハードコードしないでください。
- フローを作成したら、必ず以下の UX レビューチェックリストを実施する — エンドユーザーとして会話をシミュレートし、フローが完成したと判断する前に、すべての項目を確認します。
- 終了前に必ず lint を検証する —
eslint . --no-ignoreを実行し、コードを提出する前にすべてのエラーを修正します。
重要なルール(よくあるバグ)
| バグ | 誤り | 正しい例 |
|---|---|---|
フロー制御で return がない | gotoFlow(flow) | return gotoFlow(flow) |
state/dynamic で 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 を2つに分けて連結します。最初に flowDynamic を送信し、次に return endFlow(...) を呼び出します |
| 不要な動的インポート | 循環依存がないのに await import('./flow') を使う | トップレベルで import { flow } from './flow' を使います — 動的インポートは、フロー A → B かつ B → A の場合にのみ使用します |
| ESM の循環インポート | 相互に参照するフロー間でトップレベルの import を使う | コールバック内で動的に const { xFlow } = await import('./x.flow') を使います |
capture なしで idle を使う | { 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 } を設定し、フローが自動的に期限切れになるようにします |
フローチェーン
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
state クイックリファレンス
// 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()
UX レビューチェックリスト
フローを構築したら、エンドユーザーの立場で会話を頭の中でたどり、以下の各項目を確認してください。コードを納品する前に、問題があれば修正してください。
| # | 質問 | 確認・修正する内容 |
|---|---|---|
| 1 | すべてのプロンプトで、ユーザーが何を入力すればよいかを正確に伝えていますか? | capture: true を指定した各 addAnswer で、期待する入力を明示する必要があります(例:「1、2、または 3 と返信してください」)。 |
| 2 | 無効な入力をすべて処理していますか? | 入力を受け取る各ステップに、無効な入力用の fallBack('...') 分岐が必要です。 |
| 3 | 入力待ちが長い場合にアイドルタイムアウトを設定していますか? | { capture: true, idle: 60000 } を追加してください。タイムアウト後、フローは自動的に終了します。 |
| 4 | ユーザーはいつでも終了できますか? | キャンセルキーワード(例:「cancel」、「salir」)を用意するか、入力待ちの中で認識して return endFlow('...') を呼び出してください。 |
| 5 | メッセージは短く、モバイル向けで、バブル数が最大3〜4個ですか? | 長文にも、バブルの乱発にもならないようにしてください。FlowDynamicMessage.body の中で \n を使って行をまとめます。flowDynamic([...]) 配列内の各文字列は個別の WhatsApp メッセージになります。長いリストを分割して送信しないでください。バブルごとにランダムな遅延を追加してください。 |
| 6 | 自然な箇所でユーザーの名前を使っていますか? | 挨拶や確認では、利用可能な場合に ctx.name を参照してください。 |
| 7 | メニューは番号付きのテキストリストですか(buttons は使っていませんか)? | ['Option 1', '1. Foo', '2. Bar'] と capture: true、fallBack を組み合わせてください。 |
| 8 | 複数ステップの各フローで、確定前に確認していますか? | 取り消せない操作(注文、支払い、削除)の前に、概要を表示して「確認しますか? yes / no」と尋ねてください。 |
| 9 | フローは、明確な締めくくりのメッセージで終わっていますか? | 最後のステップで、何が起きたかと次に何をすべきかをユーザーに伝えてください(または別れの挨拶をしてください)。 |
| 10 | エラーメッセージは、対処方法がわかる内容ですか? | 「エラー」とだけ伝えてはいけません。何が問題で、どう修正すればよいかを伝えてください。例:return fallBack('有効なメールアドレスではないようです。もう一度入力してください:') |
WhatsApp テキストの書式設定
WhatsApp 独自の Markdown を使用します。メッセージ文字列には必ず適用してください。
| スタイル | 構文 | 例 |
|---|---|---|
| 太字 | *text* | *Pepperoni* |
| 斜体 | _text_ | _Tomate y mozzarella_ |
| 取り消し線 | ~text~ | ~$15~ |
| 等幅 | ```text``` | ```CODE123``` |
| 箇条書き | • または - | • Opción 1 |
ルール
- 商品名、合計、セクション見出し、ユーザーが実行する必要のある操作には
*bold*を使います。 - 説明、ヒント、補足情報には
_italic_を使います。 - リスト項目には
-ではなく•を使います。モバイルで見やすく表示されます。 - 1つのバブル内でセクションを区切るには、視覚的な区切りとして
━━━━━━━(または---)を使います。 - HTML タグ(
<b>、<br>など)は使わないでください。 WhatsApp では無視されます。 - 標準の Markdown(
**bold**、## heading)は使わないでください。 サポートされていません。
例 — 書式設定されたバブル
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 メッセージの規範
これらのルールはすべてのフローに適用されます。違反すると、ボットがスパムのように感じられます。
重要な違い — flowDynamic と配列を使った addAnswer:
| 呼び出し | 配列の動作 |
|---|---|
flowDynamic(['a','b','c']) | ⚠️ 各文字列が個別の WhatsApp メッセージになります |
addAnswer(['a','b','c']) | ✅ 改行で結合され、1つのメッセージになります |
flowDynamicを呼び出すときは、body内で.join('\n')を使ったFlowDynamicMessage[]を必ず使用してください。生の文字列配列を渡してはいけません。
| ルール | 誤り | 正しい |
|---|---|---|
| 1ターンあたり最大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() }]) |
ランダム遅延ヘルパー(ファイルごとに一度定義)
const rnd = () => Math.floor(Math.random() * 800) + 500
正しいパターン — 最大3個のバブル
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
プレゼンス更新(Baileys / Sherpa のみ)
メッセージを送信する前に「入力中…」または「録音中…」をシミュレートします。ボットが人間らしく感じられます。
BaileysProviderとSherpaProviderでのみ利用できます。
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = typing bubble recording = audio bubble
パターン — 各メッセージの前に入力インジケーターを表示
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)
})
ルール
- 送信後は必ず
sendPresenceUpdate('paused', ...)を呼び出してください。インジケーターがクリアされます。 rnd()による遅延と組み合わせてください。プレゼンス更新 → 待機 →flowDynamic→ paused の順です。- テキストの返信には
composing、音声のコンテキストにはrecordingを使用してください。 - Baileys/Sherpa 以外のプロバイダーでは絶対に使用しないでください。実行時にエラーが発生します。
デバッグチェックリスト
- フローが切り替わらない →
return gotoFlow(...) - セッションが終了しない →
return endFlow(...) - フォールバックが繰り返されない →
return fallBack(...) - 状態が保存されない →
await state.update(...) - アイドルイベントが発生しない →
idleと併せてcapture: trueを追加してください(idleFallBackは絶対に確認しないでください) - EVENTS フローがトリガーされない → プロバイダーがイベントペイロードを
EVENTS.*にマッピングしていることを確認してください - 循環インポートでクラッシュする → コールバック内で動的な
await import()を使用してください(require()は絶対に使用しないでください) - 動的インポートで言語サーバーに「Cannot find module」と表示される → 循環依存が実際に存在するか確認してください。存在しない場合は、静的なトップレベルの
importに置き換えてください - 変更後に ESLint エラーが発生する →
eslint . --no-ignoreを実行し、完了前に修正してください builderbot/func-prefix-endflow-flowdynamicエラー →flowDynamicとendFlowが同じコールバック内にあります。2 つのaddActionに分けてチェーンしてください:.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) }) .addAction(async (_, { endFlow }) => { return endFlow('bye') })
モジュールシステム
このプロジェクトでは ESM を使用しています(package.json の "type": "module"、tsconfig の "module": "ES2022")。
- 絶対に
require()を使用しないでください。ESM では利用できません。 - 基本的には静的なトップレベルのインポートを使用してください。 循環依存がない場合、動的インポートを使うと言語サーバーで TypeScript エラーが発生します。
await import()を使用する前に、依存関係のグラフを描いてください。動的インポートが正当化されるのは、フロー A がフロー B を呼び出し、かつフロー B がフロー A を呼び出す(実際に循環している)場合のみです。
✅ 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
フロー同士が相互に参照する(循環する)場合は、コールバック内で 動的な import() を使用してください。
.addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})
モジュール構成(推奨)
フローを src/flows/ ディレクトリに整理し、組み立てたフローをエクスポートするバレル index.ts を用意してください。
src/
├── app.ts
├── flows/
│ ├── index.ts # createFlow([...all flows])
│ ├── welcome.flow.ts
│ └── order.flow.ts
└── services/
// 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])
// 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()
参考資料
- コードパターン(スキャフォールド、キャプチャ、gotoFlow、メディア、idle、flowDynamic、fallBack、REST、EVENTS、UX パターン):patterns.md
- プロバイダー設定、データベース設定、TypeScript 型:providers.md