# How to Create Dynamic Persona Transitions in AstrBot

> Create dynamic persona transitions in AstrBot easily. Write session rules to the async key-value store sp to override personas at runtime no restart needed.

- Repository: [AstrBot AI/AstrBot](https://github.com/AstrBotDevs/AstrBot)
- Tags: how-to-guide
- Published: 2026-03-12

---

**You can create dynamic persona transitions in AstrBot by writing session-level rules to the async key-value store `sp`, which overrides conversation-level and provider-default personas at runtime without restarting the bot.**

AstrBot's persona architecture separates static configuration from runtime selection logic, enabling mid-conversation personality switches based on triggers, content analysis, or scheduling. This guide explains how to implement dynamic persona transitions in the AstrBotDevs/AstrBot repository using session overrides, conversation management, and the resolution hierarchy defined in the core persona manager.

## Understanding AstrBot's Persona Resolution Hierarchy

AstrBot determines the active persona for each message through a four-tier resolution chain implemented in `PersonaManager.resolve_selected_persona` ([`astrbot/core/persona_mgr.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/persona_mgr.py), lines 64-112). The system evaluates these sources in strict priority order:

1. **Session-level overrides** – Rules stored in the async key-value store `sp` under the `umo` (user-message-origin) scope with the key `session_service_config`.
2. **Conversation-level persona** – The `persona_id` persisted in the current conversation record via `conversation_manager`.
3. **Provider defaults** – The `default_personality` setting defined in the provider configuration.
4. **WebChat special fallback** – The internal `_chatui_default_` persona injected automatically when the platform is "webchat" and no explicit match exists.

The `resolve_selected_persona` method returns a tuple containing the final `persona_id`, the `Personality` object (or `None`), the `force_applied_persona_id` from session rules, and a boolean `use_webchat_special_default` indicating whether the webchat fallback triggered.

## Implementing Session-Level Persona Overrides

Dynamic transitions rely on step one of the resolution hierarchy: session-level rules that temporarily override all other settings.

### How `resolve_selected_persona` Processes Overrides

When processing a message, the method executes this flow (lines 81-109 in [`persona_mgr.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/persona_mgr.py)):

- **Load session config**: `session_service_config = await sp.get_async(scope="umo", scope_id=str(umo), key="session_service_config", default={})`
- **Apply forced persona**: If `session_service_config` contains `persona_id`, that value becomes `force_applied_persona_id` and overrides the conversation setting.
- **Fallback chain**: If no session rule exists, the system checks `conversation_persona_id`, then provider `default_personality`.
- **WebChat handling**: If still unresolved and the platform is "webchat", the method sets `persona_id = "_chatui_default_"`.

Because session rules are stored in the async `sp` store rather than the conversation database, you can modify them instantly without persisting changes to the conversation record.

### Writing Dynamic Rules with `sp.set_async`

To trigger a transition programmatically, write a rule to the session scope:

```python
from astrbot.api import sp

async def set_temporary_persona(umo: str, persona_name: str):
    await sp.set_async(
        scope="umo",
        scope_id=umo,
        key="session_service_config",
        value={"persona_id": persona_name},
    )

```

The next message processed for that UMO will resolve to the specified persona. To revert, delete the key or overwrite it with a new value.

## Practical Trigger Mechanisms for Persona Switching

You can attach dynamic transitions to various triggers depending on your use case.

### Command-Based Switching

The built-in `/persona` command ([`astrbot/builtin_stars/builtin_commands/commands/persona.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/builtin_stars/builtin_commands/commands/persona.py), lines 95-100) updates the conversation-level persona:

```python
await self.context.conversation_manager.update_conversation_persona_id(
    message.unified_msg_origin,
    "detective",
)

```

This persists across the conversation but affects only the conversation level, not the session override layer.

### Event-Driven Automatic Switching

Create a plugin that listens to `AstrMessageEvent` and sets session rules based on message content:

```python
from astrbot.api import sp

async def on_message(event):
    msg = event.message_str.lower()
    if "tell me a joke" in msg:
        await sp.set_async(
            scope="umo",
            scope_id=str(event.unified_msg_origin),
            key="session_service_config",
            value={"persona_id": "jokester"},
        )
        await event.reply("Switching to the jokester persona…")

```

This switches personas immediately when specific keywords are detected, overriding the conversation default for subsequent turns.

### Time-Based and Scheduled Transitions

For time-sensitive personas (e.g., a "night" persona after 22:00), use a background task or cron job to write rules to `sp` for active sessions. Call `sp.set_async` with the appropriate `persona_id` for target UMOs based on your scheduling logic.

## Code Examples for Dynamic Transitions

### Creating a Temporary Session Rule

Use this pattern to switch personas for a single session without modifying the conversation record:

```python
from astrbot.api import sp

async def activate_night_mode(umo: str):
    await sp.set_async(
        scope="umo",
        scope_id=umo,
        key="session_service_config",
        value={"persona_id": "night_assistant"},
    )

```

### Clearing Rules to Revert Defaults

To remove the session override and fall back to the conversation-level or provider-default persona:

```python
await sp.delete_async(
    scope="umo",
    scope_id=umo,
    key="session_service_config",
)

```

### Creating Personas for Dynamic Use

Before you can transition to a persona, it must exist in the database. Create it programmatically:

```python
await context.persona_manager.create_persona(
    persona_id="jokester",
    system_prompt="You love telling jokes and witty remarks.",
    tools=None,
    skills=None,
)

```

*Source*: `PersonaManager.create_persona` in [`astrbot/core/persona_mgr.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/persona_mgr.py), lines 304-340.

### WebChat Automatic Fallback

No code is required for WebChat dynamic defaults. When the platform identifier is "webchat" and `resolve_selected_persona` finds no matching persona, it automatically injects `_chatui_default_` (lines 106-109 in [`persona_mgr.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/persona_mgr.py)).

## Key Source Files and Architecture

| File | Role |
|------|------|
| **[`astrbot/core/persona_mgr.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/persona_mgr.py)** | Contains `PersonaManager.resolve_selected_persona` (lines 64-112), the central resolution logic for all persona transitions, plus `create_persona` for personality definitions. |
| **[`astrbot/builtin_stars/builtin_commands/commands/persona.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/builtin_stars/builtin_commands/commands/persona.py)** | Implements the `/persona` slash command for conversation-level switching and persona listing. |
| **[`astrbot/api/sp.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/api/sp.py)** | Async key-value store interface for session-level overrides (`session_service_config`). |
| **[`astrbot/core/conversation_manager.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/conversation_manager.py)** | Persists conversation-level `persona_id` settings that serve as the fallback when no session rule exists. |
| **[`astrbot/core/platform/message_session.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/core/platform/message_session.py)** | Defines the UMO (user-message-origin) identifier used as the scope for session rules. |
| **[`astrbot/dashboard/routes/persona.py`](https://github.com/AstrBotDevs/AstrBot/blob/main/astrbot/dashboard/routes/persona.py)** | Web dashboard API for persona CRUD operations. |

## Summary

- **Session rules override everything**: Write to `sp.set_async(scope="umo", key="session_service_config")` to dynamically switch personas without touching conversation records.
- **Four-tier fallback**: Resolution proceeds from session overrides → conversation persona → provider default → WebChat special default.
- **Instant transitions**: Changes to the `sp` store take effect on the next message processed by `PersonaManager.resolve_selected_persona`.
- **No restart required**: Dynamic transitions operate entirely at runtime through the async store and resolution logic.
- **Clean revert**: Use `sp.delete_async` to remove session rules and automatically fall back to conversation or provider defaults.

## Frequently Asked Questions

### How do I temporarily switch personas for just one reply?

Write a session rule using `sp.set_async` before processing the message, then delete it immediately after the response using `sp.delete_async`. This creates a single-message persona switch that reverts automatically for subsequent turns.

### What's the difference between conversation-level and session-level personas?

Conversation-level personas are persisted in the conversation record via `conversation_manager.update_conversation_persona_id` and survive until explicitly changed. Session-level personas are stored in the ephemeral `sp` cache under `session_service_config` and override the conversation setting only for the current session, disappearing when deleted or when the session expires.

### Can I use dynamic personas with the WebChat interface?

Yes. The WebChat platform triggers a special fallback to `_chatui_default_` when no explicit persona matches. You can also force specific personas for WebChat sessions by writing UMO-scoped rules to `sp` using the WebChat session identifier, just as you would for any other platform.

### How do I check which persona is currently active in code?

Call `await context.persona_manager.resolve_selected_persona(...)` with the current conversation and provider settings. Inspect the returned tuple: the first element is the final `persona_id`, the third element indicates if a session rule forced the selection (`force_applied_persona_id`), and the fourth boolean shows if the WebChat default triggered.