# Telego Predicate-Based Routing: A Complete Guide to Telegram Bot Update Handling

> Master Telego predicate-based routing for efficient Telegram bot update handling. Learn to filter updates with boolean predicates and logical operators for composable control.

- Repository: [Artem Yadelskyi/telego](https://github.com/mymmrac/telego)
- Tags: deep-dive
- Published: 2026-03-07

---

**Telego routes incoming Telegram updates to handlers using boolean predicate functions that inspect update contents, enabling composable filtering through logical operators like `And`, `Or`, and `Not` instead of manual conditional statements.**

Telego is a comprehensive Go library for building Telegram bots. At the heart of its `telegohandler` package lies a **predicate-based routing** system that decouples update filtering from business logic. This architecture allows developers to construct complex routing trees using simple, reusable boolean functions rather than writing nested `if` statements.

## Core Concepts of Telego Predicate-Based Routing

The routing mechanism rests on three fundamental structures defined in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go) and [`telegohandler/predicates.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/predicates.go).

### The Predicate Type

A **Predicate** is a first-class function that evaluates whether an update matches specific criteria:

```go
// telegohandler/predicates.go
type Predicate func(context.Context, telego.Update) bool

```

Predicates receive the update context and the update itself, returning `true` only if the update should be routed to the associated handler.

### Route Structure

The unexported `route` struct encapsulates a single routing entry:

```go
// telegohandler/handler_group.go#L18-L31
type route struct {
    predicates []Predicate
    handler    Handler
    group      *HandlerGroup
}

```

Each route maintains a slice of predicates that must **all** evaluate to `true` for the route to match. Routes can terminate in a handler function, a middleware chain, or a nested sub-group.

### HandlerGroup Tree

`HandlerGroup` forms a hierarchical tree structure:

```go
// telegohandler/handler_group.go
type HandlerGroup struct {
    parent *HandlerGroup
    routes []route
    // ... other fields
}

```

The root group receives all updates and traverses its routes recursively. Child groups created via `Group()` inherit the parent's context but maintain their own predicate logic.

## How Route Matching Works

When `BotHandler` receives an update, it delegates to `HandlerGroup.HandleUpdate`, which creates a `Context` and invokes `Context.Next`.

### The Routing Algorithm

The `Next` method implements a depth-first traversal with backtracking:

```go
// telegohandler/context.go#L10-L41
func (c *Context) Next(update telego.Update) error {
    // iterate over the current group's routes
    for i := c.stack[len(c.stack)-1] + 1; i < len(c.group.routes); i++ {
        r := c.group.routes[i]
        if r.match(c.ctx, update) {          // ← predicate check
            c.stack[len(c.stack)-1] = i

            if r.handler != nil {            // handler or middleware
                return r.handler(c, update)
            }
            // sub-group: descend recursively
            c.group = r.group
            c.stack = append(c.stack, -1)
            return c.Next(update)
        }
    }
    // back-track to parent groups when nothing matches
    if c.group.parent != nil && c.group != c.finalGroup {
        c.group = c.group.parent
        c.stack = c.stack[:len(c.stack)-1]
        return c.Next(update)
    }
    return nil
}

```

### Predicate Evaluation

The `route.match` method evaluates predicates against a cloned update to prevent mutation:

```go
// telegohandler/handler_group.go#L18-L31
func (r route) match(ctx context.Context, update telego.Update) bool {
    if len(r.predicates) == 0 {
        return true // no predicates → always match
    }
    // work on a copy so predicates cannot mutate the original update
    update = update.Clone()
    for _, p := range r.predicates {
        if !p(ctx, update) {
            return false
        }
    }
    return true
}

```

If **all** predicates return `true`, the route matches and the handler executes.

## Built-in Predicates

The [`telegohandler/predicates.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/predicates.go) file provides over 70 ready-to-use predicates covering every Telegram update field:

| Predicate | Description | Signature Reference |
|-----------|-------------|---------------------|
| `Any()` | Always returns `true` | `predicates.go#L11-L15` |
| `None()` | Always returns `false` | `predicates.go#L18-L22` |
| `And(p1, p2, …)` | Logical **AND** of predicates | `predicates.go#L25-L34` |
| `Or(p1, p2, …)` | Logical **OR** of predicates | `predicates.go#L37-L46` |
| `Not(p)` | Logical **NOT** of a predicate | `predicates.go#L49-L54` |
| `AnyMessage()` | Update contains a non-nil `Message` | `predicates.go#L60-L64` |
| `AnyMessageWithText()` | Message present with non-empty `Text` | `predicates.go#L71-L75` |
| `CommandEqual("start")` | Message is command `/start` (case-insensitive) | `predicates.go#L25-L40` |
| `TextContains("hello")` | Message text contains substring | `predicates.go#L44-L48` |
| `CallbackDataPrefix("pay_")` | Callback query data starts with prefix | `predicates.go#L30-L34` |

All predicates compose naturally with `And`, `Or`, and `Not`, enabling complex filtering without nested conditionals.

## Registering Routes and Middleware

The `HandlerGroup` API in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go) provides three primary registration methods.

### Handle

Registers a handler function with optional predicates:

```go
// telegohandler/handler_group.go#L82-L100
func (h *HandlerGroup) Handle(handler Handler, predicates ...Predicate) {
    h.routes = append(h.routes, route{
        predicates: predicates,
        handler:    handler,
    })
}

```

### Group

Creates a nested sub-group that inherits predicates:

```go
// telegohandler/handler_group.go#L101-L121
func (h *HandlerGroup) Group(predicates ...Predicate) *HandlerGroup {
    group := &HandlerGroup{parent: h}
    h.routes = append(h.routes, route{
        predicates: predicates,
        group:      group,
    })
    return group
}

```

### Use

Registers middleware that executes before handlers:

```go
// telegohandler/handler_group.go#L123-L141
func (h *HandlerGroup) Use(middlewares ...Handler) {
    for _, m := range middlewares {
        h.routes = append(h.routes, route{handler: m})
    }
}

```

## Practical Example: Command Echo Bot

This complete example demonstrates **Telego predicate-based routing** with middleware, grouped routes, and command filtering:

```go
package main

import (
    "context"
    "log"

    "github.com/mymmrac/telego"
    "github.com/mymmrac/telego/telegohandler"
)

func main() {
    // Initialise bot (token omitted for security)
    bot, err := telego.NewBot("YOUR_BOT_TOKEN")
    if err != nil {
        log.Fatal(err)
    }

    // Create a handler group
    h := telegohandler.NewBotHandler(bot)

    // Middleware that logs every incoming update
    h.Use(func(ctx *telegohandler.Context, upd telego.Update) error {
        log.Printf("update %d from chat %d", upd.UpdateID, upd.Message.Chat.ID)
        return ctx.Next(upd) // continue routing
    })

    // Echo only text messages that start with "/say"
    h.Group(
        telegohandler.CommandPrefix("/say"), // matches /say, /sayHello, etc.
        telegohandler.AnyMessageWithText(), // ensure we have text
    ).HandleMessage(func(ctx *telegohandler.Context, msg telego.Message) error {
        // Echo the text after the command
        reply := telego.NewMessage(msg.Chat.ID, msg.Text[len("/say"):])
        _, err := bot.SendMessage(reply)
        return err
    })

    // Start long-polling
    err = bot.StartLongPolling(context.Background())
    if err != nil {
        log.Fatal(err)
    }
}

```

**Key implementation details:**

- `CommandPrefix("/say")` and `AnyMessageWithText()` combine implicitly through the `Group` method, creating a predicate slice that requires both conditions.
- The middleware calls `ctx.Next(upd)` to pass control to the next matching route, enabling the middleware chain pattern.
- `HandleMessage` registers a final handler that executes only when all grouped predicates return `true`.

## Creating Custom Predicates

When built-in predicates don't cover specific business logic, implement the `Predicate` type directly:

```go
func FromUser(username string) telegohandler.Predicate {
    return func(_ context.Context, upd telego.Update) bool {
        return upd.Message != nil && upd.Message.From != nil &&
               strings.EqualFold(upd.Message.From.Username, username)
    }
}

```

Usage follows the same pattern as library predicates:

```go
h.Group(telegohandler.AnyMessage(), FromUser("alice")).HandleMessage(myHandler)

```

This composability enables sophisticated routing trees—such as admin-only commands or feature-flagged responses—without modifying the core handler logic.

## Summary

- **Predicate-based routing** in Telego uses boolean functions (`func(context.Context, telego.Update) bool`) to filter incoming updates, defined in [`telegohandler/predicates.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/predicates.go).
- The `route` struct in [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go) aggregates predicates and associates them with handlers, middleware, or sub-groups.
- `Context.Next` in [`telegohandler/context.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/context.go) implements a depth-first traversal with backtracking, evaluating all predicates in a route via `route.match` before executing handlers.
- Built-in predicates cover message types, commands, text content, callback queries, and logical operations (`And`, `Or`, `Not`), all composable through the `HandlerGroup` API.
- Custom predicates implement the same function signature, enabling domain-specific routing logic while maintaining type safety.

## Frequently Asked Questions

### How does Telego handle multiple predicates in a single route?

When multiple predicates are passed to `Handle()` or `Group()`, Telego evaluates them as a logical **AND** operation. In [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go), the `route.match` method iterates through all predicates in the slice and returns `false` immediately if any predicate returns `false`. The update is cloned before evaluation to prevent mutation by individual predicates.

### What is the difference between Handle and Group in Telego?

`Handle` registers a terminal handler function that executes when all predicates match, storing it in a `route` struct with the handler field set. `Group` creates a nested `HandlerGroup` that can contain its own routes, middleware, and sub-groups, enabling hierarchical routing trees. Groups are useful for applying common predicates (like admin checks) to multiple handlers without repetition.

### How does Telego predicate-based routing compare to regex-based routing?

Telego predicates are **type-safe boolean functions** that inspect struct fields directly, offering compile-time safety and better performance than string parsing. Regex-based routing typically matches against raw update text or JSON strings, which is flexible but error-prone and slower for complex conditions. Telego's approach allows logical composition (`And`, `Or`, `Not`) and direct access to Telegram API types like `Message`, `CallbackQuery`, and `Chat`.

### Can predicates modify the update during routing?

No, predicates receive a **clone** of the update, not the original. In [`telegohandler/handler_group.go`](https://github.com/mymmrac/telego/blob/main/telegohandler/handler_group.go), the `route.match` method calls `update.Clone()` before iterating through predicates. This design prevents side effects where one predicate might mutate the update (e.g., trimming text) and affect subsequent predicates or the final handler. The original update remains immutable during the routing phase.