一键导入
yaebal-flows
Use when building multi-step dialogs in a yaebal bot — choosing between scenes, conversation, and prompt, and wiring their persistence.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when building multi-step dialogs in a yaebal bot — choosing between scenes, conversation, and prompt, and wiring their persistence.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when writing or modifying a Telegram bot built on the yaebal framework — bot setup, handlers, filter queries, commands, and context typing.
Use when adding LLM features to a yaebal bot with @yaebal/ai — ctx.ai streaming replies, model adapters, conversation memory, and rate limits.
Use when writing a custom yaebal plugin — the Plugin<In, Out> contract, derive vs decorate, and typed plugin dependencies.
Use when a yaebal bot fails to compile, start, or respond — install-order type errors, ESM specifier errors, Telegram 400/409/429 responses.
Use when running a yaebal bot in production — long polling with @yaebal/runner vs webhooks with @yaebal/web, graceful shutdown, and error handling.
Use when adding inline/reply keyboards or handling button presses in a yaebal bot — @yaebal/keyboard builders and @yaebal/callback-data typed payloads.
| name | yaebal-flows |
| description | Use when building multi-step dialogs in a yaebal bot — choosing between scenes, conversation, and prompt, and wiring their persistence. |
four first-party options, by shape of the flow:
@yaebal/prompt — one-off question: "ask, handle the next message". in-memory, lost on
restart. reach for it first; escalate only when the flow grows.@yaebal/conversation — linear async flows written as a straight line of
await cv.waitFor(...). most natural to read; durable only if you opt in.@yaebal/scenes — declarative step machines. each step re-asks on its own, so wizards
navigate (next/previous/go), nest, validate per step, and resume mid-flow after a
restart. best for long registration/checkout wizards that must survive deploys.@yaebal/morda — keyboard-first dialogs: declarative windows rendered into ONE
editable message, callback routing by button id, a persisted navigation stack. best for
menus/settings/cockpits where the user taps buttons instead of typing.import { prompt } from "@yaebal/prompt";
bot.install(prompt());
bot.command("name", (ctx) =>
ctx.prompt("what's your name?", (answer) => answer.send(`hi ${answer.text}!`)),
);
the answering message is consumed — check for /-escapes inside the handler if users may
type a command instead of an answer.
import { conversation, createConversation } from "@yaebal/conversation";
const greet = createConversation(async (cv, ctx) => {
await ctx.send("what's your name?");
const answer = await cv.waitFor("message:text"); // narrowed: answer.text is string
await answer.send(`hi ${answer.text}!`);
});
const bot = createBot(token).install(conversation({ greet }));
bot.command("greet", (ctx) => ctx.conversation.enter("greet"));
bot.command("cancel", (ctx) => ctx.conversation.active && ctx.conversation.leave());
cv.form.text/int/choice/confirm are ready-made ask-validate-reask loops; every wait accepts
a timeout. pass options.storage (a @yaebal/sklad StorageAdapter) to switch to the
durable replay engine — then the builder must be deterministic: route every side effect
through ctx or cv.external().
import { ask, defineScene, scenes } from "@yaebal/scenes";
const register = defineScene<Context, { name: string; age: number }>({
steps: [
ask("name", { question: "what's your name?" }),
ask("age", {
question: (ctx) => `nice, ${ctx.scene.state.name}! how old are you?`,
parse: (text) => (/^\d+$/.test(text) ? Number(text) : undefined),
invalid: "age is a number — try again",
}),
],
onLeave: (ctx, info) => info.reason === "finish" && ctx.send(`saved ${ctx.scene.state.name}`),
});
const bot = createBot(token).install(scenes({ register }, { storage }));
bot.command("register", (ctx) => ctx.scene.enter("register"));
bot.command("cancel", (ctx) => ctx.scene.leave({ cancelled: true }));
import { back, button, defineDialog, dialogs, switchTo } from "@yaebal/morda";
type BotContext = Context & { session: { dark: boolean } };
// declare the ambient context with defineDialog (curried — window ids stay literal):
// window callbacks then see plugin fields typed, and install order is compiler-checked.
// NEVER take ctx as `unknown` and cast inside a window — declare the context here instead.
const def = defineDialog<BotContext>()({
main: () => ({ text: "cockpit", keyboard: [[switchTo("settings →", "settings")]] }),
settings: {
render: (ctx) => ({
text: `dark: ${ctx.session.dark ? "on" : "off"}`,
keyboard: [[button<BotContext>("toggle", {
id: "t",
onClick: async (c) => {
c.session.dark = !c.session.dark; // typed — session comes from BotContext
await c.dialog.rerender();
},
})], [back()]],
}),
},
});
const bot = createBot(token)
.install(session({ initial: () => ({ dark: false }) }))
.install(dialogs(def, { storage }));
bot.command("start", (ctx) => ctx.dialog.start("main"));
on a createBot() bot the runtime context inside windows is the rich per-update class —
include it in the declared context (MessageContext & { session: … }) and shortcuts like
ctx.delete() type-check inside onText too.
@yaebal/session — persistence goes through a @yaebal/sklad
StorageAdapter passed to the plugin itself. the default is in-memory (lost on restart):
fine for prompt and dev, pass real storage (redis/sqlite/file) for production wizards./commands bypass an active scene/conversation by default (passCommands), and unclaimed
updates fall through to normal handlers (passthrough) — so a global /cancel keeps
working. don't re-implement command escapes per step.chat.id:from.id by default — safe in groups.conversation.enter() resolves when the flow starts, not when it finishes — read results
in onLeave(ctx, info) via info.result.on filter queries — for
inline-keyboard wizards give the step on: "callback_query:data".