@yaebal/onboarding

declarative first-run tutorials and product tours. build a flow, install it as a typed plugin, then control it from handlers through ctx.onboarding.<id>.

install

terminal
pnpm add @yaebal/onboarding

usage

createOnboarding({ id }) returns a fluent builder. each step() adds a typed step id; after .build(), bot.install(welcome) widens the context with ctx.onboarding.welcome.

bot.ts
import { Bot } from "@yaebal/core";
import { createOnboarding } from "@yaebal/onboarding";

const welcome = createOnboarding({ id: "welcome" })
  .step("hello", {
    text: "hi. i'll show you around.",
    buttons: ["next", "dismiss"],
  })
  .step("commands", {
    text: "use /help for commands and /settings to tune the bot.",
    buttons: ["next", "exit"],
  })
  .step("done", { text: "you're ready." })
  .onComplete((ctx) => ctx.send("welcome aboard."))
  .build();

const bot = new Bot(token).install(welcome);

bot.command("start", (ctx) => ctx.onboarding.welcome.start());
bot.command("tour", (ctx) => ctx.onboarding.welcome.start({ force: true }));

flow controls

controls.ts
bot.command("status", (ctx) => {
  const flow = ctx.onboarding.welcome;
  return ctx.reply("status=" + flow.status + ", step=" + (flow.currentStep ?? "none"));
});

bot.command("skip", (ctx) => ctx.onboarding.welcome.skip());
bot.command("exit", (ctx) => ctx.onboarding.welcome.exit());
bot.command("disable", (ctx) => ctx.onboarding.disableAll());
bot.command("enable", (ctx) => ctx.onboarding.enableAll());
memberdescription
statusnull | active | paused | exited | completed | dismissed
currentStepthe active step id, typed from the builder chain
datamutable JSON-ish bag persisted with the flow record
start(opts?)start or resume. pass force: true to restart after completion
next({ from })advance with an optional stale-step guard
goto(id)jump to a typed step id
skip/exit/dismiss/undismiss/completeterminal and convenience operations

buttons

built-in buttons generate safe onboarding callback tokens. explicit button objects can jump to a step, link out, or use your own callback_data.

buttons.ts
.step("pick", {
  text: "where next?",
  buttons: [
    "next",
    { text: "skip setup", goto: "done" },
    { text: "docs", url: "https://yaebal.pages.dev" },
  ],
})

storage & scope

storage.ts
createOnboarding({
  id: "welcome",
  storage: myStorage, // { get(key), set(key, value), delete(key) }
  scope: "user",      // default. use "chat" for per-chat tours
});

state is in memory by default. scope: "user" keys by ctx.from.id; scope: "chat" keys by ctx.chat.id; a function can return a custom key.

example bot

there is a runnable bot under examples/onboarding. run it with pnpm --filter @yaebal/example-onboarding dev after adding BOT_TOKEN to its .env file.

callback_data budget matters. flow and step ids are embedded into Telegram callback data. keep them short and use only letters, numbers, _, and -.

last step completes after render. if a flow has no next step, onboarding renders the current step, marks the flow completed, then runs onComplete.