Teleflow - Simple Telegram Bot Framework

August 27, 2026 ยท View on GitHub

Go Reference Go Report Card Codecov

Quick Start for Go Developers: Simple, type-safe Telegram bot framework with fluent flows and automatic state management.

๐Ÿš€ Quick Overview

Teleflow is a Telegram bot framework designed for simplicity and ease of use:

  • Type-Safe Flows: Use Go generics (teleflow.Flow[T]) for compile-time safety
  • Automatic State Management: No manual session handling needed
  • Non-Linear Conversations: Named steps and GoTo actions for complex flows
  • Middleware Support: Standard Handler interface for authentication, logging, etc.
  • Large Callback Data: Built-in mechanism to pass complex data through buttons
  • Clean API: Simple, consistent interface for all common bot operations

๐Ÿ“š Documentation

For comprehensive guides and architectural details, see our documentation:

๐Ÿ—๏ธ Basic Bot Setup

Minimal Bot

import (
 "log"
 "os"

 teleflow "github.com/kslamph/teleflow/core"
)

// Set up router with handlers
router := teleflow.NewRouter()
router.Handle("start", teleflow.Func(func(ctx *teleflow.Context) error {
    return ctx.ReplyText("Welcome to Teleflow!")
}))

// Create bot with token and router
bot, err := teleflow.NewBot(os.Getenv("TELEGRAM_BOT_TOKEN"), router)
if err != nil {
    log.Fatal(err)
}

log.Fatal(bot.Start())

Simple Flow Bot

// 1. Define a struct for your flow's data.
type UserProfile struct {
    Name string
    Age  int
}

// 2. Create a new Flow with your data type.
profileFlow := teleflow.NewFlow[UserProfile]()

// 3. Define the steps of your conversation.
nameStep := teleflow.Step[UserProfile]{
    Name: "Name",
    Prompt: func(ctx *teleflow.Context, data *UserProfile) error {
        return ctx.ReplyText("What is your name?")
    },
    Process: func(ctx *teleflow.Context, data *UserProfile) (teleflow.FlowAction, error) {
        data.Name = ctx.Text()
        return teleflow.Next(), nil
    },
}

ageStep := teleflow.Step[UserProfile]{
    Name: "Age",
    Prompt: func(ctx *teleflow.Context, data *UserProfile) error {
        return ctx.ReplyText(fmt.Sprintf("Nice to meet you, %s! How old are you?", data.Name))
    },
    Process: func(ctx *teleflow.Context, data *UserProfile) (teleflow.FlowAction, error) {
        age, err := strconv.Atoi(ctx.Text())
        if err != nil {
            ctx.ReplyText("Please enter a valid number.")
            return teleflow.Retry(), nil
        }
        data.Age = age
        return teleflow.End(), nil // End the flow.
    },
}

// 4. Add steps to the flow and set a completion handler.
if _, err := profileFlow.AddStep(nameStep); err != nil { log.Fatal(err) }
if _, err := profileFlow.AddStep(ageStep); err != nil { log.Fatal(err) }
profileFlow.OnComplete(func(ctx *teleflow.Context, data UserProfile) error {
    return ctx.ReplyText(fmt.Sprintf("Thanks, %s! Your profile is saved.", data.Name))
})

// 5. Set up the router and bot.
router := teleflow.NewRouter()
router.Handle("profile", profileFlow)

bot, err := teleflow.NewBot(os.Getenv("TELEGRAM_BOT_TOKEN"), router)
if err != nil {
    log.Fatal(err)
}

log.Fatal(bot.Start())

๐Ÿ“ก Advanced Messaging

Simple Messaging

// Simple text only
ctx.ReplyText("Hello World")

Rich Messaging with Keyboards

// Rich message with inline keyboard
keyboard := teleflow.NewInlineKeyboard().AddRow(
    teleflow.ButtonCallback("Option 1", "opt1"),
    teleflow.ButtonCallback("Option 2", "opt2"),
).Build()

ctx.ReplyWithKeyboard("Choose an option:", keyboard)

Formatted Messaging with Parse Modes

Teleflow supports sending formatted messages using HTML, Markdown, or MarkdownV2 parse modes:

// HTML formatting
ctx.ReplyTextWithParseMode("<b>Bold text</b> and <i>italic text</i>", teleflow.ParseModeHTML)

// MarkdownV2 formatting
ctx.ReplyTextWithParseMode("*Bold text* and _italic text_", teleflow.ParseModeMarkdownV2)

// With keyboard and formatting
keyboard := teleflow.NewInlineKeyboard().AddRow(
    teleflow.ButtonCallback("Option 1", "opt1"),
    teleflow.ButtonCallback("Option 2", "opt2"),
).Build()

ctx.ReplyWithKeyboardAndParseMode("*Choose an option:*", keyboard, teleflow.ParseModeMarkdownV2)

// Editing messages with formatting
ctx.EditMessageTextWithParseMode(messageID, "<u>Updated</u> <s>text</s>", teleflow.ParseModeHTML)

๐ŸŒŠ Flow Architecture

Simple Flow with OnComplete

// Define your data structure
type RegistrationData struct {
    Name string
    Age  int
}

// Create a new flow
registrationFlow := teleflow.NewFlow[RegistrationData]()

// Define steps
welcomeStep := teleflow.Step[RegistrationData]{
    Name: "Welcome",
    Prompt: func(ctx *teleflow.Context, data *RegistrationData) error {
        return ctx.ReplyText("Welcome! What is your name?")
    },
    Process: func(ctx *teleflow.Context, data *RegistrationData) (teleflow.FlowAction, error) {
        data.Name = ctx.Text()
        return teleflow.Next(), nil
    },
}

ageStep := teleflow.Step[RegistrationData]{
    Name: "Age",
    Prompt: func(ctx *teleflow.Context, data *RegistrationData) error {
        return ctx.ReplyText(fmt.Sprintf("Nice to meet you, %s! How old are you?", data.Name))
    },
    Process: func(ctx *teleflow.Context, data *RegistrationData) (teleflow.FlowAction, error) {
        age, err := strconv.Atoi(ctx.Text())
        if err != nil {
            ctx.ReplyText("Please enter a valid number.")
            return teleflow.Retry(), nil
        }
        data.Age = age
        return teleflow.End(), nil
    },
}

// Add steps to the flow
if _, err := registrationFlow.AddStep(welcomeStep); err != nil { log.Fatal(err) }
if _, err := registrationFlow.AddStep(ageStep); err != nil { log.Fatal(err) }

// Set completion handler
registrationFlow.OnComplete(func(ctx *teleflow.Context, data RegistrationData) error {
    return ctx.ReplyText(fmt.Sprintf("Thanks, %s! Registration complete. Age: %d", data.Name, data.Age))
})

// Register with router
router := teleflow.NewRouter()
router.Handle("register", registrationFlow)

๐ŸŽ›๏ธ Flow Control

Teleflow provides several flow control actions:

  • teleflow.Next() - Continue to next step
  • teleflow.Retry() - Re-prompt current step
  • teleflow.End() - Finish flow, trigger OnComplete
  • teleflow.GoTo("step_name") - Jump to specific named step

Non-Linear Flow Example

confirmationStep := teleflow.Step[RegistrationData]{
    Name: "Confirmation",
    Prompt: func(ctx *teleflow.Context, data *RegistrationData) error {
        keyboard := teleflow.NewInlineKeyboard().AddRow(
            teleflow.ButtonCallback("โœ… Yes, Correct", "confirm"),
            teleflow.ButtonCallback("โŒ No, Restart", "restart"),
        ).Build()

        promptText := fmt.Sprintf(
            "Please confirm your details:\nName: %s\nAge: %d",
            data.Name, data.Age,
        )
        return ctx.ReplyWithKeyboard(promptText, keyboard)
    },
    Process: func(ctx *teleflow.Context, data *RegistrationData) (teleflow.FlowAction, error) {
        switch ctx.Text() {
        case "confirm":
            return teleflow.End(), nil
        case "restart":
            // Reset data and go back to the beginning
            *data = RegistrationData{}
            return teleflow.GoTo("Welcome"), nil
        default:
            ctx.ReplyText("Please use the buttons to confirm or restart.")
            return teleflow.Retry(), nil
        }
    },
}

๐Ÿ”ง Advanced Features

Inline-Button Routing

Inline-keyboard button presses arrive as CallbackQuery updates, not as commands. The Router only routes msg.IsCommand() messages, so inline buttons will not be dispatched by the router. Instead, buttons reach a Flow through one of two mechanisms:

  1. Default handler forwarding โ€” the pattern used by all examples:

    router.Default(teleflow.Func(func(ctx *teleflow.Context) error {
        if flow.IsUserInFlow(ctx.ChatID(), ctx.UserID()) {
            return flow.HandleUpdate(ctx)
        }
        return ctx.ReplyText("Unknown command. Use /start.")
    }))
    
  2. One-time callback handler โ€” via ctx.RegisterCallback(handler), which runs once and auto-removes.

Complex Inline Data Handling

Teleflow transparently handles complex data structures in callback buttons:

// Define structured callback data
type TransactionData struct {
    Action     string  `json:"action"`
    Amount     float64 `json:"amount"`
    AccountID  string  `json:"account_id"`
}

// In a flow step
step := teleflow.Step[MyData]{
    Name: "Transaction",
    Prompt: func(ctx *teleflow.Context, data *MyData) error {
        // Create complex callback data
        approveData := TransactionData{
            Action:    "approve",
            Amount:    1500.50,
            AccountID: "acc_12345",
        }

        keyboard := teleflow.NewInlineKeyboard().AddRow(
            teleflow.ButtonCallback("โœ… Approve \$1,500.50", ctx.RegisterData(approveData)),
        ).Build()

        return ctx.ReplyWithKeyboard("Review this transaction:", keyboard)
    },
    Process: func(ctx *teleflow.Context, data *MyData) (teleflow.FlowAction, error) {
        // Retrieve the complex data
        retrievedData, ok := ctx.GetData()
        if !ok {
            return teleflow.Retry(), nil
        }

        // Type assertion to get our structured data
        txData, ok := retrievedData.(TransactionData)
        if !ok {
            return teleflow.Retry(), nil
        }

        // Process based on the data
        switch txData.Action {
        case "approve":
            // Handle approval
            return teleflow.End(), nil
        }

        return teleflow.Retry(), nil
    },
}

Middleware Support

Teleflow supports middleware through the standard Handler interface:

// Auth middleware example
func AuthMiddleware(next teleflow.Handler) teleflow.Handler {
    return teleflow.Func(func(ctx *teleflow.Context) error {
        // Check if user is authorized
        if !isUserAuthorized(ctx.UserID()) {
            return ctx.ReplyText("You are not authorized to use this command.")
        }

        // Continue with the next handler
        return next.HandleUpdate(ctx)
    })
}

// Apply middleware when registering handlers
router.Handle("admin", AuthMiddleware(adminFlow))

๐Ÿ“ˆ Testing Strategies

// Test flow behavior
func TestUserRegistrationFlow(t *testing.T) {
    // Create test flow
    flow := teleflow.NewFlow[TestData]()

    // Add test steps
    if _, err := flow.AddStep(teleflow.Step[TestData]{
        Name: "TestStep",
        Prompt: func(ctx *teleflow.Context, data *TestData) error {
            return ctx.ReplyText("Test prompt")
        },
        Process: func(ctx *teleflow.Context, data *TestData) (teleflow.FlowAction, error) {
            data.Value = ctx.Text()
            return teleflow.End(), nil
        },
    }); err != nil {
        t.Fatal(err)
    }

    // Test flow execution
    // ... testing logic
}

๐Ÿ”ฅ Quick Reference

Flow Control

  • teleflow.Next() - Continue to next step
  • teleflow.Retry() - Re-prompt current step
  • teleflow.End() - Finish flow, trigger OnComplete
  • teleflow.GoTo("step_name") - Jump to specific named step

Message Actions

  • ctx.ReplyText(msg) - Simple text message
  • ctx.ReplyWithKeyboard(text, keyboard) - Message with inline keyboard
  • ctx.ReplyTextWithParseMode(text, parseMode) - Text message with formatting
  • ctx.ReplyWithKeyboardAndParseMode(text, keyboard, parseMode) - Message with keyboard and formatting
  • ctx.EditMessageText(messageID, text) - Edit existing message
  • ctx.EditMessageTextWithParseMode(messageID, text, parseMode) - Edit existing message with formatting
  • ctx.AnswerCallback() - Acknowledge callback query

Data Management

  • ctx.Set(key, value) - Store request-scoped data
  • ctx.Get(key) - Retrieve request data
  • ctx.RegisterData(data) - Register complex data for callbacks
  • ctx.GetData() - Retrieve complex callback data

This guide provides the foundation for building Telegram bots with Teleflow. The framework handles the complexity while giving you the power to build sophisticated conversational experiences.