Tflows
A FlowBot-based scripting system that lets you define Discord bot behavior entirely through a concise, expressive DSL — no callback hell, no boilerplate.
What is Tflows?
Tflows is a lightweight Python library built on top of discord.py that replaces the traditional event-callback model with a compact domain-specific language (DSL). Instead of writing Python handler functions for every command, you pass a plain-text script to bot.command() and Tflows handles parsing, variable resolution, and Discord output at runtime.
It is designed for developers who want fast iteration — define a new command in seconds, iterate on its output without restarting the bot, and keep all logic in one readable place.
🐢 Traditional discord.py
- Per-command function callbacks
- Manual ctx.send() everywhere
- Boilerplate embed construction
- Verbose variable lookup code
- Restart required for every change
⚡ Tflows DSL
- Scripts passed as plain text strings
- Auto-routed text or embed output
- Inline embed directives
$variablesyntax, resolved at runtime- Hot-editable scripts, minimal surface
Key Features
Command Scripting
Define bot commands with a simple string-based DSL instead of callback functions.
Runtime Parsing
Scripts are parsed and executed at invocation time — no pre-compilation required.
Variable System
Rich built-in variables: $ping, $id, $image, $server, and more.
Embed DSL
Build rich Discord embeds with just a few directives — no Embed objects needed.
Mode Flags
Context-aware variables with mode flags for flexible output formatting.
Zero Config
Works out of the box. Just install, import FlowBot, and start scripting.
Who is it for?
Tflows is for Discord bot developers who want to ship command logic quickly — whether you're prototyping a bot, building a community tool, or just tired of writing the same boilerplate over and over. It's especially useful when you want non-technical collaborators to edit bot behavior without touching Python.
Tflows currently wraps discord.py. Python 3.8+ and a valid Discord bot token are required.
Installation #
Install Tflows from PyPI using pip. No additional dependencies need to be manually installed — discord.py is pulled in automatically.
$ pip install tflows
Verify the installation
import tflows
print(tflows.__version__)
Links
Quick Start #
Below is a minimal working bot. It registers a single !test command that replies with a message showing current latency.
from tflows import FlowBot
bot = FlowBot(prefix="!")
bot.command(
name="test",
code="""
send Hello World $ping
"""
)
bot.run("YOUR_BOT_TOKEN")
Trigger it in Discord with !test. The bot replies: Hello World 42ms (latency will vary).
Never hard-code your bot token in source files. Use environment variables or a .env file with python-dotenv.
A richer example with an embed
embed
$title[Server Status]
$desc[
Latency: $pingms
Server: $server
Members: $membercount
Time: $time(24h)
]
Architecture #
Tflows is a thin orchestration layer over discord.py. It replaces the event-callback model with a parse-and-execute pipeline that runs inside a command's context.
Components
FlowBot
Extends discord.py's bot client. Adds .command() for DSL registration and holds the command registry.
Command Registry
A dictionary mapping command names to their DSL code strings. Looked up on every invocation.
Script Parser
Reads the code string line-by-line, identifies directives (embed, send), and delegates each token.
Variable Resolver
Scans each line for $variable patterns and replaces them with live values from the Discord context.
Output Builder
Constructs either a plain string or a discord.Embed object depending on whether embed mode was activated.
discord.py
The underlying library handles gateway events, authentication, message delivery, and rate limits.
FlowBot #
FlowBot is the main entry point. It subclasses discord.py's commands.Bot and adds DSL-aware command registration.
from tflows import FlowBot
bot = FlowBot(prefix="!")
bot.command(
name="greet",
code="""
send Hi $id(user)!
"""
)
bot.run("TOKEN")
Constructor
| Parameter | Type | Description |
|---|---|---|
| prefix | str | The command prefix character(s), e.g. "!", "?", "..". |
bot.command() Parameters
| Parameter | Type | Description |
|---|---|---|
| name | str | The command trigger name. Users invoke it as {prefix}{name}. |
| code | str | A multi-line DSL script string. Parsed and executed at invocation time. |
Execution lifecycle
Command triggered
User types !{name} in a Discord channel with the correct prefix.
Registry lookup
FlowBot finds the matching command entry and retrieves the code string.
DSL parsing
The code is split into lines and each line is tokenised by the Script Parser.
Variable resolution
All $variable tokens are replaced with live values from the Discord context object.
Output dispatched
The Output Builder calls ctx.send() with a plain string or a constructed discord.Embed.
DSL Commands #
The DSL supports a small set of keywords that control output mode and content. Every script is evaluated line-by-line from top to bottom.
send
Sends a plain text message to the channel. Everything after the keyword is the message content.
send Hello World $ping
embed
Switches the output mode to embed. After this keyword, subsequent $title and $desc directives build up a discord.Embed object. No arguments required.
The embed is sent automatically when the script block finishes execution. You do not need a closing statement.
Embed directives
| Directive | Description |
|---|---|
| $title[TEXT] | Sets the embed title. Supports $ping and other variable interpolation. |
| $desc[TEXT] | Sets the embed description. Supports multiline content and all variable types. |
Available variables in directives
Embed DSL #
Embeds are Discord's rich message format — they support titles, descriptions, colors, and more. Tflows' embed mode lets you build them declaratively with zero Python embed object management.
embed
$title[Hello from Tflows]
$desc[
Ping: $pingms
Server: $server
Members: $membercount
]
How it works
Mode switch
The parser reads embed on its own line and activates embed mode for all subsequent lines.
Directive parsing
$title[…] and $desc[…] capture everything between the brackets as their value.
Variable interpolation
Values inside brackets are scanned for $variable tokens and resolved before the embed is built.
Auto-send
At end-of-script, the completed discord.Embed is sent via ctx.send(embed=...).
Supported fields
| Directive | Type | Notes |
|---|---|---|
| $title[TEXT] | string | Embed title. Variables supported inside brackets. |
| $desc[TEXT] | string | Embed description. Multiline content supported. Variables supported inside brackets. |
Variables #
Variables are special tokens embedded in your DSL scripts, prefixed with $. They are resolved at runtime against the live Discord command context and replaced with their current values before output is generated.
Variables come in two flavors: static (no arguments, always produce the same type of value) and function (accept an optional mode flag in parentheses).
$ping
staticReturns the bot's current WebSocket latency in milliseconds as a rounded integer string.
| Usage | Returns |
|---|---|
| $ping | Latency in ms, e.g. 42 |
send Pong! $pingms
$uptime
dynamicReturns how long the bot has been running since startup. Supports multiple formatting modes including full, short, clock, seconds, and custom token-based output.
| Usage | Returns |
|---|---|
| $uptime | Default format, e.g. 1h 23m 10s |
| $uptime(full) | Full format including days, e.g. 0d 1h 23m 10s |
| $uptime(short) | Compact format, e.g. 1h 23m |
| $uptime(clock) | Clock-style format, e.g. 01:23:10 |
| $uptime(seconds) | Raw uptime in seconds, e.g. 4990 |
| $uptime(d, h, m, s) | Custom formatted output using tokens |
embed
$title[Uptime Variants]
$desc[
Default: $uptime
Full: $uptime(full)
Short: $uptime(short)
Clock: $uptime(clock)
Seconds: $uptime(seconds)
Custom: $uptime(d, h, m, s)
]
$log
functionSends a message to a specific Discord channel for tracking command usage, user actions, and bot events. Unlike console logging, this outputs directly inside a server channel.
| Parameter | Description |
|---|---|
| CHANNEL_ID | Target Discord channel ID where the log message will be sent |
| MESSAGE | Content of the log message |
$log(CHANNEL_ID){User executed a command}
Behavior
- Sends a message directly to the specified Discord channel
- Executes when the command runs successfully (unless wrapped in error handling logic)
- Supports variables like
$id,$time, and other engine variables - Does not print to console, only sends to Discord channels
Examples
$log(123456789012345678){User executed !ping command}
$log(123456789012345678){$user used a command at $time}
$log(987654321098765432){$user was muted by <@$id> for spam}
Notes
- Bot must have permission to send messages in the target channel
- If the channel ID is invalid, logging may silently fail or throw an error depending on configuration
- You can use
<@$id>to mention the command user since$idreturns raw ID
$image
functionReturns a Discord CDN avatar image URL. Accepts an optional mode flag to control whose avatar is returned.
| Mode | Description |
|---|---|
| $image default | Command author's avatar URL |
| $image(user) | Same as default — author's avatar |
| $image(mention) | First mentioned user's avatar. Falls back to author if no mention. |
| $image(act) | Mentioned user's avatar if a mention is present, otherwise author's. |
$image
$image(user)
$image(mention)
$image(act)
$server
functionReturns information about the current Discord guild (server). Defaults to returning the server name.
| Mode | Description |
|---|---|
| $server default | Guild name |
| $server(name) | Explicitly returns the guild name |
| $server(boost) | Current number of Nitro boosts |
| $server(boostlvl) | Current boost tier level (0, 1, 2, or 3) |
$server
$server(name)
$server(boost)
$server(boostlvl)
$membercount
functionReturns the guild member count. Can be filtered to include all members, only humans, or only bots.
| Mode | Description |
|---|---|
| $membercount default | Total member count (humans + bots) |
| $membercount(all) | Same as default |
| $membercount(user) | Count of non-bot members only |
| $membercount(bots) | Count of bot accounts only |
$membercount
$membercount(all)
$membercount(user)
$membercount(bots)
$time
functionOutputs the current date and/or time. Accepts flags to control format; multiple flags can be combined with a semicolon delimiter.
| Mode | Description |
|---|---|
| $time() default | Full date + time in the default format |
| $time(12h) | 12-hour clock format (AM/PM) |
| $time(24h) | 24-hour clock format |
| $time(nodate) | Time only — date portion omitted |
| $time(notime) | Date only — time portion omitted |
| $time(nodate;24h) | Combine flags with ; for composed output |
$time()
$time(12h)
$time(24h)
$time(nodate)
$time(notime)
$time(nodate;24h)
$id
functionReturns a Discord user ID as a string. Accepts an optional mode to select which user's ID to return.
| Mode | Description |
|---|---|
| $id default | Command author's Discord user ID |
| $id(user) | Same as default |
| $id(mention) | First mentioned user's ID. Falls back to author if no mention. |
| $id(act) | Mentioned user's ID if present, otherwise author's ID. |
$id
$id(user)
$id(mention)
$id(act)
Execution Flow #
Understanding exactly what happens when a user invokes a Tflows command helps you write more predictable scripts and debug issues faster.
Discord gateway event
A user sends a message in a channel your bot can read. The discord.py gateway fires an on_message event containing the full message object and context.
Prefix check
FlowBot checks whether the message starts with the configured prefix. If not, it's ignored entirely.
Registry lookup
The command name (the word after the prefix) is looked up in the registry dictionary. If not found, the invocation silently fails.
Script handed to parser
The stored code string is split by newlines. Empty lines are skipped. Each non-empty line is processed in order.
Mode detection
If a line is the literal token embed, the parser switches into embed mode. All subsequent directive lines feed into the embed builder.
Variable resolution
Each line is scanned with a regex for $identifier patterns. Matched tokens are replaced by calling the corresponding resolver with the live Discord context.
Output dispatch
In text mode: the resolved line content is sent via ctx.send(content). In embed mode: a discord.Embed is assembled from the resolved directives and sent via ctx.send(embed=...).
Scripts are re-parsed on every invocation. This means you can update the code string at runtime and the change takes effect on the very next command call — without restarting the bot.
Best Practices #
Following these patterns will make your Tflows scripts more readable, maintainable, and robust.
One command, one purpose
Keep each command focused. A !status command should show server stats; don't mix in unrelated data. Lean scripts are easier to debug and iterate on.
Never expose your bot token
Store tokens in environment variables or a .env file loaded with python-dotenv. Use os.environ.get("BOT_TOKEN") instead of string literals.
Use embeds for information-dense output
If you're displaying more than two or three pieces of data, switch to embed mode. Embeds are visually structured and easier for users to scan.
Prefer act mode for flexible mention handling
Using $id(act) and $image(act) means your command works gracefully whether or not the user mentions someone — sensible fallback built-in.
Test variables individually
Build a !debug command that dumps all variables you need. Verify their output before composing them into production commands.
Keep script strings in separate files for large bots
For bots with many commands, store DSL scripts as .txt or .tflow files and load them with open().read(). This keeps your main Python file clean.
Common Mistakes #
These are the most frequent errors new Tflows users encounter, and how to fix them.
Forgetting the embed keyword
Using $title or $desc without first writing embed on its own line will not produce an embed — the directives will either error or be ignored.
# ❌ embed mode never activated
$title[My Title]
$desc[My description]
# ✅ embed mode activated first
embed
$title[My Title]
$desc[My description]
Wrong bracket syntax for directives
Directive values must use square brackets […]. Parentheses and curly braces will not be parsed correctly.
$title(Wrong brackets) # ❌ parentheses
$desc{Also wrong} # ❌ curly braces
$title[Correct] # ✅ square brackets
Incorrect mode flag names
Mode flags are case-sensitive and must match the documented identifiers exactly. Check the Variables reference for the exact spelling.
$membercount(users) # ❌ should be "user"
$image(Mention) # ❌ should be lowercase "mention"
All variable mode flags are lowercase. $time(24H) will not work; use $time(24h).
Using mention without an actual mention
$id(mention) and $image(mention) fall back to the author if no mention is present — but the fallback exists to prevent crashes, not to produce the expected output. If you need flexible mention handling, use act mode instead and document the expected usage.
Examples #
Server info command
bot.command(
name="server",
code="""
embed
$title[$server Info]
$desc[
Members: $membercount
Humans: $membercount(user)
Bots: $membercount(bots)
Boost Level: $server(boostlvl)
Boosts: $server(boost)
]
"""
)
Ping + time status command
bot.command(
name="status",
code="""
embed
$title[Bot Status]
$desc[
Latency: $pingms
Time: $time(nodate;24h)
Date: $time(notime)
]
"""
)
Avatar + ID lookup
bot.command(
name="whois",
code="""
embed
$title[User Info]
$desc[
ID: $id(act)
Avatar: $image(act)
]
"""
)
Simple plain-text output
send Pong! $pingms — $time(nodate;24h)
Complete time system demo
bot.command(
name="time",
code="""
embed
$title[Tflows Time Demo]
$desc[
Default: $time()
No Date: $time(nodate)
No Time: $time(notime)
24h: $time(24h)
12h: $time(12h)
24h No Date: $time(nodate;24h)
]
"""
)