# HoshinoBot Trigger Types: A Complete Guide to Prefix, Suffix, Keyword, and Regex

> Explore HoshinoBot trigger types: prefix, suffix, keyword, and regex. Learn how to match messages with leading text, trailing text, substrings, or regex patterns for powerful bot automation.

- Repository: [ice9coffee/hoshinobot](https://github.com/ice9coffee/hoshinobot)
- Tags: deep-dive
- Published: 2026-03-03

---

**HoshinoBot provides four core trigger classes—PrefixTrigger, SuffixTrigger, KeywordTrigger, and RexTrigger—that match incoming messages based on leading text, trailing text, substring containment, and regular expression patterns respectively.**

HoshinoBot, an open-source QQ bot framework maintained in the `ice9coffee/hoshinobot` repository, routes messages to service functions through a flexible trigger system defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py). Developers register handlers using decorators exposed by the `Service` class in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py), allowing precise matching of user commands and natural language input using four distinct matcher types.

## The Four Core Trigger Classes

HoshinoBot implements four lightweight matcher classes that form the foundation of its message routing system. Each class specializes in a specific text-matching strategy and exposes a corresponding decorator method on the `Service` class.

### PrefixTrigger (on_prefix)

The **PrefixTrigger** matches when a message segment begins with a specified string. According to the source code in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 26–65), this trigger stores all registered prefixes in a trie data structure for O(1) lookup efficiency. When a message arrives, the framework extracts the longest matching prefix and routes to the associated handler.

Register a prefix trigger using the `@sv.on_prefix()` decorator:

```python
from hoshino import Service

sv = Service('example')

@sv.on_prefix('help', 'h')
async def show_help(bot, ev):
    await bot.send(ev, 'Available commands: ...')

```

The decorator implementation resides in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py) (lines 200–213), which instantiates `PrefixTrigger` objects and adds them to the global trigger chain.

### SuffixTrigger (on_suffix)

The **SuffixTrigger** matches when a message ends with a specific string. As implemented in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 67–106), this trigger reverses the input text and registered suffixes, then performs a trie lookup identical to the prefix mechanism—effectively creating a reversed trie for efficient suffix matching.

Use the `@sv.on_suffix()` decorator (defined in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py), lines 444–456) to register suffix handlers:

```python
@sv.on_suffix('!', '？')
async def handle_excited(bot, ev):
    await bot.send(ev, 'That sounds exciting!')

```

### KeywordTrigger (on_keyword)

The **KeywordTrigger** performs substring containment matching, activating when the specified keyword appears anywhere in the message text. Located in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 108–133), this trigger checks all registered keywords against either the raw message or a normalized version depending on the `normalize` parameter.

The `@sv.on_keyword()` decorator (lines 558–572 in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py)) supports optional text normalization:

```python
@sv.on_keyword('weather', '天气', normalize=True)
async def check_weather(bot, ev):
    await bot.send(ev, 'Current conditions: Sunny')

```

When `normalize=True`, the trigger uses `hoshino.util.normalize_str` to convert full-width characters to half-width, fold case to lowercase, and remove punctuation before matching.

### RexTrigger (on_rex)

The **RexTrigger** provides full regular expression matching capabilities. Defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 132–150), this trigger iterates through registered compiled regex patterns and executes `search()` against the message text. Like keyword triggers, it supports the `normalize` parameter for case-insensitive and punctuation-insensitive matching.

Register regex triggers via `@sv.on_rex()` (lines 724–735 in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py)):

```python
@sv.on_rex(r'(\d{4})-(\d{2})-(\d{2})', normalize=True)
async def parse_date(bot, ev):
    match = ev['match']  # Access the MatchObject

    year, month, day = match.groups()
    await bot.send(ev, f'Date parsed: {year}/{month}/{day}')

```

When a regex matches, the `MatchObject` is injected into the event dictionary under the key `'match'`, accessible within the handler.

## How the Trigger Chain Processes Messages

HoshinoBot evaluates triggers in a strict priority sequence defined at the bottom of [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 64–76). When a message arrives, the framework constructs a trigger chain that executes in the following order:

1. **prefix** – Longest matching prefix lookup via trie
2. **suffix** – Longest matching suffix lookup via reversed trie
3. **_TextNormalizer** – Extracts plain text and creates `event.norm_text` if normalization is required
4. **rex** – Iterates through all registered regex patterns
5. **keyword** – Checks all keywords against plain or normalized text

Only the **first trigger** that yields a matching `ServiceFunc` executes; subsequent triggers in the chain are skipped for that message. This short-circuit behavior ensures that prefix commands take precedence over keyword matches, while regex patterns are evaluated before keyword containment checks.

## Implementing Triggers in Service Modules

The `Service` class in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py) provides decorator methods that abstract trigger registration. Each decorator wraps the handler function in a `ServiceFunc` object (defined in lines 61–71) and registers it with the appropriate trigger class.

### Basic Registration Syntax

All four trigger types follow a consistent decorator pattern:

```python
from hoshino import Service

sv = Service('my_service')

# Prefix trigger

@sv.on_prefix('cmd')
async def handle_cmd(bot, ev):
    pass

# Suffix trigger

@sv.on_suffix('!')
async def handle_suffix(bot, ev):
    pass

# Keyword trigger

@sv.on_keyword('trigger_word')
async def handle_keyword(bot, ev):
    pass

# Regex trigger

@sv.on_rex(r'pattern')
async def handle_regex(bot, ev):
    pass

```

### Combining Prefix and Suffix Triggers

Decorators are independent; applying multiple triggers to one function requires both conditions to be met separately (the framework checks each trigger individually):

```python
@sv.on_prefix('calc')
@sv.on_suffix('=')
async def calculate(bot, ev):
    # Handles messages like "calc 2+2 ="

    text = ev.message.extract_plain_text()
    expr = text.strip().removeprefix('calc').removesuffix('=').strip()
    try:
        result = eval(expr)
        await bot.send(ev, f'Result: {result}')
    except:
        await bot.send(ev, 'Invalid expression')

```

### Keyword Matching with Text Normalization

The `normalize` parameter enables fuzzy matching by preprocessing text through `hoshino.util.normalize_str`. This converts full-width alphanumeric characters to half-width, transforms Chinese punctuation to ASCII equivalents, and applies Unicode case folding:

```python
@sv.on_keyword('hello', '你好', normalize=True)
async def greet(bot, ev):
    # Matches "Hello!", "HELLO", "ｈｅｌｌｏ", etc.

    await bot.send(ev, 'Hi there!')

```

### Regex Pattern Matching with Capture Groups

Regex triggers provide the most flexibility, supporting pattern extraction via capture groups. The matched groups are accessible through `ev['match'].groups()`:

```python
@sv.on_rex(r'(\d+)\s*\+\s*(\d+)', normalize=True)
async def add_numbers(bot, ev):
    a, b = map(int, ev['match'].groups())
    await bot.send(ev, f'{a} + {b} = {a + b}')

```

## Summary

- **PrefixTrigger** matches the beginning of messages using a trie structure for efficient O(1) lookups; registered via `@sv.on_prefix()`.
- **SuffixTrigger** matches message endings by reversing text and using the same trie mechanism; registered via `@sv.on_suffix()`.
- **KeywordTrigger** performs substring containment checks with optional Unicode normalization; registered via `@sv.on_keyword()`.
- **RexTrigger** executes regular expression searches with full capture group support; registered via `@sv.on_rex()`.
- The trigger chain executes in fixed priority: prefix → suffix → text normalizer → regex → keyword, with only the first match firing.
- All trigger decorators are implemented in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py) while the matching logic resides in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py).

## Frequently Asked Questions

### What is the execution priority of HoshinoBot trigger types?

HoshinoBot processes triggers in a fixed chain order defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py): prefix triggers execute first, followed by suffix triggers, then the text normalizer (if needed), then regex triggers, and finally keyword triggers. Only the first trigger that finds a match executes its handler; subsequent triggers in the chain are ignored for that message.

### How does text normalization work with keyword and regex triggers?

When `normalize=True` is passed to `@sv.on_keyword()` or `@sv.on_rex()`, the trigger preprocesses the message using `hoshino.util.normalize_str`. This function converts full-width characters to half-width, replaces Chinese punctuation with ASCII equivalents, applies Unicode case folding (lowercasing), and removes extra whitespace. The normalized text is stored in `event.norm_text` while the original remains in `event.message`.

### Can I apply multiple triggers to a single handler function?

Yes. You can stack decorators such as `@sv.on_prefix('cmd')` and `@sv.on_suffix('!')` on the same async function. Each decorator registers the function independently with its respective trigger class. The order of decorators does not affect registration, though the handler will be invoked if **either** trigger condition is met (not both simultaneously).

### Where are the trigger classes and decorators defined in the source code?

The four trigger classes (`PrefixTrigger`, `SuffixTrigger`, `KeywordTrigger`, `RexTrigger`) are defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 26–150). The decorator methods (`on_prefix`, `on_suffix`, `on_keyword`, `on_rex`) that register handlers with these triggers are implemented in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py) (lines 200–735). The global trigger chain that orchestrates execution order is constructed at the bottom of [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py) (lines 64–76).