Set your Discord bot's status: Playing, Watching and custom

A bot's status is the dot by its name (online, idle, do not disturb) and the line under it: Playing, Watching, or text of your own. Here is how to set both in discord.py and discord.js, keep a changing one up to date, and stop it quietly going back.

6 min readIntermediateUpdated Examples tested with discord.py 2.7.1 and discord.js 14.27

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 seediscord.pydiscord.js
Playing namediscord.Game(name=...)ActivityType.Playing (the default)
Listening to namediscord.Activity(type=discord.ActivityType.listening, name=...)ActivityType.Listening
Watching namediscord.ActivityType.watchingActivityType.Watching
Competing in namediscord.ActivityType.competingActivityType.Competing
Streaming namediscord.Streaming(name=..., url=...)ActivityType.Streaming and a url
Your own textdiscord.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):

Python
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:

JavaScript
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:

Python
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:

JavaScript
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:

Python
await bot.change_presence(activity=discord.CustomActivity(name="Answering /help"))
JavaScript
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:

Python
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:

JavaScript
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:

Python
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:

JavaScript
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 into ClientReady.
  • The activity vanished when the status changed: in discord.py, change_presence was called with only status. Pass the activity too.
  • RuntimeError: Task is already launched and is not completed.: a task loop started in on_ready. Start it in setup_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.

Press / or Ctrl K on any page to search.