Skip to content

Tutorial: Command Automation

Build a command-driven automation that stays maintainable as command count grows.

Outcome

At the end of this tutorial you will have: - clean command parser - command dispatch table - production-safe error handling and idempotency guard

Level 1: Basic Command Router

import asyncio

from tryx.backend import SqliteBackend
from tryx.client import Tryx, TryxClient
from tryx.events import EvMessage

backend = SqliteBackend("whatsapp.db")
app = Tryx(backend)


def normalize(text: str | None) -> str:
    return (text or "").strip().lower()


@app.on(EvMessage)
async def on_message(client: TryxClient, event: EvMessage) -> None:
    text = normalize(event.data.get_text())
    chat = event.data.message_info.source.chat

    if text == "ping":
        await client.send_text(chat, "pong", quoted=event)
    elif text == "help":
        await client.send_text(chat, "commands: ping, help", quoted=event)


asyncio.run(app.run())

Level 2: Table-driven Commands

from collections.abc import Awaitable, Callable

CommandHandler = Callable[[TryxClient, EvMessage, list[str]], Awaitable[None]]


async def cmd_ping(client: TryxClient, event: EvMessage, args: list[str]) -> None:
    chat = event.data.message_info.source.chat
    await client.send_text(chat, "pong", quoted=event)


async def cmd_echo(client: TryxClient, event: EvMessage, args: list[str]) -> None:
    chat = event.data.message_info.source.chat
    await client.send_text(chat, " ".join(args) or "(empty)", quoted=event)


COMMANDS: dict[str, CommandHandler] = {
    "ping": cmd_ping,
    "echo": cmd_echo,
}


@app.on(EvMessage)
async def on_command(client: TryxClient, event: EvMessage) -> None:
    text = (event.data.get_text() or "").strip()
    if not text.startswith("/"):
        return

    parts = text[1:].split()
    name, args = parts[0].lower(), parts[1:]
    fn = COMMANDS.get(name)
    if fn is None:
        await client.send_text(event.data.message_info.source.chat, f"Unknown command: {name}")
        return
    await fn(client, event, args)

Level 3: Production Pattern

  • Add per-message idempotency key from event.data.message_info.id.
  • Separate command parsing from side effects.
  • Add structured logging (command, chat, message_id).
  • Use allowlist for admin-only commands.
  • Validate argument length/type before action.
  • Wrap outbound mutations with retry policy for transient failures.
  • Send client.chatstate.send_composing(chat) during slow command execution.
  • Return explicit error message for invalid command arguments.

Production Example: Idempotent Dispatch

seen_ids: set[str] = set()


@app.on(EvMessage)
async def on_idempotent(client: TryxClient, event: EvMessage) -> None:
    message_id = event.data.message_info.id
    if message_id in seen_ids:
        return
    seen_ids.add(message_id)

    # dispatch command here
Advanced: plugin command registry

For larger bots, store command handlers in module-level plugins and register them into a central command registry at startup. This keeps each command domain isolated and testable.

Memory growth

If you store processed IDs in memory, add TTL eviction or persist compact dedupe state.

Where To Go Next