Each example below was run to check exactly what it sends to Discord. A status needs no intent and no permission, and only people who share a server with the bot can see it.
What a bot can show
Two things: its status, one of online, idle, do not disturb or invisible, and one activity, the line under its name:
| People see | discord.py | discord.js |
|---|---|---|
| Playing name | discord.Game(name=...) | ActivityType.Playing (the default) |
| Listening to name | discord.Activity(type=discord.ActivityType.listening, name=...) | ActivityType.Listening |
| Watching name | discord.ActivityType.watching | ActivityType.Watching |
| Competing in name | discord.ActivityType.competing | ActivityType.Competing |
| Streaming name | discord.Streaming(name=..., url=...) | ActivityType.Streaming and a url |
| Your own text | discord.CustomActivity(name=...) | ActivityType.Custom |
A bot can set an activity's name, type, state and stream address, and nothing else: no buttons, pictures or elapsed-time counters, which belong to apps on people's own accounts. For the same reason a bot's custom status has no emoji. Streaming only works with a twitch.tv or youtube.com address.
Set it when the bot starts
The simplest way, and the one that lasts: give it to the client, and it goes to Discord with the login itself. In discord.py (discord.Client takes the same two arguments):
bot = commands.Bot(
command_prefix="!",
intents=discord.Intents.default(),
activity=discord.Activity(type=discord.ActivityType.listening, name="/help"),
status=discord.Status.online,
)In discord.js:
const { ActivityType, Client, Events, GatewayIntentBits } = require("discord.js");
const client = new Client({
intents: [GatewayIntentBits.Guilds],
presence: {
activities: [{ name: "/help", type: ActivityType.Listening }],
status: "online",
},
});Both show "Listening to /help". The statuses are online, idle, dnd and invisible; in discord.py, discord.Status.idle and so on. An invisible bot looks offline and still works.
Change it while it runs
In discord.py:
await bot.change_presence(activity=discord.Game(name="with slash commands"))
await bot.change_presence(
status=discord.Status.idle,
activity=discord.Activity(type=discord.ActivityType.watching, name="the server"),
)change_presence replaces both halves every time. Leave out activity and the activity is cleared; leave out status and the bot goes back to online. Pass both when you want to keep both.
In discord.js:
client.user.setActivity("the server", { type: ActivityType.Watching });
client.user.setStatus("idle");
client.user.setPresence({ activities: [{ name: "with slash commands" }], status: "dnd" });discord.js keeps whichever half you leave out: setStatus keeps the activity and setActivity keeps the status. None of the three returns a promise, so there is nothing to await. They need client.user, which is null until the bot has logged in, so call them in ClientReady or later.
A custom status
Text of your own, shown without "Playing" in front:
await bot.change_presence(activity=discord.CustomActivity(name="Answering /help"))client.user.setActivity("Answering /help", { type: ActivityType.Custom });Both libraries send your text as the activity's state, which is where Discord reads a custom status from.
A status that changes, like a server count
In discord.py, a task loop refreshes it every ten minutes:
import os
import discord
from discord.ext import commands, tasks
bot = commands.Bot(
command_prefix="!",
intents=discord.Intents.default(),
activity=discord.Activity(type=discord.ActivityType.listening, name="/help"),
)
async def set_status():
await bot.change_presence(
activity=discord.Activity(type=discord.ActivityType.watching, name=f"{len(bot.guilds)} servers")
)
@tasks.loop(minutes=10)
async def refresh_status():
await set_status()
@refresh_status.before_loop
async def before_refresh_status():
await bot.wait_until_ready()
@bot.event
async def setup_hook():
refresh_status.start()
@bot.event
async def on_ready():
await set_status()
bot.run(os.environ["BOT_TOKEN"])setup_hook runs once, before the bot connects, so the loop starts exactly once, and before_loop holds its first run until the bot is ready. Start a loop in on_ready instead and it fails the second time on_ready runs, with RuntimeError: Task is already launched and is not completed. The on_ready handler is explained further down.
In discord.js, an interval does the same:
const { ActivityType, Client, Events, GatewayIntentBits } = require("discord.js");
const client = new Client({
intents: [GatewayIntentBits.Guilds],
presence: { activities: [{ name: "/help", type: ActivityType.Listening }] },
});
function updateStatus() {
client.user.setActivity(`${client.guilds.cache.size} servers`, { type: ActivityType.Watching });
}
client.once(Events.ClientReady, () => {
updateStatus();
setInterval(updateStatus, 10 * 60 * 1000);
});
client.on(Events.ShardReady, () => {
if (client.isReady()) updateStatus();
});
client.login(process.env.BOT_TOKEN);To take turns between a few lines, pick one by a counter each time. In discord.py, in a loop started the same way:
STATUSES = ["Type /help to start", "In {servers} servers"]
@tasks.loop(minutes=5)
async def rotate_status():
text = STATUSES[rotate_status.current_loop % len(STATUSES)]
await bot.change_presence(activity=discord.CustomActivity(name=text.format(servers=len(bot.guilds))))In discord.js, called from the interval:
let turn = 0;
function rotateStatus() {
const texts = ["Type /help to start", `In ${client.guilds.cache.size} servers`];
client.user.setActivity(texts[turn++ % texts.length], { type: ActivityType.Custom });
}Why a status goes back on its own
Now and then Discord ends a bot's session, and it cannot always pick up where it left off. When it cannot, the library logs in again from scratch, and a fresh login carries the status the client was created with, not the one you set later. So a status set once at startup goes back to the constructor's, or to nothing, hours or days later, with no error.
The examples above deal with it. discord.py runs on_ready again after a fresh login, so setting the status there puts it back. discord.js fires ClientReady only once, but ShardReady after every login, which is why the discord.js example sets it there too (the isReady() check skips the first one, which ClientReady covers). A status that never changes is safest in the constructor, where nothing can undo it.
How often to change it
Every few minutes is plenty. Each change is a message on the bot's connection to Discord, which takes 120 messages a minute for everything the bot sends there, and nobody reads a status that changes every few seconds.
Common problems
TypeError: Cannot read properties of null (reading 'setActivity'): discord.js was asked before the bot had logged in. Move the call intoClientReady.- The activity vanished when the status changed: in discord.py,
change_presencewas called with onlystatus. Pass the activity too. RuntimeError: Task is already launched and is not completed.: a task loop started inon_ready. Start it insetup_hook.- It worked for a while, then went back: a fresh login, described above. Set it in the constructor, or again after every login.
- The bot looks offline but answers commands: its status is
invisible.
Did this page help?
Thanks for the feedback.