Wiki
猫Scenes
Scenes are Laniakea's stateful routing layer for multi-step and modal bot flows. A scene is registered inside a plugin, entered through MessageContext, stored through SessionStore, and routed before normal command handling while the session is active.
What scenes give you
- Scene registration through
Plugin.NewScene(...)andPlugin.AddScene(...). - Explicit entry and exit through
MessageContext.EnterScene(...),EnterSceneStep(...), andExitScene(). - Per-user, per-chat, or per-user-chat session scopes.
- Step handlers, scene-local commands, and a scene-level message fallback.
- JSON-backed scene state through
SceneContext.BindData(...)andSaveData(...). - A default in-memory store plus
SessionStorefor custom persistence.
Core API
type SceneScope int
const (
SceneScopeUser SceneScope = iota
SceneScopeChat
SceneScopeUserChat
)
type SceneSession struct {
Scene string
Step string
Data []byte
}
type SessionStore interface {
Get(key string) (SceneSession, error)
Set(key string, session SceneSession) error
Delete(key string) error
}
MemorySessionStore is the default implementation. Use Bot.SetSessionStore(...) to replace it.
Scopes
SceneScopeUsershares one scene session across all chats for a user.SceneScopeChatshares one scene session across all users in a chat.SceneScopeUserChatisolates one session per(user, chat)pair.
Recommended default:
- Use
SceneScopeUserChatfor most interactive flows. - Use
SceneScopeUseronly when the same logical flow should continue across chats. - Use
SceneScopeChatfor room-level shared workflows.
Registration
Scenes are registered inside plugins in the same style as commands and payloads.
plugin.NewScene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", askName).
OnStep("confirm", confirmSignup).
OnCommand("cancel", cancelSignup).
OnMessage(fallbackMessage)
SetEntry(...) is required for ctx.EnterScene(...). ctx.EnterSceneStep(...) can start from a specific registered step instead.
Handler model
Normal commands use *MessageContext. Scene handlers use *SceneContext.
type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
SceneContext embeds *MessageContext and adds scene helpers:
ctx.Stay()ctx.Next(step)ctx.Exit()ctx.Pass()ctx.BindData(&dst)ctx.SaveData(src)
Scene handlers return SceneResult to control session flow:
Stay: keep the same scene and step.Next(step): move to another registered step.Exit: delete the current session.Pass: do not change the current session and continue normal routing.
SceneActionPass is intentionally a no-op for scene state. If a handler calls SaveData(...) and then returns Pass, that state is not persisted.
Routing order
While a scene session is active, routing works like this:
- Bot middleware runs first.
- The bot resolves the active scene session from the configured scope priority.
- Plugin middleware for the owning plugin runs.
- Scene-local commands are checked first.
- If no scene command matches, the current step handler runs.
- If no step handler matches, the scene-level
OnMessage(...)fallback runs. - If the scene returns
Pass, normal plugin command and update routing continues.
The scene text comes from message text or caption. Scenes are therefore designed around message-based flows.
State between steps
Use SceneSession.Data only through SceneContext.BindData(...) and SaveData(...) unless you are implementing custom store behavior.
type ProfileDraft struct {
Name string
Age int
}
func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
var draft ProfileDraft
if err := ctx.BindData(&draft); err != nil {
return laniakea.SceneResult{}, err
}
draft.Name = ctx.Text
if err := ctx.SaveData(draft); err != nil {
return laniakea.SceneResult{}, err
}
return ctx.Next("age"), nil
}
The store contract stays intentionally small because Data []byte is storage-agnostic.
Example flow
Command entry:
func startSignup(ctx *laniakea.MessageContext, db *App) error {
return ctx.EnterScene("signup")
}
Step handler:
func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
if ctx.Text == "" {
ctx.Answer("What is your name?")
return ctx.Stay(), nil
}
ctx.Answer("Thanks.")
return ctx.Next("confirm"), nil
}
Scene-local command:
func cancelSignup(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
ctx.Answer("Signup cancelled")
return ctx.Exit(), nil
}
Deliberate limits of the current model
- Scene-local payload routing is not implemented.
- The internal scene runtime is not exposed as public inspection API.
- The helper surface for scene state is intentionally small.
Related pages:
Navigation
Start here
Runtime and Architecture
- Bot-Lifecycle
- Webhook-Runtime
- Middleware
- Runners
- Error-Handling
- Logging
- Update-Routing-Model
- Policies
- Scenes
Interaction and Telegram API
- Inline-Keyboards-and-Payloads
- Auto-Generated-Commands
- Drafts
- Rich-Messages
- Localization
- Rate-Limiting
- tgapi-Overview