<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ bot - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ bot - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 24 Oct 2026 18:35:42 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/bot/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Basic Discord Storytelling, Chat, and Mental Wellness Bot with Python ]]>
                </title>
                <description>
                    <![CDATA[ Discord bots can look surprisingly complicated when you see them in action. A bot can respond to messages, tell stories, remember parts of conversations, and stay online around the clock. When I first ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-basic-discord-bot-with-python/</link>
                <guid isPermaLink="false">6a7f44fc58366ecdaf016624</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python 3 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ techblog ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eva J Patel ]]>
                </dc:creator>
                <pubDate>Fri, 14 Aug 2026 16:40:28 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6a444c51-d332-4915-aa1f-326b57b17472.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Discord bots can look surprisingly complicated when you see them in action. A bot can respond to messages, tell stories, remember parts of conversations, and stay online around the clock.</p>
<p>When I first started looking into how they worked, I assumed there had to be a huge amount of complicated code behind all of it.</p>
<p>But the basic idea is actually pretty simple.</p>
<p>At its core, a Discord bot is just a Python program that connects to Discord, waits for something to happen, and then decides how to respond. Once you understand that basic idea, you can start adding features one at a time and turn a simple bot into something much more interesting.</p>
<p>In this tutorial, we'll start with a very small bot and gradually build it into something more capable. Along the way, you'll learn about Discord commands, events, asynchronous Python, user state, environment variables, and basic deployment.</p>
<p>One quick disclaimer before we start: the mental-wellness feature that we'll be integrating in this bot in this project is <strong>not therapy</strong>, and the bot is not a therapist or medical professional. It should only provide general supportive suggestions and encourage users to reach out to a trusted person when appropriate.</p>
<p>With that out of the way, let's get coding!</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-were-building">What We're Building</a></p>
</li>
<li><p><a href="#heading-what-you-need">What You Need</a></p>
</li>
<li><p><a href="#heading-create-the-discord-bot">Create the Discord Bot</a></p>
</li>
<li><p><a href="#heading-give-the-bot-permission-to-read-messages">Give the Bot Permission to Read Messages</a></p>
</li>
<li><p><a href="#heading-create-the-project">Create the Project</a></p>
</li>
<li><p><a href="#heading-create-a-virtual-environment">Create a Virtual Environment</a></p>
</li>
<li><p><a href="#heading-install-discordpy">Install discord.py</a></p>
</li>
<li><p><a href="#heading-create-your-first-bot">Create Your First Bot</a></p>
<ul>
<li><p><a href="#heading-importing-our-libraries">Importing Our Libraries</a></p>
</li>
<li><p><a href="#heading-loading-the-token">Loading the Token</a></p>
</li>
<li><p><a href="#heading-understanding-intents">Understanding Intents</a></p>
</li>
<li><p><a href="#heading-what-is-ctx">What Isctx?</a></p>
</li>
<li><p><a href="#heading-why-does-everything-say-async-and-await">Why Does Everything Sayasyncandawait?</a></p>
</li>
<li><p><a href="#heading-run-the-bot">Run the Bot</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-storytelling-system">Build the Storytelling System</a></p>
<ul>
<li><p><a href="#heading-lets-make-the-story-remember-the-user">Let's Make the Story Remember the User</a></p>
</li>
<li><p><a href="#heading-add-a-story-choice">Add a Story Choice</a></p>
</li>
<li><p><a href="#heading-add-a-casual-chat-command">Add a Casual Chat Command</a></p>
</li>
<li><p><a href="#heading-add-a-mental-wellness-support-feature">Add a Mental-Wellness Support Feature</a></p>
</li>
<li><p><a href="#heading-add-a-help-command">Add a Help Command</a></p>
</li>
<li><p><a href="#heading-improve-error-handling">Improve Error Handling</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-put-everything-together">Put Everything Together</a></p>
</li>
<li><p><a href="#heading-our-bot-doesnt-actually-remember-anything">Our Bot Doesn't Actually Remember Anything</a></p>
<ul>
<li><p><a href="#heading-create-the-database">Create the Database</a></p>
</li>
<li><p><a href="#heading-save-a-users-story">Save a User's Story</a></p>
</li>
<li><p><a href="#heading-get-the-story-back">Get the Story Back</a></p>
</li>
<li><p><a href="#heading-put-it-into-a-command">Put It Into a Command</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-adding-real-ai-chat">Adding Real AI Chat</a></p>
<ul>
<li><p><a href="#heading-install-the-hugging-face-library">Install the Hugging Face Library</a></p>
</li>
<li><p><a href="#heading-create-the-hugging-face-client">Create the Hugging Face Client</a></p>
</li>
<li><p><a href="#heading-connect-the-ai-model-to-the-bot">Connect the AI Model to the Bot</a></p>
</li>
<li><p><a href="#heading-handle-ai-errors">Handle AI Errors</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-do-we-keep-the-bot-online">How Do We Keep the Bot Online?</a></p>
<ul>
<li><p><a href="#heading-option-1-run-it-on-your-computer">Option 1: Run It on Your Computer</a></p>
</li>
<li><p><a href="#heading-option-2-host-it-on-a-server">Option 2: Host It on a Server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-what-forever-actually-means">What "Forever" Actually Means</a></p>
</li>
<li><p><a href="#heading-dont-try-to-keep-it-awake-with-random-tricks">Don't Try to "Keep It Awake" With Random Tricks</a></p>
</li>
<li><p><a href="#heading-additional-features-and-where-to-go-next">Additional Features and Where to Go Next</a></p>
</li>
<li><p><a href="#heading-test-everything-locally-first">Test Everything Locally First</a></p>
</li>
<li><p><a href="#heading-deploying-the-bot">Deploying the Bot</a></p>
<ul>
<li><a href="#heading-the-start-command">The Start Command</a></li>
</ul>
</li>
<li><p><a href="#heading-remember-keep-your-secrets-secret">Remember: Keep Your Secrets Secret</a></p>
</li>
<li><p><a href="#heading-what-you-learned">What You Learned</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-were-building">What We're Building</h2>
<p>Our finished bot will have several commands:</p>
<pre><code class="language-text">!hello
!story
!chat hello!
!support I'm having a stressful day
!help
</code></pre>
<p>For example:</p>
<pre><code class="language-text">User:
!story

Bot:
You wake up inside an abandoned library.

There are three doors in front of you:

1. A red wooden door
2. A metal door covered in strange symbols
3. A staircase leading underground

Which one do you choose?
</code></pre>
<p>The user can then continue the story.</p>
<p>For chat:</p>
<pre><code class="language-text">User:
!chat What's a good way to learn Python?

Bot:
Try building small projects instead of only reading tutorials.
A Discord bot is actually a pretty fun project to start with.
</code></pre>
<p>And for mental-wellness support:</p>
<pre><code class="language-text">User:
!support I'm really stressed about school.

Bot:
That sounds like a lot to deal with. You could try breaking
the work into one small task at a time and taking a short
break between tasks.

I'm a bot, not a therapist, so if you need personal support,
consider talking with someone you trust.
</code></pre>
<p>The goal isn't to make a magical robot therapist. It's to build a useful bot while learning how Discord APIs, Python functions, events, asynchronous programming, and basic conversational logic fit together.</p>
<h2 id="heading-what-you-need">What You Need</h2>
<p>You only need a few things:</p>
<ul>
<li><p>Python (version 3.8+ is recommended)</p>
</li>
<li><p>A Discord account</p>
</li>
<li><p>A Discord server where you have permission to add a bot</p>
</li>
<li><p>A code editor (I personally prefer VS Code or PyCharm)</p>
</li>
<li><p>The <code>discord.py</code> library</p>
</li>
</ul>
<p>We'll also use Python's built-in <code>os</code> module for reading environment variables.</p>
<p>If you don't already have Python installed, install a current supported version of Python from the official Python website.</p>
<p>Then check that Python works:</p>
<pre><code class="language-bash">python --version
</code></pre>
<p>You should see something similar to:</p>
<pre><code class="language-text">Python 3.x.x
</code></pre>
<h2 id="heading-create-the-discord-bot">Create the Discord Bot</h2>
<p>Before Python can control Discord, we need to create a Discord application.</p>
<p>Go to the Discord Developer Portal: <a href="https://discord.com/developers/applications">https://discord.com/developers/applications</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/64a5aac8-fb53-42d0-9b91-fb6453b3eb1d.png" alt="Picture of the discord developer application page" style="display: block;" width="2398" height="1396" loading="lazy">

<p>This is what the page will look like, you might need to login with your discord email/username and password before you start.</p>
<p>Click on the "New Application" button on the top right and give your bot a name. For this tutorial, let's call ours <code>StoryBot</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/84bd2f32-941c-4de2-8e6b-30ab22cc5ba1.png" alt="Picture of what it looks like when you click on the &quot;New Application&quot; button" style="display: block;" width="2397" height="1303" loading="lazy">

<p>The application is basically the home for your bot.</p>
<p>Discord's developer platform provides the tools needed to create and configure applications and bots.</p>
<p>Once you've created the application, open its <strong>Bot</strong> section and create the bot user. You can add your own icon picture and your own banner if you want to.</p>
<p>You will then go to the <strong>Token</strong> section and click on "Reset Token" to generate your token. Treat that token like a password. Do <strong>NOT</strong> put it directly into your Python source code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/7537d64e-706c-43b9-a574-746294703f6d.png" alt="Picture of what the Token part in the Bots section looks like" style="display: block;" width="1894" height="189" loading="lazy">

<p>Never do this:</p>
<pre><code class="language-python">bot.run("my-secret-token")
</code></pre>
<p>And definitely don't upload a token to GitHub or commit it to source control. Instead, we'll store it in an environment variable, which we'll talk about later.</p>
<h2 id="heading-give-the-bot-permission-to-read-messages">Give the Bot Permission to Read Messages</h2>
<p>Our bot needs to see the messages that contain commands.</p>
<p>Discord uses something called <strong>Gateway Intents</strong> to control which types of events a bot receives. The <code>discord.py</code> documentation explains that intents must be enabled both in your code and, for privileged intents, in the Discord Developer Portal.</p>
<p>In the Developer Portal, find:</p>
<pre><code class="language-text">Bot
→ Privileged Gateway Intents
</code></pre>
<p>Enable:</p>
<pre><code class="language-text">Message Content Intent
</code></pre>
<p>It should look somewhat like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a581501af6af179dc1987d5/0273691f-8723-41d3-8cfc-d59656dfb6e2.png" alt="What should the &quot;Message Content Intent&quot; section look like" style="display: block;" width="1933" height="501" loading="lazy">

<p>We'll also enable it in Python, which we will talk about later in this article.</p>
<h2 id="heading-create-the-project">Create the Project</h2>
<p>Create a folder:</p>
<pre><code class="language-text">discord-story-bot/
</code></pre>
<p>Inside it, we'll eventually have:</p>
<pre><code class="language-text">discord-story-bot/
│
├── bot.py
├── requirements.txt
└── .env
</code></pre>
<p>The three important files are:</p>
<ul>
<li><p><code>bot.py</code>: our Python program</p>
</li>
<li><p><code>requirements.txt</code>: text file that contains the name of the packages our bot needs</p>
</li>
<li><p><code>.env</code>: our secret token during local development</p>
</li>
</ul>
<h2 id="heading-create-a-virtual-environment">Create a Virtual Environment</h2>
<p>Open your terminal inside the project folder.</p>
<p>Run:</p>
<pre><code class="language-bash">python -m venv venv
</code></pre>
<p>Then activate it.</p>
<p>On Windows:</p>
<pre><code class="language-bash">venv\Scripts\activate
</code></pre>
<p>On macOS/Linux:</p>
<pre><code class="language-bash">source venv/bin/activate
</code></pre>
<p>A virtual environment gives this project its own little Python bubble.</p>
<p>That means packages installed for this bot won't randomly interfere with packages used by another project.</p>
<h2 id="heading-install-discordpy">Install discord.py</h2>
<p>Now install the Discord library:</p>
<pre><code class="language-bash">pip install -U discord.py
</code></pre>
<p>The official <code>discord.py</code> documentation uses this installation approach for setting up the library.</p>
<p>We'll also install <code>python-dotenv</code>, which makes reading our local <code>.env</code> file easier:</p>
<pre><code class="language-bash">pip install python-dotenv
</code></pre>
<p>Then save the dependencies:</p>
<pre><code class="language-bash">pip freeze &gt; requirements.txt
</code></pre>
<p>Your <code>requirements.txt</code> should contain the packages needed by the project.</p>
<h2 id="heading-create-your-first-bot">Create Your First Bot</h2>
<p>Let's start small.</p>
<p>Open <code>bot.py</code>:</p>
<pre><code class="language-python">import os

import discord
from discord.ext import commands
from dotenv import load_dotenv


load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


@bot.command()
async def hello(ctx):
    await ctx.send("Hello! I'm online.")


bot.run(TOKEN)
</code></pre>
<p>That is already a functional Discord bot.</p>
<p>Let's break it apart piece by piece.</p>
<h3 id="heading-importing-our-libraries">Importing Our Libraries</h3>
<p>First:</p>
<pre><code class="language-python">import os
</code></pre>
<p><code>os</code> lets Python communicate with parts of the operating system.</p>
<p>We'll use it to read environment variables.</p>
<p>Next:</p>
<pre><code class="language-python">import discord
</code></pre>
<p>This imports <code>discord.py</code>.</p>
<p>Then:</p>
<pre><code class="language-python">from discord.ext import commands
</code></pre>
<p>The <code>commands</code> extension makes creating commands much easier.</p>
<p>Instead of manually checking every message for something like <code>!hello</code>, we can write:</p>
<pre><code class="language-python">@bot.command()
async def hello(ctx):
    await ctx.send("Hello!")
</code></pre>
<p>The <code>discord.py</code> command system is built around Python functions decorated as commands.</p>
<p>Finally:</p>
<pre><code class="language-python">from dotenv import load_dotenv
</code></pre>
<p>This lets us load values from our <code>.env</code> file.</p>
<h3 id="heading-loading-the-token">Loading the Token</h3>
<p>Our bot needs a token to connect our Python program to Discord. Think of the token as a password that allows our program to authenticate as the bot.</p>
<p>We don't want to put this secret directly into our Python code. Instead, we'll store it in an environment variable.</p>
<p>First, install <code>python-dotenv</code>:</p>
<pre><code class="language-bash">pip install python-dotenv
</code></pre>
<p>This package lets Python read values from a <code>.env</code> file.</p>
<p>Now create a new file called <code>.env</code> in the same folder as <code>bot.py</code>.</p>
<p>Inside <code>.env</code>, add:</p>
<pre><code class="language-text">DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
</code></pre>
<p>Replace <code>YOUR_BOT_TOKEN_HERE</code> with the token you copied from the Discord Developer Portal.</p>
<p>Your file should look something like this:</p>
<pre><code class="language-text">DISCORD_TOKEN=your_actual_token_here
</code></pre>
<p>Don't share this token with anyone or upload your <code>.env</code> file to GitHub. Your bot token should be treated like a password.</p>
<p>To make sure Git doesn't accidentally include the <code>.env</code> file in a repository, create a file called <code>.gitignore</code> in your project folder and add:</p>
<pre><code class="language-text">.env
venv/
__pycache__/
</code></pre>
<p>Now let's load the token in Python.</p>
<p>At the top of <code>bot.py</code>, add:</p>
<pre><code class="language-python">import os
from dotenv import load_dotenv
</code></pre>
<p>Then add:</p>
<pre><code class="language-python">load_dotenv()
</code></pre>
<p>This tells Python to look for the <code>.env</code> file and load the variables inside it.</p>
<p>Now we can get our Discord token:</p>
<pre><code class="language-python">TOKEN = os.getenv("DISCORD_TOKEN")
</code></pre>
<p><code>os.getenv()</code> looks for the environment variable named <code>"DISCORD_TOKEN"</code> and gives us its value.</p>
<p>We can also check that the token was actually found:</p>
<pre><code class="language-python">if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")
</code></pre>
<p>If Python can't find the token, the program stops and gives us a clear error message instead of failing later in a confusing way.</p>
<h3 id="heading-understanding-intents">Understanding Intents</h3>
<p>Remember how we talked about enabling message content readability in python? We are going to do that now.</p>
<p>Add:</p>
<pre><code class="language-python">intents = discord.Intents.default()
intents.message_content = True
</code></pre>
<p>The first line creates a set of Discord's default intents.</p>
<p>The second line tells Discord that our bot needs access to message content.</p>
<p>Now we need to give these intents to our bot when we create it:</p>
<pre><code class="language-python">bot = commands.Bot(
    command_prefix="!",
    intents=intents
)
</code></pre>
<p>The <code>command_prefix="!"</code> means our bot will recognize commands that begin with <code>!</code>.</p>
<p>For example:</p>
<pre><code class="language-text">!hello
</code></pre>
<p>The <code>intents=intents</code> part gives our bot the permissions we configured above.</p>
<p>There are two steps here because Discord needs to know that our bot is allowed to receive message content, while our Python program also needs to tell Discord that it wants to receive it.</p>
<p>Our basic setup should now look like this:</p>
<pre><code class="language-python">import os
import discord

from dotenv import load_dotenv
from discord.ext import commands

load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)
</code></pre>
<p>Now our bot has its token safely loaded and <code>discord.py</code> knows which intents to request when it connects to Discord.</p>
<h3 id="heading-what-is-ctx">What Is <code>ctx</code>?</h3>
<p>This part can look weird when you're learning Discord bots:</p>
<pre><code class="language-python">async def hello(ctx):
</code></pre>
<p>What is <code>ctx</code>? <code>ctx</code> stands for <strong>context</strong>. It contains information about the command that was used.</p>
<p>For example, it can tell us:</p>
<ul>
<li><p>Who ran the command</p>
</li>
<li><p>Which server it came from</p>
</li>
<li><p>Which channel it came from</p>
</li>
<li><p>What message triggered it</p>
</li>
</ul>
<p>Then:</p>
<pre><code class="language-python">await ctx.send("Hello!")
</code></pre>
<p>means:</p>
<blockquote>
<p>"Send this message back to the place where the command was used."</p>
</blockquote>
<h3 id="heading-why-does-everything-say-async-and-await">Why Does Everything Say <code>async</code> and <code>await</code>?</h3>
<p>You might notice:</p>
<pre><code class="language-python">async def hello(ctx):
</code></pre>
<p>and:</p>
<pre><code class="language-python">await ctx.send(...)
</code></pre>
<p>Discord bots spend a lot of time waiting.</p>
<p>They wait for:</p>
<ul>
<li><p>Messages</p>
</li>
<li><p>Discord responses</p>
</li>
<li><p>API requests</p>
</li>
<li><p>Timers</p>
</li>
<li><p>Other events</p>
</li>
</ul>
<p>Python's asynchronous programming features allow the bot to wait for these operations without freezing everything else.</p>
<p>You don't need to become an async-programming expert before building your first bot.</p>
<p>For now, think of <code>await</code> as:</p>
<blockquote>
<p>"Pause this task until this operation finishes, while letting the bot handle other things."</p>
</blockquote>
<h3 id="heading-run-the-bot">Run the Bot</h3>
<p>Start it with:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>If everything works, your terminal should print something similar to:</p>
<pre><code class="language-text">Logged in as StoryBot
</code></pre>
<p>Now go to your Discord server and type <code>!hello</code>. Your bot should respond.</p>
<p>Congratulations! You've officially made a Discord bot.</p>
<p>Now let's make it interesting.</p>
<h2 id="heading-build-the-storytelling-system">Build the Storytelling System</h2>
<p>First, we're going to create an interactive storytelling command.</p>
<p>At the top of <code>bot.py</code>, add:</p>
<pre><code class="language-python">import random
</code></pre>
<p>Then create some story ingredients:</p>
<pre><code class="language-python">story_locations = [
    "an abandoned library",
    "a mysterious island",
    "a futuristic city",
    "a hidden underground laboratory",
    "a forest that never appears on maps"
]

story_items = [
    "a glowing key",
    "an ancient notebook",
    "a strange compass",
    "a locked metal box",
    "a mysterious photograph"
]

story_events = [
    "You hear footsteps behind you.",
    "The lights suddenly turn off.",
    "A hidden door opens nearby.",
    "Your phone starts displaying a message from an unknown sender.",
    "You notice that the room has changed."
]
</code></pre>
<p>Now create the command:</p>
<pre><code class="language-python">@bot.command()
async def story(ctx):
    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    story_text = (
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )

    await ctx.send(story_text)
</code></pre>
<p>Now <code>!story</code> might produce:</p>
<pre><code class="language-text">You wake up in a futuristic city.

Next to you is an ancient notebook.

A hidden door opens nearby.

What do you do?
</code></pre>
<p>Run it again and you might get something completely different.</p>
<p>That's because of:</p>
<pre><code class="language-python">random.choice(...)
</code></pre>
<p>Python randomly picks one item from each list.</p>
<p>It's a simple technique, but suddenly your bot can generate hundreds of different combinations.</p>
<h3 id="heading-lets-make-the-story-remember-the-user">Let's Make the Story Remember the User</h3>
<p>Random stories are fun, but interactive stories are much better when the bot remembers what happened.</p>
<p>We can create a dictionary:</p>
<pre><code class="language-python">user_stories = {}
</code></pre>
<p>The dictionary will store story information for each user.</p>
<p>For example:</p>
<pre><code class="language-text">user ID → current story
</code></pre>
<p>Now let's modify the story command:</p>
<pre><code class="language-python">@bot.command()
async def story(ctx):
    user_id = ctx.author.id

    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    user_stories[user_id] = {
        "location": location,
        "item": item,
        "event": event
    }

    await ctx.send(
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )
</code></pre>
<p>Now each user can have their own active story.</p>
<h3 id="heading-add-a-story-choice">Add a Story Choice</h3>
<p>Let's give users choices.</p>
<pre><code class="language-python">@bot.command()
async def choose(ctx, choice: str):
    user_id = ctx.author.id

    if user_id not in user_stories:
        await ctx.send("You don't have an active story. Try `!story` first.")
        return

    choice = choice.lower()

    if choice == "left":
        response = (
            "You head left and discover a room filled with old maps. "
            "One of them has your name written on it."
        )

    elif choice == "right":
        response = (
            "You head right and find a staircase leading toward "
            "a strange blue light."
        )

    else:
        response = "Try choosing `left` or `right`."

    await ctx.send(response)
</code></pre>
<p>Now users can type:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p>or:</p>
<pre><code class="language-text">!choose right
</code></pre>
<p>Notice this:</p>
<pre><code class="language-python">async def choose(ctx, choice: str):
</code></pre>
<p>The <code>choice</code> parameter receives the text after the command.</p>
<p>So:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p>becomes approximately:</p>
<pre><code class="language-python">choice = "left"
</code></pre>
<p>This is one of the reasons command frameworks are so convenient. A <strong>command framework</strong> is a set of tools that makes it easier to create and manage commands in a program. In our case, <code>discord.py</code> provides the command framework that lets us turn Python functions into Discord commands using decorators like <code>@bot.command()</code>.</p>
<p>Instead of manually checking every message to figure out whether someone typed <code>!choose</code>, <code>discord.py</code> handles that work for us. It recognizes the command, takes the user's arguments, and passes them to our function.</p>
<p>So when someone types:</p>
<pre><code class="language-text">!choose left
</code></pre>
<p><code>discord.py</code> knows that choose is the command, <code>"left"</code> is the argument, and that it should call our <code>choose()</code> function with that information.</p>
<h3 id="heading-add-a-casual-chat-command">Add a Casual Chat Command</h3>
<p>Now let's make the bot capable of basic conversation.</p>
<p>We could connect it to a large language model API, but you don't actually need AI to learn how a chat command works. We'll start with a simple keyword-based response system.</p>
<p>First, we'll create a dictionary containing some keywords and possible responses:</p>
<pre><code class="language-python">chat_responses = {
    "hello": [
        "Hey! What's up?",
        "Hello! How's your day going?",
        "Hi! What are you working on?"
    ],
    "python": [
        "Python is a great language for beginners because its syntax is pretty readable.",
        "If you're learning Python, try building something instead of only watching tutorials."
    ],
    "discord": [
        "Discord bots are a fun way to practice Python because you get instant feedback.",
        "Once you understand commands and events, you can build some surprisingly complex bots."
    ]
}

Think of `chat_responses` as a small collection of things our bot knows how to talk about. Each key, such as `"python"` or `"discord"`, represents a keyword the bot can look for. The value associated with each key is a list of possible responses.

We use a list instead of a single response so the bot doesn't give exactly the same answer every time. Later, we'll randomly choose one of these responses.

Now let's create the actual `!chat` command:

```python
@bot.command()
async def chat(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in chat_responses.items():
        if keyword in text:
            await ctx.send(random.choice(responses))
            return

    await ctx.send(
        "I'm still learning how to respond to that. "
        "Try talking to me about Python or Discord!"
    )
</code></pre>
<p>There are a few things happening here, so let's break it down.</p>
<p>First, this part:</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
</code></pre>
<p>turns the <code>chat()</code> function into a Discord command. The <code>*</code> is important because it tells <code>discord.py</code> to treat everything after the command as one argument.</p>
<p>For example, if someone types:</p>
<pre><code class="language-text">!chat I want to learn Python
</code></pre>
<p>the entire phrase after <code>!chat</code> becomes the value of <code>message</code>:</p>
<pre><code class="language-python">message = "I want to learn Python"
</code></pre>
<p>Next, we have:</p>
<pre><code class="language-python">text = message.lower()
</code></pre>
<p>This converts the message to lowercase. That means <code>Python</code>, <code>python</code>, and <code>PYTHON</code> will all become <code>python</code>. Without this, our keyword check could miss a match simply because the user capitalized a word differently.</p>
<p>Now we get to the loop:</p>
<pre><code class="language-python">for keyword, responses in chat_responses.items():
</code></pre>
<p><code>.items()</code> lets us go through both the keyword and its corresponding list of responses. During each loop, <code>keyword</code> contains something like <code>"python"</code>, while <code>responses</code> contains the list of responses associated with it.</p>
<p>Then we check:</p>
<pre><code class="language-python">if keyword in text:
</code></pre>
<p>This asks whether the current keyword appears anywhere in the user's message.</p>
<p>If the user writes:</p>
<pre><code class="language-text">!chat I want to learn Python
</code></pre>
<p>the lowercase version becomes:</p>
<pre><code class="language-text">i want to learn python
</code></pre>
<p>Since <code>"python"</code> appears inside that text, the condition is true.</p>
<p>The bot can then choose a random response:</p>
<pre><code class="language-python">await ctx.send(random.choice(responses))
</code></pre>
<p><code>random.choice()</code> picks one item from the response list, while <code>ctx.send()</code> sends that response back to the Discord channel.</p>
<p>Finally, we have:</p>
<pre><code class="language-python">return
</code></pre>
<p>This stops the function after a matching keyword is found. Without it, the loop would continue checking the other keywords even after the bot had already responded.</p>
<p>But what happens if none of the keywords match?</p>
<p>That's what this part handles:</p>
<pre><code class="language-python">await ctx.send(
    "I'm still learning how to respond to that. "
    "Try talking to me about Python or Discord!"
)
</code></pre>
<p>If the loop finishes without finding a keyword, the bot sends this fallback message instead.</p>
<p>For example:</p>
<pre><code class="language-text">!chat I like pizza
</code></pre>
<p>doesn't contain <code>"hello"</code>, <code>"python"</code>, or <code>"discord"</code>, so the bot doesn't have a specific response to use.</p>
<p>This gives us a simple way for the bot to have conversations without needing an AI model.</p>
<h3 id="heading-add-a-mental-wellness-support-feature">Add a Mental-Wellness Support Feature</h3>
<p>Now for the feature that needs a little more care.</p>
<p>Instead of calling this a "therapy command" internally, we'll call it:</p>
<pre><code class="language-text">!support
</code></pre>
<p>Quick additional disclaimer before we start...this is just a fun wellness script, not a real therapist!</p>
<p>Create:</p>
<pre><code class="language-python">support_responses = {
    "stress": [
        "That sounds like a lot to handle. Try breaking the situation into one small task at a time.",
        "When everything feels overwhelming, it can help to pause and focus on what needs attention right now."
    ],

    "school": [
        "School can pile up quickly. Consider choosing one assignment to work on first instead of trying to solve everything at once.",
        "If school stress is getting difficult to manage, talking with a trusted person can make things feel less like something you have to handle alone."
    ],

    "sad": [
        "I'm sorry you're having a difficult moment. Taking a short break, doing something calming, or talking with someone you trust may help.",
        "You don't have to solve everything immediately. Give yourself some time and consider reaching out to someone you trust."
    ]
}
</code></pre>
<p>Now create the command:</p>
<pre><code class="language-python">@bot.command()
async def support(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in support_responses.items():
        if keyword in text:
            response = random.choice(responses)

            await ctx.send(
                f"{response}\n\n"
                "I'm a bot, not a therapist or medical professional. "
                "If you need personal support, consider talking with "
                "someone you trust."
            )
            return

    await ctx.send(
        "It sounds like something is bothering you. "
        "I can offer general wellness suggestions, but I'm not a therapist. "
        "If you need personal support, consider reaching out to someone you trust."
    )
</code></pre>
<p>Now someone can type:</p>
<pre><code class="language-text">!support I'm stressed about school
</code></pre>
<p>The bot sees the word:</p>
<pre><code class="language-text">school
</code></pre>
<p>and chooses one of the school-related responses.</p>
<p>This is deliberately simple.</p>
<p>For a real public bot, you'd want much more careful safety handling, testing, moderation, privacy protection, and escalation logic before allowing users to rely on it for sensitive situations.</p>
<h3 id="heading-add-a-help-command">Add a Help Command</h3>
<p>A good bot should explain itself.</p>
<pre><code class="language-python">@bot.command()
async def commands_help(ctx):
    await ctx.send(
        "**Available commands:**\n"
        "`!hello` - Say hello\n"
        "`!story` - Start a new story\n"
        "`!choose left` - Choose the left path\n"
        "`!choose right` - Choose the right path\n"
        "`!chat &lt;message&gt;` - Have a casual conversation\n"
        "`!support &lt;message&gt;` - Get general wellness support"
    )
</code></pre>
<p>There's one small issue.</p>
<p>Discord's default help command is already called <code>help</code>.</p>
<p>So instead of:</p>
<pre><code class="language-python">async def help(ctx):
</code></pre>
<p>we've named ours:</p>
<pre><code class="language-python">commands_help
</code></pre>
<p>If you want the command itself to be called <code>!help</code>, you can write:</p>
<pre><code class="language-python">@bot.command(name="help")
async def commands_help(ctx):
    ...
</code></pre>
<p>That tells Discord:</p>
<blockquote>
<p>Use <code>!help</code> for this function even though the Python function has another name.</p>
</blockquote>
<h3 id="heading-improve-error-handling">Improve Error Handling</h3>
<p>Bots shouldn't crash just because someone enters an invalid command.</p>
<p>Add:</p>
<pre><code class="language-python">@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(
            "You're missing something. Try `!help` to see how the command works."
        )

    elif isinstance(error, commands.CommandNotFound):
        return

    else:
        print(f"Error: {error}")
</code></pre>
<p>Now if someone types:</p>
<pre><code class="language-text">!chat
</code></pre>
<p>without giving the bot a message, it can respond with a useful explanation instead of dumping a confusing error into the conversation.</p>
<h2 id="heading-put-everything-together">Put Everything Together</h2>
<p>At this point, your <code>bot.py</code> can look like this:</p>
<pre><code class="language-python">import os
import random

import discord
from discord.ext import commands
from dotenv import load_dotenv


load_dotenv()

TOKEN = os.getenv("DISCORD_TOKEN")

if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")


intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix="!",
    intents=intents
)


story_locations = [
    "an abandoned library",
    "a mysterious island",
    "a futuristic city",
    "a hidden underground laboratory",
    "a forest that never appears on maps"
]

story_items = [
    "a glowing key",
    "an ancient notebook",
    "a strange compass",
    "a locked metal box",
    "a mysterious photograph"
]

story_events = [
    "You hear footsteps behind you.",
    "The lights suddenly turn off.",
    "A hidden door opens nearby.",
    "Your phone starts displaying a message from an unknown sender.",
    "You notice that the room has changed."
]


user_stories = {}


chat_responses = {
    "hello": [
        "Hey! What's up?",
        "Hello! How's your day going?",
        "Hi! What are you working on?"
    ],

    "python": [
        "Python is a great language for beginners because its syntax is pretty readable.",
        "If you're learning Python, try building something instead of only watching tutorials."
    ],

    "discord": [
        "Discord bots are a fun way to practice Python because you get instant feedback.",
        "Once you understand commands and events, you can build some surprisingly complex bots."
    ]
}


support_responses = {
    "stress": [
        "That sounds like a lot to handle. Try breaking the situation into one small task at a time.",
        "When everything feels overwhelming, it can help to pause and focus on what needs attention right now."
    ],

    "school": [
        "School can pile up quickly. Consider choosing one assignment to work on first instead of trying to solve everything at once.",
        "If school stress is getting difficult to manage, talking with a trusted person can make things feel less like something you have to handle alone."
    ],

    "sad": [
        "I'm sorry you're having a difficult moment. Taking a short break, doing something calming, or talking with someone you trust may help.",
        "You don't have to solve everything immediately. Give yourself some time and consider reaching out to someone you trust."
    ]
}


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


@bot.command()
async def hello(ctx):
    await ctx.send("Hello! I'm online.")


@bot.command()
async def story(ctx):
    user_id = ctx.author.id

    location = random.choice(story_locations)
    item = random.choice(story_items)
    event = random.choice(story_events)

    user_stories[user_id] = {
        "location": location,
        "item": item,
        "event": event
    }

    await ctx.send(
        f"You wake up in {location}.\n\n"
        f"Next to you is {item}.\n\n"
        f"{event}\n\n"
        "What do you do?"
    )


@bot.command()
async def choose(ctx, choice: str):
    user_id = ctx.author.id

    if user_id not in user_stories:
        await ctx.send(
            "You don't have an active story. Try `!story` first."
        )
        return

    choice = choice.lower()

    if choice == "left":
        response = (
            "You head left and discover a room filled with old maps. "
            "One of them has your name written on it."
        )

    elif choice == "right":
        response = (
            "You head right and find a staircase leading toward "
            "a strange blue light."
        )

    else:
        response = "Try choosing `left` or `right`."

    await ctx.send(response)


@bot.command()
async def chat(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in chat_responses.items():
        if keyword in text:
            await ctx.send(random.choice(responses))
            return

    await ctx.send(
        "I'm still learning how to respond to that. "
        "Try talking to me about Python or Discord!"
    )


@bot.command()
async def support(ctx, *, message: str):
    text = message.lower()

    for keyword, responses in support_responses.items():
        if keyword in text:
            response = random.choice(responses)

            await ctx.send(
                f"{response}\n\n"
                "I'm a bot, not a therapist or medical professional. "
                "If you need personal support, consider talking with "
                "someone you trust."
            )
            return

    await ctx.send(
        "It sounds like something is bothering you. "
        "I can offer general wellness suggestions, but I'm not a therapist. "
        "If you need personal support, consider reaching out to someone you trust."
    )


@bot.command(name="help")
async def commands_help(ctx):
    await ctx.send(
        "**Available commands:**\n"
        "`!hello` - Say hello\n"
        "`!story` - Start a new story\n"
        "`!choose left` - Choose the left path\n"
        "`!choose right` - Choose the right path\n"
        "`!chat &lt;message&gt;` - Have a casual conversation\n"
        "`!support &lt;message&gt;` - Get general wellness support"
    )


@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(
            "You're missing something. Try `!help` to see how the command works."
        )

    elif isinstance(error, commands.CommandNotFound):
        return

    else:
        print(f"Error: {error}")


bot.run(TOKEN)
</code></pre>
<p>This is enough to create a surprisingly capable beginner Discord project.</p>
<p>But there's an important limitation.</p>
<h2 id="heading-our-bot-doesnt-actually-remember-anything">Our Bot Doesn't Actually Remember Anything</h2>
<p>There's one small problem with our bot so far: it doesn't actually remember anything after it shuts down.</p>
<p>Right now, we're storing our story information in a Python dictionary:</p>
<pre><code class="language-python">user_stories = {}
</code></pre>
<p>This works while the bot is running. But if you stop the program and start it again, the dictionary starts empty.</p>
<p>To fix this, we need somewhere to permanently store our data. That's where a <strong>database</strong> comes in.</p>
<p>For this project, we'll use <strong>SQLite</strong>. SQLite is a lightweight database that stores information in a file on your computer. Python already includes SQLite through the built-in <code>sqlite3</code> module, so we don't need to install anything extra.</p>
<h3 id="heading-create-the-database">Create the Database</h3>
<p>First, add this import near the top of <code>bot.py</code>:</p>
<pre><code class="language-python">import sqlite3
</code></pre>
<p>Then create a connection to a database file:</p>
<pre><code class="language-python">db = sqlite3.connect("bot.db")
cursor = db.cursor()
</code></pre>
<p>The first line creates a database file called <code>bot.db</code> if one doesn't already exist. If the file already exists, SQLite simply opens it.</p>
<p>The second line creates a <strong>cursor</strong>. You can think of the cursor as the part of our Python program that lets us send instructions to the database.</p>
<p>Now we need to create a table where we can store our users' story information:</p>
<pre><code class="language-python">cursor.execute("""
    CREATE TABLE IF NOT EXISTS user_stories (
        user_id INTEGER PRIMARY KEY,
        location TEXT,
        item TEXT,
        event TEXT
    )
""")

db.commit()
</code></pre>
<p>Let's break this down.</p>
<p><code>cursor.execute()</code> tells SQLite to run the SQL command inside the parentheses.</p>
<p>The SQL command starts with:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS user_stories
</code></pre>
<p>This tells SQLite to create a table called <code>user_stories</code>, but only if that table doesn't already exist.</p>
<p>Inside the parentheses, we define the information that each row can contain:</p>
<pre><code class="language-sql">user_id INTEGER PRIMARY KEY,
location TEXT,
item TEXT,
event TEXT
</code></pre>
<p><code>user_id</code> stores the Discord user's ID. We use it as the <code>PRIMARY KEY</code>, which means each user gets their own unique row.</p>
<p><code>location</code>, <code>item</code>, and <code>event</code> are all pieces of information about the user's current story.</p>
<p>Finally:</p>
<pre><code class="language-python">db.commit()
</code></pre>
<p>saves the changes to the database.</p>
<p>At this point, your project folder should contain a new file called:</p>
<pre><code class="language-text">bot.db
</code></pre>
<p>You don't need to open or edit this file manually. SQLite will manage it for us.</p>
<h3 id="heading-save-a-users-story">Save a User's Story</h3>
<p>Now let's actually put information into our database.</p>
<p>Suppose we have these variables:</p>
<pre><code class="language-python">user_id = ctx.author.id
location = "an abandoned castle"
item = "a mysterious key"
event = "a locked door"
</code></pre>
<p>We can save them using:</p>
<pre><code class="language-python">cursor.execute(
    """
    INSERT OR REPLACE INTO user_stories
    (user_id, location, item, event)
    VALUES (?, ?, ?, ?)
    """,
    (user_id, location, item, event)
)

db.commit()
</code></pre>
<p>The SQL statement tells SQLite to insert the information into the <code>user_stories</code> table.</p>
<p>The <code>?</code> symbols are placeholders for the actual values. The values are provided separately here:</p>
<pre><code class="language-python">(user_id, location, item, event)
</code></pre>
<p>This is safer than manually inserting values directly into the SQL string.</p>
<p><code>INSERT OR REPLACE</code> also means that if this user already has a saved story, their old story information can be replaced with the new information.</p>
<h3 id="heading-get-the-story-back">Get the Story Back</h3>
<p>Saving information is only half of the job. We also need to be able to retrieve it.</p>
<p>We can search the database for a user's story like this:</p>
<pre><code class="language-python">cursor.execute(
    """
    SELECT location, item, event
    FROM user_stories
    WHERE user_id = ?
    """,
    (user_id,)
)

story = cursor.fetchone()
</code></pre>
<p>This time, we're using <code>SELECT</code> to ask SQLite for information.</p>
<p>The <code>WHERE</code> part is important:</p>
<pre><code class="language-sql">WHERE user_id = ?
</code></pre>
<p>It tells SQLite to find the row belonging to this specific Discord user.</p>
<p>Then:</p>
<pre><code class="language-python">story = cursor.fetchone()
</code></pre>
<p>gets the first matching result.</p>
<p>If the user has a saved story, <code>story</code> will contain their information. If they don't, <code>story</code> will be <code>None</code>.</p>
<p>We can check for that:</p>
<pre><code class="language-python">if story:
    location, item, event = story

    await ctx.send(
        f"You're currently in {location}. "
        f"You have {item}, and you're facing {event}."
    )
else:
    await ctx.send("I don't have a saved story for you yet!")
</code></pre>
<p>Now the bot can retrieve information that was saved earlier, even after the Python program has been restarted.</p>
<h3 id="heading-put-it-into-a-command">Put It Into a Command</h3>
<p>We can turn this into a simple command that lets users check their saved story:</p>
<pre><code class="language-python">@bot.command()
async def status(ctx):
    user_id = ctx.author.id

    cursor.execute(
        """
        SELECT location, item, event
        FROM user_stories
        WHERE user_id = ?
        """,
        (user_id,)
    )

    story = cursor.fetchone()

    if story:
        location, item, event = story

        await ctx.send(
            f"You're currently in {location}. "
            f"You have {item}, and you're facing {event}."
        )
    else:
        await ctx.send(
            "You don't have a saved story yet. "
            "Start one with `!story`!"
        )
</code></pre>
<p>Now a user can type:</p>
<pre><code class="language-text">!status
</code></pre>
<p>and the bot can look up their story from the database.</p>
<p>This is a big improvement over our original dictionary. A dictionary only remembers information while the Python program is running. SQLite lets us save that information so it can still be there when the bot starts again.</p>
<p>For a larger bot, you could eventually store things like user preferences, story progress, inventory, conversation history, or other data. But for now, this simple database is enough to give our bot some real memory.</p>
<h2 id="heading-adding-real-ai-chat">Adding Real AI Chat</h2>
<p>Before we connect our bot to an AI model, let's quickly talk about <strong>Hugging Face</strong>.</p>
<p>If you've never used it before, Hugging Face is a platform where developers can find, share, and use machine learning models and datasets. Think of it as a huge community and library for AI tools.</p>
<p>Hugging Face also provides tools that let Python programs communicate with these models without having to build and train an AI model from scratch.</p>
<p>For our bot, we'll use Hugging Face's <strong>Inference Providers</strong> to send a user's message to a supported language model and receive its response.</p>
<p>We won't be training an AI model ourselves. Instead, we'll use an existing model and connect it to our Discord bot through Python.</p>
<p>Now that we know what Hugging Face is, let's connect it to our bot.</p>
<h3 id="heading-install-the-hugging-face-library">Install the Hugging Face Library</h3>
<p>First, install <code>huggingface_hub</code>:</p>
<pre><code class="language-bash">pip install -U huggingface_hub
</code></pre>
<p>We already installed <code>python-dotenv</code>, so we can use the same <code>.env</code> file from earlier to keep our Hugging Face token out of the source code.</p>
<p>Add your Hugging Face token to <code>.env</code>:</p>
<pre><code class="language-text">DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
HF_TOKEN=YOUR_HUGGING_FACE_TOKEN_HERE
</code></pre>
<p>Replace <code>YOUR_HUGGING_FACE_TOKEN_HERE</code> with your actual Hugging Face access token.</p>
<p>Just like your Discord bot token, <strong>don't share this token or upload it to GitHub</strong>.</p>
<h3 id="heading-create-the-hugging-face-client">Create the Hugging Face Client</h3>
<p>Now add this import near the top of <code>bot.py</code>:</p>
<pre><code class="language-python">from huggingface_hub import InferenceClient
</code></pre>
<p>Then load the token:</p>
<pre><code class="language-python">HF_TOKEN = os.getenv("HF_TOKEN")

if not HF_TOKEN:
    raise RuntimeError("HF_TOKEN is not set.")
</code></pre>
<p>The first line gets the token from our environment variables. The <code>if</code> statement checks whether the token actually exists. If it doesn't, Python stops and gives us a useful error instead of letting the program fail later in a confusing way.</p>
<p>Now create the Hugging Face client:</p>
<pre><code class="language-python">client = InferenceClient(
    api_key=HF_TOKEN
)
</code></pre>
<p>The <code>InferenceClient</code> is what our Python program will use to communicate with Hugging Face's inference service.</p>
<h3 id="heading-connect-the-ai-model-to-the-bot">Connect the AI Model to the Bot</h3>
<p>Now we can replace our previous keyword-based <code>!chat</code> command with one that sends the user's message to a language model.</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
    try:
        response = client.chat_completion(
            model="YOUR_SUPPORTED_MODEL_ID",
            messages=[
                {
                    "role": "system",
                    "content": (
                        "You are a friendly Discord bot. "
                        "Keep responses helpful, concise, and conversational."
                    )
                },
                {
                    "role": "user",
                    "content": message
                }
            ],
            max_tokens=200
        )

        answer = response.choices[0].message.content

        await ctx.send(answer)

    except Exception as error:
        print(f"AI error: {error}")
        await ctx.send(
            "I couldn't generate a response right now. "
            "Please try again later."
        )
</code></pre>
<p>There's quite a bit happening here, so let's walk through it.</p>
<p>We start with the same command structure we've already used:</p>
<pre><code class="language-python">@bot.command()
async def chat(ctx, *, message: str):
</code></pre>
<p>This creates our <code>!chat</code> command and stores everything the user types after it in <code>message</code>.</p>
<p>For example:</p>
<pre><code class="language-text">!chat What is Python?
</code></pre>
<p>gives us:</p>
<pre><code class="language-python">message = "What is Python?"
</code></pre>
<p>Next, we use:</p>
<pre><code class="language-python">try:
</code></pre>
<p>This tells Python that we're about to run code that could potentially fail. Since we're communicating with an external service, things like an unavailable model, an invalid token, or a temporary connection problem can happen.</p>
<p>Now we call:</p>
<pre><code class="language-python">response = client.chat_completion(
</code></pre>
<p>This sends a chat-completion request to the model through Hugging Face. The <code>messages</code> parameter contains the conversation we want the model to respond to.</p>
<p>The first message has the role <code>"system"</code>:</p>
<pre><code class="language-python">{
    "role": "system",
    "content": (
        "You are a friendly Discord bot. "
        "Keep responses helpful, concise, and conversational."
    )
}
</code></pre>
<p>The system message gives the model instructions about how it should respond.</p>
<p>Then we provide the user's actual message:</p>
<pre><code class="language-python">{
    "role": "user",
    "content": message
}
</code></pre>
<p>If the user typed:</p>
<pre><code class="language-text">!chat What is Python?
</code></pre>
<p>then <code>message</code> contains:</p>
<pre><code class="language-text">What is Python?
</code></pre>
<p>So the model receives that as the user's input.</p>
<p>We also have:</p>
<pre><code class="language-python">max_tokens=200
</code></pre>
<p>This limits how much text the model can generate for one response. Keeping responses relatively short works well for Discord because huge blocks of text aren't always very pleasant to read in a chat channel.</p>
<p>You also need to replace:</p>
<pre><code class="language-python">model="YOUR_SUPPORTED_MODEL_ID"
</code></pre>
<p>with the ID of a model currently available through the Hugging Face Inference Providers you are using. Hugging Face's documentation shows that <code>InferenceClient</code> can use a model ID hosted on the Hugging Face Hub for chat completion.</p>
<p>Once the request is complete, we need to get the actual text from the response:</p>
<pre><code class="language-python">answer = response.choices[0].message.content
</code></pre>
<p>The response contains information about the model's output. <code>choices[0]</code> gets the first generated response, and <code>.message.content</code> gives us the actual text.</p>
<p>Then we send it to Discord:</p>
<pre><code class="language-python">await ctx.send(answer)
</code></pre>
<p>So the whole process looks like this:</p>
<pre><code class="language-text">User types !chat
        ↓
Discord sends the command to our bot
        ↓
Python gets the user's message
        ↓
Hugging Face receives the message
        ↓
The AI model generates a response
        ↓
Python gets the generated text
        ↓
The bot sends it back to Discord
</code></pre>
<h3 id="heading-handle-ai-errors">Handle AI Errors</h3>
<p>The last part of our command is:</p>
<pre><code class="language-python">except Exception as error:
    print(f"AI error: {error}")
    await ctx.send(
        "I couldn't generate a response right now. "
        "Please try again later."
    )
</code></pre>
<p>If something goes wrong inside the <code>try</code> block, Python jumps to the <code>except</code> block instead of crashing the entire bot.</p>
<p>The error is printed in the terminal so you can investigate what happened:</p>
<pre><code class="language-python">print(f"AI error: {error}")
</code></pre>
<p>Meanwhile, the Discord user gets a simple message:</p>
<pre><code class="language-text">I couldn't generate a response right now. Please try again later.
</code></pre>
<p>This is much better than letting an API error take down the whole bot.</p>
<p>At this point, you have a real AI-powered <code>!chat</code> command. You can type something like:</p>
<pre><code class="language-text">!chat Tell me an interesting fact about space.
</code></pre>
<p>and the model can generate a response instead of choosing from a small list of pre-written messages.</p>
<p>One thing to remember is that this bot is sending user messages to an external AI service. Don't automatically send private or sensitive conversations to an AI provider. If you make this bot available to other people, be clear about what information it processes and avoid storing or sending more data than the bot actually needs.</p>
<p>You can also combine this AI system with the SQLite database from earlier. For example, you could save a limited amount of conversation history and send relevant previous messages along with a new message. That would allow the bot to keep some context between messages instead of treating every message as a completely new conversation.</p>
<h2 id="heading-how-do-we-keep-the-bot-online">How Do We Keep the Bot Online?</h2>
<p>Here's where the phrase "online forever" needs a little clarification.</p>
<p>There are two different situations.</p>
<h3 id="heading-option-1-run-it-on-your-computer">Option 1: Run It on Your Computer</h3>
<p>When you run:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>the bot stays online while that program is running.</p>
<p>Close the terminal?</p>
<p>Bot goes offline.</p>
<p>Turn off the computer?</p>
<p>Bot goes offline.</p>
<p>Lose internet?</p>
<p>Bot goes offline.</p>
<p>This is perfect for development but it's not a 24/7 production setup.</p>
<h3 id="heading-option-2-host-it-on-a-server">Option 2: Host It on a Server</h3>
<p>For a bot that should stay online while your computer is off, you need a computer somewhere that stays available.</p>
<p>That computer can be a cloud server.</p>
<p>You upload your project, install the dependencies, add your environment variables, and start:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>Now the cloud machine runs the program instead of your laptop.</p>
<p>Services designed for continuously running workloads can be used for this kind of application. For example, Render currently provides a <strong>Background Worker</strong> service type for continuously running processes that don't need to receive incoming web traffic.</p>
<p>But you should check the provider's current pricing and service limitations before deploying. Free hosting tiers aren't necessarily designed for an always-on Discord bot, and a "free forever" 24/7 setup isn't something you should assume a hosting platform will provide.</p>
<h2 id="heading-what-forever-actually-means">What "Forever" Actually Means</h2>
<p>There isn't really a magical:</p>
<pre><code class="language-text">ONLINE_FOREVER = True
</code></pre>
<p>setting.</p>
<p>A bot can stay online continuously only as long as the computer or server running it continues operating.</p>
<p>Even a professionally hosted bot can go offline because of:</p>
<ul>
<li><p>Server maintenance</p>
</li>
<li><p>Deployments</p>
</li>
<li><p>Bugs</p>
</li>
<li><p>Network problems</p>
</li>
<li><p>Provider outages</p>
</li>
<li><p>Invalid credentials</p>
</li>
<li><p>API changes</p>
</li>
<li><p>Billing or account issues</p>
</li>
</ul>
<p>So the realistic goal is to keep the bot running automatically and restart it when something goes wrong.</p>
<p>That is what production hosting is designed to help with.</p>
<p>If your provider supports automatic restarts, enable them.</p>
<p>You can also make your Python code fail clearly when an important environment variable is missing:</p>
<pre><code class="language-python">if not TOKEN:
    raise RuntimeError("DISCORD_TOKEN is not set.")
</code></pre>
<p>A clear error is much easier to debug than a mysterious bot that simply doesn't appear online.</p>
<h2 id="heading-dont-try-to-keep-it-awake-with-random-tricks">Don't Try to "Keep It Awake" With Random Tricks</h2>
<p>You may find tutorials suggesting that you deploy a web server and repeatedly ping it from another service to prevent a free hosting instance from sleeping.</p>
<p>Be careful with that approach.</p>
<p>Hosting providers change their free-tier rules, and attempting to work around those limits can violate their terms.</p>
<p>If you need an actually persistent bot, use a hosting option that explicitly supports the workload.</p>
<p>For example, a background worker is designed for continuously running processes. That's much cleaner than trying to convince a web service that your Discord bot is secretly a website.</p>
<h2 id="heading-additional-features-and-where-to-go-next"><strong>Additional Featur</strong>es and Where to Go Next</h2>
<p>Now that you have a working Discord bot, there are plenty of directions you can take the project next.</p>
<p>You could turn the storytelling system into a more complete game by adding an inventory, multiple chapters, puzzles, or different endings. You could also replace text-based commands with Discord slash commands and buttons to make the bot easier to interact with.</p>
<p>If you're interested in AI, you could expand the chat system by giving the bot different personalities, adding carefully limited conversation context, or using AI to generate parts of the stories.</p>
<p>You could also add moderation features, daily story prompts, or other commands that fit the kind of Discord community you're building.</p>
<p>These are ideas for extending the project rather than features we'll build step by step in this tutorial. The important thing is that you now have the foundation to experiment with them yourself.</p>
<p>Start with one small feature, figure out how it works, and build from there. You don't need to turn the bot into a massive project all at once.</p>
<p>The more you experiment with the code, the more you'll start seeing how Python, Discord, databases, and AI can work together in a real application.</p>
<h2 id="heading-test-everything-locally-first">Test Everything Locally First</h2>
<p>Before deploying, test:</p>
<pre><code class="language-text">!hello
!story
!choose left
!choose right
!chat hello
!chat I want to learn Python
!support I'm stressed
!help
</code></pre>
<p>Then test weird inputs:</p>
<pre><code class="language-text">!choose banana
!chat
!support
!unknowncommand
</code></pre>
<p>You want to discover bugs while you're sitting in front of your computer, not three days later when someone tells you:</p>
<blockquote>
<p>"Your bot has been broken since Tuesday."</p>
</blockquote>
<h2 id="heading-deploying-the-bot">Deploying the Bot</h2>
<p>First, make sure your project contains:</p>
<pre><code class="language-text">discord-story-bot/
│
├── bot.py
├── requirements.txt
├── .gitignore
└── .python-version
</code></pre>
<p>A <code>.python-version</code> file can contain something like:</p>
<pre><code class="language-text">3.13
</code></pre>
<p>Using a version file makes your deployment environment more predictable. Render currently supports specifying a Python version through <code>.python-version</code> or an environment variable.</p>
<p>Your <code>requirements.txt</code> should contain your dependencies.</p>
<p>For example:</p>
<pre><code class="language-text">discord.py
python-dotenv
</code></pre>
<p>For deployment, you generally don't need the local <code>.env</code> file.</p>
<p>Instead, add:</p>
<pre><code class="language-text">DISCORD_TOKEN
</code></pre>
<p>as an environment variable in your hosting provider's dashboard.</p>
<p>That way the secret isn't stored inside your repository.</p>
<h3 id="heading-the-start-command">The Start Command</h3>
<p>Your deployment service needs to know what to run.</p>
<p>For this project, the start command is:</p>
<pre><code class="language-bash">python bot.py
</code></pre>
<p>The important thing is that the process doesn't immediately exit.</p>
<p>A Discord bot stays alive because <code>bot.run(TOKEN)</code> starts the Discord connection and keeps the program running.</p>
<p>If your hosting service supports background workers, that's a natural fit for a bot like this because the bot doesn't need to serve normal HTTP requests. Render specifically describes background workers as continuously running services that don't receive incoming network traffic.</p>
<h2 id="heading-remember-keep-your-secrets-secret">Remember: Keep Your Secrets Secret</h2>
<p>This is worth repeating because it causes a lot of beginner projects to get compromised.</p>
<p>Never commit this:</p>
<pre><code class="language-python">bot.run("YOUR_REAL_TOKEN")
</code></pre>
<p>Never upload:</p>
<pre><code class="language-text">.env
</code></pre>
<p>Never paste your actual token into a public GitHub issue.</p>
<p>If a token accidentally becomes public, treat it as compromised and regenerate it.</p>
<p>Environment variables are your friend.</p>
<h2 id="heading-what-you-learned">What You Learned</h2>
<p>You've now built a Discord bot that demonstrates several real programming concepts.</p>
<p>You learned how to:</p>
<ul>
<li><p>Create a Discord application</p>
</li>
<li><p>Connect Python to Discord</p>
</li>
<li><p>Use <code>discord.py</code></p>
</li>
<li><p>Configure Gateway Intents</p>
</li>
<li><p>Create commands</p>
</li>
<li><p>Use asynchronous functions</p>
</li>
<li><p>Read command arguments</p>
</li>
<li><p>Generate random stories</p>
</li>
<li><p>Store temporary user state</p>
</li>
<li><p>Create a basic chat system</p>
</li>
<li><p>Create a mental-wellness support feature</p>
</li>
<li><p>Handle command errors</p>
</li>
<li><p>Keep secrets out of source code</p>
</li>
<li><p>Prepare a project for deployment</p>
</li>
<li><p>Think about persistent hosting</p>
</li>
</ul>
<p>And underneath all those features, the architecture is still surprisingly simple:</p>
<pre><code class="language-text">User sends command
        ↓
Discord receives message
        ↓
discord.py receives event
        ↓
Python function runs
        ↓
Bot generates response
        ↓
Discord displays response
</code></pre>
<p>You don't need thousands of lines of code to get started.</p>
<p>You need a clear idea, a few Python concepts, and the willingness to keep debugging when something inevitably breaks.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>The coolest part of this project isn't really the Discord bot. It's what the project teaches you.</p>
<p>And once you understand the pieces, you can reuse the same ideas in countless projects.</p>
<p>A Discord bot can become a game, which could become a web application, which could also become a larger software project.</p>
<p>And suddenly you're not just learning Python syntax anymore. You're learning how software actually gets built, one command at a time.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a LangGraph and Composio-Powered Discord Bot ]]>
                </title>
                <description>
                    <![CDATA[ With the rise of AI tools over the past couple years, most of us are learning how to use them in our projects. And in this article, I’ll teach you how to build a quick Discord bot with LangGraph and Composio. You’ll use LangGraph nodes to build a bra... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-langgraph-composio-powered-discord-bot/</link>
                <guid isPermaLink="false">685b12877dabc4d300e53706</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agentic AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ langgraph ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bot ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shrijal Acharya ]]>
                </dc:creator>
                <pubDate>Tue, 24 Jun 2025 21:03:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750798930964/65dd7078-e4e7-42d0-a797-1e7d72690513.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>With the rise of AI tools over the past couple years, most of us are learning how to use them in our projects. And in this article, I’ll teach you how to build a quick Discord bot with LangGraph and Composio.</p>
<p>You’ll use LangGraph nodes to build a branching flow that processes incoming messages and detects intent like chat, support, or tool usage. It’ll then route them to the right logic based on what the user says.</p>
<p>I know it may sound a bit weird to use LangGraph for a Discord bot, but you’ll soon see that this project is a pretty fun way to visualize how node-based AI workflows actually run.</p>
<p>For now, the workflow is simple: you’ll figure out if the user is just chatting, asking a support question, or requesting that the bot perform an action, and respond based on that.</p>
<p><strong>What you will learn:</strong> 👀</p>
<ul>
<li><p>How to use LangGraph to create an AI-driven workflow that powers your bot’s logic.</p>
</li>
<li><p>How you can integrate Composio to let your bot take real-world actions using external tools.</p>
</li>
<li><p>How you can use Discord.js and handle different message types like replies, threads, and embeds.</p>
</li>
<li><p>How you can maintain per-channel context using message history and pass it into AI.</p>
</li>
</ul>
<p>By the end of this article, you’ll have a quite decent and functional Discord bot that you can add to your server. It replies to users based on message context and even has tool-calling support! (And there’s a small challenge for you to implement something yourself.) 😉</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Make sure you have Discord installed on your machine so you can test the bot easily.</p>
<p>This project is designed to demonstrate how you can build a bot powered by LangGraph and Composio. Before proceeding, it is helpful to have a basic understanding of:</p>
<ul>
<li><p>How to work with Node.js</p>
</li>
<li><p>Rough idea of what LangGraph is and how it works</p>
</li>
<li><p>How to work with Discord.js</p>
</li>
<li><p>What AI Agents are</p>
</li>
</ul>
<p>If you’re not confident about any of these, try following along anyway. You might pick things up just fine. And if it ever gets confusing, you can always check out the full source code <a target="_blank" href="https://github.com/shricodev/discord-bot-langgraph-composio">here</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-the-environment">How to Set Up the Environment</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-initialize-the-project">Initialize the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-install-dependencies">Install Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-composio">Configure Composio</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-discord-integration">Configure Discord Integration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-environment-variables">Add Environment Variables</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-build-the-application-logic">Build the Application Logic</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-define-types-and-utility-helpers">Define Types and Utility Helpers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implement-langgraph-workflow">Implement LangGraph Workflow</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-discordjs-client">Set Up Discord.js Client</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-how-to-set-up-the-environment">How to Set Up the Environment</h2>
<p>In this section, we will get everything set up for building the project.</p>
<h3 id="heading-initialize-the-project">Initialize the Project</h3>
<p>Initialize a Node.js application with the following command:</p>
<p>💁 Here I'm using Bun, but you can choose any package manager of your choice.</p>
<pre><code class="lang-bash">mkdir discord-bot-langgraph &amp;&amp; <span class="hljs-built_in">cd</span> discord-bot-langgraph \
&amp;&amp; bun init -y
</code></pre>
<p>Now, that our Node.js application is ready, let's install some dependencies.</p>
<h3 id="heading-install-dependencies">Install Dependencies</h3>
<p>We'll be using the following main packages and some other helper packages:</p>
<ul>
<li><p><a target="_blank" href="https://discord.js.org">discord.js</a>: Interacts with the Discord API</p>
</li>
<li><p><a target="_blank" href="https://composio.dev">composio</a>: Adds tools integration support to the bot</p>
</li>
<li><p><a target="_blank" href="https://platform.openai.com">openai</a>: Enables AI-powered responses</p>
</li>
<li><p><a target="_blank" href="https://www.langchain.com">langchain</a>: Manages LLM workflows</p>
</li>
<li><p><a target="_blank" href="https://zod.dev">zod</a>: Validates and parses data safely</p>
</li>
</ul>
<pre><code class="lang-bash">bun add discord.js openai @langchain/core @langchain/langgraph \
langchain composio-core dotenv zod uuid
</code></pre>
<h3 id="heading-configure-composio">Configure Composio</h3>
<p>💁 You’ll use Composio to add integrations to your application. You can choose the integration of your choice, but here I'm using Google sheets.</p>
<p>First, before moving forward, you need to get access to a Composio API key.</p>
<p>Go ahead and create an account on Composio, get your API key, and paste it in the <code>.env</code> file in the root of the project:</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkr1pys0txedp9vam4tt.png" alt="Composio dashboard" width="1920" height="972" loading="lazy"></p>
<pre><code class="lang-ini"><span class="hljs-attr">COMPOSIO_API_KEY</span>=&lt;your_composio_api_key&gt;
</code></pre>
<p>Authenticate yourself with the following command:</p>
<pre><code class="lang-bash">composio login
</code></pre>
<p>Once that’s done, run the <code>composio whoami</code> command, and if you see something like the below, you’re successfully logged in.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifzbkw6u6bwnj68lwqxt.png" alt="Output of the `composio whoami` command" width="1115" height="304" loading="lazy"></p>
<p>You're almost there: now you just need to set up integrations. Here, I’ll use Google sheets, but again you can set up any integration you like.</p>
<p>Run the following command to set up the Google Sheets integration:</p>
<pre><code class="lang-bash">composio add googlesheets
</code></pre>
<p>You should see an output similar to this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750336813743/9079ef2b-dc2a-4b10-b001-50e4cf98f3c5.png" alt="Add Composio Google Sheets integration" class="image--center mx-auto" width="1457" height="384" loading="lazy"></p>
<p>Head over to the URL that’s shown, and you should be authenticated like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750325571006/b0864445-7471-471f-88eb-f2ec8d832b39.png" alt="Composio authentication success" class="image--center mx-auto" width="1916" height="947" loading="lazy"></p>
<p>That's it. You’ve successfully added the Google Sheets integration and can access all its tools in your application.</p>
<p>Once finished, run the <code>composio integrations</code> command to verify if it worked. You should see a list of all your integrations:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750325653419/4585b63a-5581-4102-92e4-a55dca018063.png" alt="Composio list of integrations" class="image--center mx-auto" width="1175" height="268" loading="lazy"></p>
<h3 id="heading-configure-discord-integration">Configure Discord Integration</h3>
<p>This is a bit off topic for this tutorial, but basically, you’ll create an application/bot on Discord and add it to your server.</p>
<p>You can find a guide on how to create and add a bot to your server in the <a target="_blank" href="https://discordjs.guide/preparations/adding-your-bot-to-servers.html#bot-invite-links">Discord.js</a> documentation.</p>
<p>And yes, it’s free if you’re wondering whether any step here requires a pro account or anything. 😉</p>
<p>Make sure you populate these three environment variables:</p>
<pre><code class="lang-ini"><span class="hljs-attr">DISCORD_BOT_TOKEN</span>=&lt;YOUR_DISCORD_BOT_TOKEN&gt;
<span class="hljs-attr">DISCORD_BOT_GUILD_ID</span>=&lt;YOUR_DISCORD_BOT_GUILD_ID&gt;
<span class="hljs-attr">DISCORD_BOT_CHANNEL_ID</span>=&lt;YOUR_DISCORD_BOT_CHANNEL_ID&gt;
</code></pre>
<h3 id="heading-add-environment-variables">Add Environment Variables</h3>
<p>You’ll require a few other environment variables, including the OpenAI API key, for the bot to work.</p>
<p>Your final <code>.env</code> file should look something like this:</p>
<pre><code class="lang-ini"><span class="hljs-attr">OPENAI_API_KEY</span>=&lt;YOUR_OPENAI_API_KEY&gt;

<span class="hljs-attr">COMPOSIO_API_KEY</span>=&lt;YOUR_COMPOSIO_API_KEY&gt;

<span class="hljs-attr">DISCORD_BOT_TOKEN</span>=&lt;YOUR_DISCORD_BOT_TOKEN&gt;
<span class="hljs-attr">DISCORD_BOT_GUILD_ID</span>=&lt;YOUR_DISCORD_BOT_GUILD_ID&gt;
<span class="hljs-attr">DISCORD_BOT_CHANNEL_ID</span>=&lt;YOUR_DISCORD_BOT_CHANNEL_ID&gt;
</code></pre>
<h2 id="heading-build-the-application-logic">Build the Application Logic</h2>
<p>Now that you’ve laid all the groundwork, you can finally start coding the project.</p>
<h3 id="heading-define-types-and-utility-helpers">Define Types and Utility Helpers</h3>
<p>Let’s start by writing some helper functions and defining the types of data you’ll be working with.</p>
<p>It's important in any application, especially ones like the one we're building – which is prone to errors due to multiple API calls – that we set up decent logging so we know when and how things go wrong.</p>
<p>Create a new file named <code>logger.ts</code> inside the <code>utils</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/utils/logger.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DEBUG = <span class="hljs-string">"DEBUG"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> INFO = <span class="hljs-string">"INFO"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> WARN = <span class="hljs-string">"WARN"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ERROR = <span class="hljs-string">"ERROR"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> LogLevel = <span class="hljs-keyword">typeof</span> DEBUG | <span class="hljs-keyword">typeof</span> INFO | <span class="hljs-keyword">typeof</span> WARN | <span class="hljs-keyword">typeof</span> ERROR;

<span class="hljs-comment">// eslint-disable-next-line  @typescript-eslint/no-explicit-any</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">log</span>(<span class="hljs-params">level: LogLevel, message: <span class="hljs-built_in">string</span>, ...data: <span class="hljs-built_in">any</span>[]</span>) </span>{
  <span class="hljs-keyword">const</span> timestamp = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toLocaleString();
  <span class="hljs-keyword">const</span> prefix = <span class="hljs-string">`[<span class="hljs-subst">${timestamp}</span>] [<span class="hljs-subst">${level}</span>]`</span>;

  <span class="hljs-keyword">switch</span> (level) {
    <span class="hljs-keyword">case</span> ERROR:
      <span class="hljs-built_in">console</span>.error(prefix, message, ...data);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> WARN:
      <span class="hljs-built_in">console</span>.warn(prefix, message, ...data);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">default</span>:
      <span class="hljs-built_in">console</span>.log(prefix, message, ...data);
  }
}
</code></pre>
<p>This is already looking great. Why not write a small environment variables validator? Run this during the initial program startup, and if something goes wrong, the application will exit with clear logs so users know if any environment variables are missing.</p>
<p>Create a new file named <code>env-validator.ts</code> in the <code>utils</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/utils/env-validator.ts</span>

<span class="hljs-keyword">import</span> { log, ERROR } <span class="hljs-keyword">from</span> <span class="hljs-string">"./logger.js"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> OPENAI_API_KEY = <span class="hljs-string">"OPENAI_API_KEY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DISCORD_BOT_TOKEN = <span class="hljs-string">"DISCORD_BOT_TOKEN"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DISCORD_BOT_GUILD_ID = <span class="hljs-string">"DISCORD_BOT_GUILD_ID"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DISCORD_BOT_CLIENT_ID = <span class="hljs-string">"DISCORD_BOT_CLIENT_ID"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> COMPOSIO_API_KEY = <span class="hljs-string">"COMPOSIO_API_KEY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> validateEnvVars = (requiredEnvVars: <span class="hljs-built_in">string</span>[]): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> missingVars: <span class="hljs-built_in">string</span>[] = [];

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> envVar <span class="hljs-keyword">of</span> requiredEnvVars) {
    <span class="hljs-keyword">if</span> (!process.env[envVar]) {
      missingVars.push(envVar);
    }
  }

  <span class="hljs-keyword">if</span> (missingVars.length &gt; <span class="hljs-number">0</span>) {
    log(
      ERROR,
      <span class="hljs-string">"missing required environment variables. please create a .env file and add the following:"</span>,
    );
    missingVars.forEach(<span class="hljs-function">(<span class="hljs-params">envVar</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`- <span class="hljs-subst">${envVar}</span>`</span>));
    process.exit(<span class="hljs-number">1</span>);
  }
};
</code></pre>
<p>Now, let's also define the type of data you'll be working with:</p>
<p>Create a new file named <code>types.ts</code> inside the <code>types</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/types/types.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> QUESTION = <span class="hljs-string">"QUESTION"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> HELP = <span class="hljs-string">"HELP"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> SUPPORT = <span class="hljs-string">"SUPPORT"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> OTHER = <span class="hljs-string">"OTHER"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TOOL_CALL_REQUEST = <span class="hljs-string">"TOOL_CALL_REQUEST"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> FinalAction =
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY"</span>; content: <span class="hljs-built_in">string</span> }
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>; content: <span class="hljs-built_in">string</span> }
  | {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"CREATE_EMBED"</span>;
      title: <span class="hljs-built_in">string</span>;
      description: <span class="hljs-built_in">string</span>;
      roleToPing?: <span class="hljs-built_in">string</span>;
    };

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> MessageChoice =
  | <span class="hljs-keyword">typeof</span> SUPPORT
  | <span class="hljs-keyword">typeof</span> OTHER
  | <span class="hljs-keyword">typeof</span> TOOL_CALL_REQUEST;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> SupportTicketType = <span class="hljs-keyword">typeof</span> QUESTION | <span class="hljs-keyword">typeof</span> HELP;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Message = {
  author: <span class="hljs-built_in">string</span>;
  content: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> SupportTicketQuestion = {
  description: <span class="hljs-built_in">string</span>;
  answer: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> SupportTicket = {
  <span class="hljs-keyword">type</span>?: SupportTicketType;
  question?: SupportTicketQuestion;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ToolCallRequestAction = {
  <span class="hljs-comment">// actionLog is not intended to be shown to the end-user.</span>
  <span class="hljs-comment">// This is solely for logging purpose.</span>
  actionLog: <span class="hljs-built_in">string</span>;
  status: <span class="hljs-string">"success"</span> | <span class="hljs-string">"failed"</span> | <span class="hljs-string">"acknowledged"</span>;
};
</code></pre>
<p>The types are pretty self-explanatory, but here’s a quick overview.</p>
<p><code>Message</code> holds the user's input and author. Each message can be marked as support, a tool call request, or just other, like spam or small talk.</p>
<p>Support messages are further labeled as either help or a question using <code>SupportTicketType</code>.</p>
<p>The graph returns a <code>FinalAction</code>, which can be a direct reply, a reply in a thread, or an embed. If it's <code>CREATE_EMBED</code> and has <code>roleToPing</code> set, it denotes support help, so we can ping the mod.</p>
<p>For tool-based responses, <code>ToolCallRequestAction</code> stores the status and an internal log used for debugging.</p>
<p>Now, you need one last helper function to use in your nodes to extract the response from the LLM. Create a new file named <code>helpers.ts</code> and add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/utils/helpers.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AIMessage } <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/core/messages"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extractStringFromAIMessage</span>(<span class="hljs-params">
  message: AIMessage,
  fallback: <span class="hljs-built_in">string</span> = "No valid response generated by the LLM.",
</span>): <span class="hljs-title">string</span> </span>{
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> message.content === <span class="hljs-string">"string"</span>) {
    <span class="hljs-keyword">return</span> message.content;
  }

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Array</span>.isArray(message.content)) {
    <span class="hljs-keyword">const</span> textContent = message.content
      .map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> (<span class="hljs-keyword">typeof</span> item === <span class="hljs-string">"string"</span> ? item : <span class="hljs-string">""</span>))
      .join(<span class="hljs-string">" "</span>);
    <span class="hljs-keyword">return</span> textContent.trim() || fallback;
  }

  <span class="hljs-keyword">return</span> fallback;
}
</code></pre>
<p>You're all set for now with these helper functions in place. Now, you can start coding the logic.</p>
<h3 id="heading-implement-langgraph-workflow">Implement LangGraph Workflow</h3>
<p>Now that you have the types defined, structure your graph and connect it with some edges.</p>
<p>Create a new file named <code>graph.ts</code> inside the <code>src</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/src/graph.ts</span>

<span class="hljs-keyword">import</span> { Annotation, END, START, StateGraph } <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/langgraph"</span>;
<span class="hljs-keyword">import</span> {
  <span class="hljs-keyword">type</span> FinalAction,
  <span class="hljs-keyword">type</span> ToolCallRequestAction,
  <span class="hljs-keyword">type</span> Message,
  <span class="hljs-keyword">type</span> MessageChoice,
  <span class="hljs-keyword">type</span> SupportTicket,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/types.js"</span>;
<span class="hljs-keyword">import</span> {
  processToolCall,
  processMessage,
  processOther,
  processSupport,
  processSupportHelp,
  processSupportQuestion,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"./nodes.js"</span>;
<span class="hljs-keyword">import</span> { processMessageEdges, processSupportEdges } <span class="hljs-keyword">from</span> <span class="hljs-string">"./edges.js"</span>;

<span class="hljs-keyword">const</span> state = Annotation.Root({
  message: Annotation&lt;Message&gt;(),
  previousMessages: Annotation&lt;Message[]&gt;(),
  messageChoice: Annotation&lt;MessageChoice&gt;(),
  supportTicket: Annotation&lt;SupportTicket&gt;(),
  toolCallRequest: Annotation&lt;ToolCallRequestAction&gt;(),
  finalAction: Annotation&lt;FinalAction&gt;(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> State = <span class="hljs-keyword">typeof</span> state.State;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Update = <span class="hljs-keyword">typeof</span> state.Update;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initializeGraph</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> workflow = <span class="hljs-keyword">new</span> StateGraph(state);

  workflow
    .addNode(<span class="hljs-string">"process-message"</span>, processMessage)
    .addNode(<span class="hljs-string">"process-support"</span>, processSupport)
    .addNode(<span class="hljs-string">"process-other"</span>, processOther)

    .addNode(<span class="hljs-string">"process-support-question"</span>, processSupportQuestion)
    .addNode(<span class="hljs-string">"process-support-help"</span>, processSupportHelp)
    .addNode(<span class="hljs-string">"process-tool-call"</span>, processToolCall)

    <span class="hljs-comment">// Edges setup starts here....</span>
    .addEdge(START, <span class="hljs-string">"process-message"</span>)

    .addConditionalEdges(<span class="hljs-string">"process-message"</span>, processMessageEdges)
    .addConditionalEdges(<span class="hljs-string">"process-support"</span>, processSupportEdges)

    .addEdge(<span class="hljs-string">"process-other"</span>, END)
    .addEdge(<span class="hljs-string">"process-support-question"</span>, END)
    .addEdge(<span class="hljs-string">"process-support-help"</span>, END)
    .addEdge(<span class="hljs-string">"process-tool-call"</span>, END);

  <span class="hljs-keyword">const</span> graph = workflow.compile();

  <span class="hljs-comment">// To get the graph in png</span>
  <span class="hljs-comment">// getGraph() is deprecated though</span>
  <span class="hljs-comment">// Bun.write("graph/graph.png", await graph.getGraph().drawMermaidPng());</span>

  <span class="hljs-keyword">return</span> graph;
}
</code></pre>
<p>The <code>initializeGraph</code> function, as the name suggests, returns the graph you can use to execute the workflow.</p>
<p>The <code>process-message</code> node is the starting point of the graph. It takes in the user’s message, processes it, and routes it to the appropriate next node: <code>process-support</code>, <code>process-tool-call</code>, or <code>process-other</code>.</p>
<p>The <code>process-support</code> node further classifies the support message and decides whether it should go to <code>process-support-help</code> or <code>process-support-question</code>.</p>
<p>The <code>process-tool-call</code> node handles messages when the user tries to trigger some kind of tool or action.</p>
<p>The <code>process-other</code> node handles everything that doesn’t fall into the support or tool call categories. These are general or fallback responses.</p>
<p>To help you visualize how things will shape up, here’s how the graph looks with all the different nodes (yet to work on!):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750327093884/fa8e6b4e-ca61-4900-9b3b-7b3a2863c296.png" alt="LangGraph nodes for the Discord bot workflow" class="image--center mx-auto" width="886" height="432" loading="lazy"></p>
<p>To wire everything together, you need to define edges between nodes, including conditional edges that dynamically decide the next step based on the state.</p>
<p>Create a new file named <code>edges.ts</code> inside the <code>src</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/src/edges.ts</span>

<span class="hljs-keyword">import</span> { END } <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/langgraph"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> State } <span class="hljs-keyword">from</span> <span class="hljs-string">"./graph.js"</span>;
<span class="hljs-keyword">import</span> { QUESTION, OTHER, SUPPORT, TOOL_CALL_REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/types.js"</span>;
<span class="hljs-keyword">import</span> { log, WARN } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/logger.js"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processMessageEdges = (
  state: State,
): <span class="hljs-string">"process-support"</span> | <span class="hljs-string">"process-other"</span> | <span class="hljs-string">"process-tool-call"</span> | <span class="hljs-string">"__end__"</span> =&gt; {
  <span class="hljs-keyword">if</span> (!state.messageChoice) {
    log(WARN, <span class="hljs-string">"state.messageChoice is undefined. Returning..."</span>);
    <span class="hljs-keyword">return</span> END;
  }

  <span class="hljs-keyword">switch</span> (state.messageChoice) {
    <span class="hljs-keyword">case</span> SUPPORT:
      <span class="hljs-keyword">return</span> <span class="hljs-string">"process-support"</span>;
    <span class="hljs-keyword">case</span> TOOL_CALL_REQUEST:
      <span class="hljs-keyword">return</span> <span class="hljs-string">"process-tool-call"</span>;
    <span class="hljs-keyword">case</span> OTHER:
      <span class="hljs-keyword">return</span> <span class="hljs-string">"process-other"</span>;
    <span class="hljs-keyword">default</span>:
      log(WARN, <span class="hljs-string">"unknown message choice. Returning..."</span>);
      <span class="hljs-keyword">return</span> END;
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processSupportEdges = (
  state: State,
): <span class="hljs-string">"process-support-question"</span> | <span class="hljs-string">"process-support-help"</span> | <span class="hljs-string">"__end__"</span> =&gt; {
  <span class="hljs-keyword">if</span> (!state.supportTicket?.type) {
    log(WARN, <span class="hljs-string">"state.supportTicket.type is undefined. Returning..."</span>);
    <span class="hljs-keyword">return</span> END;
  }

  <span class="hljs-keyword">return</span> state.supportTicket.type === QUESTION
    ? <span class="hljs-string">"process-support-question"</span>
    : <span class="hljs-string">"process-support-help"</span>;
};
</code></pre>
<p>These are the edges that connect different nodes in your application. They direct the flow in your graph.</p>
<p>Things are really shaping up – so let’s finish the core logic by implementing all the nodes for your application.</p>
<p>Create a new file named <code>nodes.ts</code> inside the <code>src</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/src/nodes.ts</span>

<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> State, <span class="hljs-keyword">type</span> Update } <span class="hljs-keyword">from</span> <span class="hljs-string">"./graph.js"</span>;
<span class="hljs-keyword">import</span> { ChatOpenAI } <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/openai"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> {
  HELP,
  TOOL_CALL_REQUEST,
  OTHER,
  QUESTION,
  SUPPORT,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/types.js"</span>;
<span class="hljs-keyword">import</span> { extractStringFromAIMessage } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/helpers.js"</span>;
<span class="hljs-keyword">import</span> { OpenAIToolSet } <span class="hljs-keyword">from</span> <span class="hljs-string">"composio-core"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ChatCompletionMessageToolCall } <span class="hljs-keyword">from</span> <span class="hljs-string">"openai/resources/chat/completions.mjs"</span>;
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { DEBUG, ERROR, INFO, log, WARN } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/logger.js"</span>;
<span class="hljs-keyword">import</span> {
  SystemMessage,
  HumanMessage,
  ToolMessage,
  BaseMessage,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@langchain/core/messages"</span>;

<span class="hljs-comment">// feel free to use any model. Here I'm going with gpt-4o-mini</span>
<span class="hljs-keyword">const</span> model = <span class="hljs-string">"gpt-4o-mini"</span>;

<span class="hljs-keyword">const</span> toolset = <span class="hljs-keyword">new</span> OpenAIToolSet();
<span class="hljs-keyword">const</span> llm = <span class="hljs-keyword">new</span> ChatOpenAI({
  model,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: <span class="hljs-number">0</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processMessage = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in process message:"</span>, state.message);

  <span class="hljs-keyword">const</span> llm = <span class="hljs-keyword">new</span> ChatOpenAI({
    model,
    apiKey: process.env.OPENAI_API_KEY,
    temperature: <span class="hljs-number">0</span>,
  });

  <span class="hljs-keyword">const</span> structuredLlm = llm.withStructuredOutput(
    z.object({
      <span class="hljs-keyword">type</span>: z.enum([SUPPORT, OTHER, TOOL_CALL_REQUEST]).describe(<span class="hljs-string">`
Categorize the user's message:
- <span class="hljs-subst">${SUPPORT}</span>: Technical support, help with problems, or questions about AI.
- <span class="hljs-subst">${TOOL_CALL_REQUEST}</span>: User asks the bot to perform tool action (e.g., "send an email", "summarize chat", "summarize google sheets").
- <span class="hljs-subst">${OTHER}</span>: General conversation, spam, or off-topic messages.
`</span>),
    }),
  );

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> structuredLlm.invoke([
    [
      <span class="hljs-string">"system"</span>,
      <span class="hljs-string">`You are an expert message analyzer AI. You need to categorize the message into
one of these categories:

- <span class="hljs-subst">${SUPPORT}</span>: If the message asks for technical support, help with a problem, or questions about AIs and LLMs.
- <span class="hljs-subst">${TOOL_CALL_REQUEST}</span>: If the message is a direct command or request for the bot to perform an action using external tools/services. Examples: "Summarize a document or Google Sheet", "Summarize the last hour of chat", "Send an email to devteam about this bug", "Create a Trello card for this feature request". Prioritize this if the user is asking the bot to *do* something beyond just answering.
- <span class="hljs-subst">${OTHER}</span>: For general chit-chat, spam, off-topic messages, or anything not fitting <span class="hljs-subst">${SUPPORT}</span> or <span class="hljs-subst">${TOOL_CALL_REQUEST}</span>.
`</span>,
    ],
    [<span class="hljs-string">"human"</span>, state.message.content],
  ]);

  <span class="hljs-keyword">return</span> {
    messageChoice: res.type,
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processSupport = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in support:"</span>, state.message);

  <span class="hljs-keyword">const</span> llm = <span class="hljs-keyword">new</span> ChatOpenAI({
    model,
    apiKey: process.env.OPENAI_API_KEY,
    temperature: <span class="hljs-number">0</span>,
  });

  <span class="hljs-keyword">const</span> structuredLlm = llm.withStructuredOutput(
    z.object({
      <span class="hljs-keyword">type</span>: z.enum([QUESTION, HELP]).describe(<span class="hljs-string">`
Type of support needed:
- <span class="hljs-subst">${QUESTION}</span>: User asks a specific question seeking information or an answer.
- <span class="hljs-subst">${HELP}</span>: User needs broader assistance, guidance, or reports an issue requiring intervention/troubleshooting.
`</span>),
    }),
  );

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> structuredLlm.invoke([
    [
      <span class="hljs-string">"system"</span>,
      <span class="hljs-string">`
You are a support ticket analyzer. Given a support message, categorize it as <span class="hljs-subst">${QUESTION}</span> or <span class="hljs-subst">${HELP}</span>.
- <span class="hljs-subst">${QUESTION}</span>: For specific questions.
- <span class="hljs-subst">${HELP}</span>: For requests for assistance, troubleshooting, or problem reports.
`</span>,
    ],
    [<span class="hljs-string">"human"</span>, state.message.content],
  ]);

  <span class="hljs-keyword">return</span> {
    supportTicket: {
      ...state.supportTicket,
      <span class="hljs-keyword">type</span>: res.type,
    },
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processSupportHelp = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in support help:"</span>, state.message);

  <span class="hljs-keyword">return</span> {
    supportTicket: {
      ...state.supportTicket,
    },
    finalAction: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"CREATE_EMBED"</span>,
      title: <span class="hljs-string">"🚨 Help Needed!"</span>,
      description: <span class="hljs-string">`A new request for help has been raised by **@<span class="hljs-subst">${state.message.author}</span>**.\n\n**Query:**\n&gt; <span class="hljs-subst">${state.message.content}</span>`</span>,
      roleToPing: process.env.DISCORD_SUPPORT_MOD_ID,
    },
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processSupportQuestion = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in support question category:"</span>, state.message);

  <span class="hljs-keyword">const</span> llm = <span class="hljs-keyword">new</span> ChatOpenAI({
    model,
    apiKey: process.env.OPENAI_API_KEY,
    temperature: <span class="hljs-number">0</span>,
  });

  <span class="hljs-keyword">const</span> systemPrompt = <span class="hljs-string">`
You are a helpful AI assistant specializing in AI, and LLMs. Answer
the user's question concisely and accurately based on general knowledge in
these areas. If the question is outside this scope (e.g., personal advice,
non-technical topics), politely state you cannot answer. User's question:
`</span>;

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> llm.invoke([
    [<span class="hljs-string">"system"</span>, systemPrompt],
    [<span class="hljs-string">"human"</span>, state.message.content],
  ]);

  <span class="hljs-keyword">const</span> llmResponse = extractStringFromAIMessage(res);
  <span class="hljs-keyword">return</span> {
    supportTicket: {
      ...state.supportTicket,
      question: {
        description: state.message.content,
        answer: llmResponse,
      },
    },
    finalAction: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY"</span>,
      content: llmResponse,
    },
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processOther = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in other category:"</span>, state.message);

  <span class="hljs-keyword">const</span> response =
    <span class="hljs-string">"This seems to be a general message. I'm here to help with technical support or perform specific actions if you ask. How can I assist you with those?"</span>;

  <span class="hljs-keyword">return</span> {
    finalAction: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>,
      content: response,
    },
  };
};
</code></pre>
<p>There’s not much to explain for these nodes. Each node in the flow functions as a message classifier. It spins up a Chat LLM instance and uses structured output to ensure the model returns a specific label from a predefined set like <code>QUESTION</code> or <code>HELP</code> for support messages. The system prompt clearly defines what each label means, and your user message is passed in for classification.</p>
<p>You’re almost there. But there’s one piece missing. Can you spot it?</p>
<p>The <code>process-tool-call</code> node that’s supposed to handle the workflow when the user asks to use a tool. This is a big piece of the workflow.</p>
<p>It’s a bit longer, so I’ll explain it separately.</p>
<p>Modify the above <code>nodes.ts</code> file to add the missing node:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/src/nodes.ts</span>

<span class="hljs-comment">// Rest of the code...</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> processToolCall = <span class="hljs-keyword">async</span> (state: State): <span class="hljs-built_in">Promise</span>&lt;Update&gt; =&gt; {
  log(DEBUG, <span class="hljs-string">"message in tool call request category:"</span>, state.message);

  <span class="hljs-keyword">const</span> structuredOutputType = z.object({
    service: z
      .string()
      .describe(<span class="hljs-string">"The target service (e.g., 'email', 'discord')."</span>),
    task: z
      .string()
      .describe(
        <span class="hljs-string">"A concise description of the task (e.g., 'send email to X', 'summarize recent chat', 'create task Y')."</span>,
      ),
    details: z
      .string()
      .optional()
      .describe(
        <span class="hljs-string">"Any specific details or parameters extracted from the message relevant to the task."</span>,
      ),
  });

  <span class="hljs-keyword">const</span> structuredLlm = llm.withStructuredOutput(structuredOutputType);

  <span class="hljs-keyword">let</span> parsedActionDetails: z.infer&lt;<span class="hljs-keyword">typeof</span> structuredOutputType&gt; = {
    service: <span class="hljs-string">"unknown"</span>,
    task: <span class="hljs-string">"perform a requested action"</span>,
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> structuredLlm.invoke([
      [
        <span class="hljs-string">"system"</span>,
        <span class="hljs-string">`Parse the user's request to identify an action. Extract the target service, a description of the task, and any relevant details or parameters.
      Examples:
      - "Remind me to check emails at 5 PM": service: calendar/reminder, task: set reminder, details: check emails at 5 PM
      - "Send a summary of this conversation to #general channel": service: discord, task: send summary to channel, details: channel #general
      - "Create a bug report for 'login fails on mobile'": service: project_manager, task: create bug report, details: title 'login fails on mobile'`</span>,
      ],
      [<span class="hljs-string">"human"</span>, state.message.content],
    ]);

    parsedActionDetails = res;
    log(INFO, <span class="hljs-string">"initial parsing action details:"</span>, parsedActionDetails);
  } <span class="hljs-keyword">catch</span> (error) {
    log(ERROR, <span class="hljs-string">"initial parsing error:"</span>, error);
    <span class="hljs-keyword">return</span> {
      toolCallRequest: {
        actionLog: <span class="hljs-string">`Failed to parse user request: <span class="hljs-subst">${state.message.content}</span>`</span>,
        status: <span class="hljs-string">"failed"</span>,
      },
      finalAction: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>,
        content:
          <span class="hljs-string">"I'm sorry, I had trouble understanding that action. Could you please rephrase it?"</span>,
      },
    };
  }

  <span class="hljs-keyword">try</span> {
    log(INFO, <span class="hljs-string">"fetching composio tools"</span>);
    <span class="hljs-keyword">const</span> tools = <span class="hljs-keyword">await</span> toolset.getTools({
      apps: [<span class="hljs-string">"GOOGLESHEETS"</span>],
    });

    log(INFO, <span class="hljs-string">`fetched <span class="hljs-subst">${tools.length}</span> tools. Errors if &gt; 128 for OpenAI:`</span>);

    <span class="hljs-keyword">if</span> (tools.length === <span class="hljs-number">0</span>) {
      log(WARN, <span class="hljs-string">"no tools fetched from Composio. skipping..."</span>);
      <span class="hljs-keyword">return</span> {
        toolCallRequest: {
          actionLog: <span class="hljs-string">`Service: <span class="hljs-subst">${parsedActionDetails.service}</span>, Task: <span class="hljs-subst">${parsedActionDetails.task}</span>. No composio tools found`</span>,
          status: <span class="hljs-string">"failed"</span>,
        },
        finalAction: {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>,
          content: <span class="hljs-string">"Couldn't find any tools to perform your action."</span>,
        },
      };
    }

    log(DEBUG, <span class="hljs-string">"starting iterative tool execution loop"</span>);

    <span class="hljs-keyword">const</span> conversationHistory: BaseMessage[] = [
      <span class="hljs-keyword">new</span> SystemMessage(
        <span class="hljs-string">"You are a helpful assistant that performs tool calls. Your task is to understand the user's request and use the available tools to fulfill the request completely. You can use multiple tools in sequence to accomplish complex tasks. Always provide a brief, conversational summary of what you accomplished after using tools."</span>,
      ),
      <span class="hljs-keyword">new</span> HumanMessage(state.message.content),
    ];

    <span class="hljs-keyword">let</span> totalToolsUsed = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">let</span> finalResponse: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">const</span> maxIterations = <span class="hljs-number">5</span>;
    <span class="hljs-keyword">let</span> iteration = <span class="hljs-number">0</span>;

    <span class="hljs-keyword">while</span> (iteration &lt; maxIterations) {
      iteration++;
      log(
        DEBUG,
        <span class="hljs-string">`Iteration <span class="hljs-subst">${iteration}</span>: calling LLM with <span class="hljs-subst">${tools.length}</span> tools`</span>,
      );

      <span class="hljs-keyword">const</span> llmResponse = <span class="hljs-keyword">await</span> llm.invoke(conversationHistory, {
        tools: tools,
      });

      log(DEBUG, <span class="hljs-string">`Iteration <span class="hljs-subst">${iteration}</span> LLM response:`</span>, llmResponse);

      <span class="hljs-keyword">const</span> toolCalls = llmResponse.tool_calls;

      <span class="hljs-keyword">if</span> ((!toolCalls || toolCalls.length === <span class="hljs-number">0</span>) &amp;&amp; llmResponse.content) {
        finalResponse =
          <span class="hljs-keyword">typeof</span> llmResponse.content === <span class="hljs-string">"string"</span>
            ? llmResponse.content
            : <span class="hljs-built_in">JSON</span>.stringify(llmResponse.content);
        log(
          INFO,
          <span class="hljs-string">`Final response received after <span class="hljs-subst">${iteration}</span> iterations:`</span>,
          finalResponse,
        );
        <span class="hljs-keyword">break</span>;
      }

      <span class="hljs-keyword">if</span> (toolCalls &amp;&amp; toolCalls.length &gt; <span class="hljs-number">0</span>) {
        log(
          INFO,
          <span class="hljs-string">`Iteration <span class="hljs-subst">${iteration}</span>: executing <span class="hljs-subst">${toolCalls.length}</span> tool(s)`</span>,
        );
        totalToolsUsed += toolCalls.length;

        conversationHistory.push(llmResponse);

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> toolCall <span class="hljs-keyword">of</span> toolCalls) {
          log(
            INFO,
            <span class="hljs-string">`Executing tool: <span class="hljs-subst">${toolCall.name}</span> with args:`</span>,
            toolCall.args,
          );

          <span class="hljs-keyword">const</span> composioCompatibleToolCall: ChatCompletionMessageToolCall = {
            id: toolCall.id || uuidv4(),
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"function"</span>,
            <span class="hljs-function"><span class="hljs-keyword">function</span>: </span>{
              name: toolCall.name,
              <span class="hljs-built_in">arguments</span>: <span class="hljs-built_in">JSON</span>.stringify(toolCall.args),
            },
          };

          <span class="hljs-keyword">let</span> toolOutputContent: <span class="hljs-built_in">string</span>;
          <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> executionResult = <span class="hljs-keyword">await</span> toolset.executeToolCall(
              composioCompatibleToolCall,
            );
            log(
              INFO,
              <span class="hljs-string">`Tool <span class="hljs-subst">${toolCall.name}</span> execution result:`</span>,
              executionResult,
            );
            toolOutputContent = <span class="hljs-built_in">JSON</span>.stringify(executionResult);
          } <span class="hljs-keyword">catch</span> (toolError) {
            log(ERROR, <span class="hljs-string">`Tool <span class="hljs-subst">${toolCall.name}</span> execution error:`</span>, toolError);
            <span class="hljs-keyword">const</span> errorMessage =
              toolError <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span>
                ? toolError.message
                : <span class="hljs-built_in">String</span>(toolError);

            toolOutputContent = <span class="hljs-string">`Error: <span class="hljs-subst">${errorMessage}</span>`</span>;
          }

          conversationHistory.push(
            <span class="hljs-keyword">new</span> ToolMessage({
              content: toolOutputContent,
              tool_call_id: toolCall.id || uuidv4(),
            }),
          );
        }

        <span class="hljs-keyword">continue</span>;
      }

      log(
        WARN,
        <span class="hljs-string">`Iteration <span class="hljs-subst">${iteration}</span>: LLM provided no tool calls or content`</span>,
      );
      <span class="hljs-keyword">break</span>;
    }

    <span class="hljs-keyword">let</span> userFriendlyResponse: <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">if</span> (totalToolsUsed &gt; <span class="hljs-number">0</span>) {
      log(DEBUG, <span class="hljs-string">"Generating user-friendly summary using LLM"</span>);

      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> summaryResponse = <span class="hljs-keyword">await</span> llm.invoke([
          <span class="hljs-keyword">new</span> SystemMessage(
            <span class="hljs-string">"You are tasked with creating a brief, friendly summary for a Discord user about what actions were just completed. Keep it conversational, under 2-3 sentences, and focus on what was accomplished rather than technical details. Start with phrases like 'Done!', 'Successfully completed', 'All set!', etc."</span>,
          ),
          <span class="hljs-keyword">new</span> HumanMessage(
            <span class="hljs-string">`The user requested: "<span class="hljs-subst">${state.message.content}</span>"

I used <span class="hljs-subst">${totalToolsUsed}</span> tools across <span class="hljs-subst">${iteration}</span> iterations to complete their request. <span class="hljs-subst">${finalResponse ? <span class="hljs-string">`My final response was: <span class="hljs-subst">${finalResponse}</span>`</span> : <span class="hljs-string">"The task was completed successfully."</span>}</span>

Generate a brief, friendly summary of what was accomplished.`</span>,
          ),
        ]);

        userFriendlyResponse =
          <span class="hljs-keyword">typeof</span> summaryResponse.content === <span class="hljs-string">"string"</span>
            ? summaryResponse.content
            : <span class="hljs-string">`Done! I've completed your request using <span class="hljs-subst">${totalToolsUsed}</span> action<span class="hljs-subst">${totalToolsUsed &gt; <span class="hljs-number">1</span> ? <span class="hljs-string">"s"</span> : <span class="hljs-string">""</span>}</span>.`</span>;

        log(INFO, <span class="hljs-string">"Generated user-friendly summary:"</span>, userFriendlyResponse);
      } <span class="hljs-keyword">catch</span> (summaryError) {
        log(ERROR, <span class="hljs-string">"Failed to generate summary:"</span>, summaryError);
        userFriendlyResponse = <span class="hljs-string">`All set! I've completed your request using <span class="hljs-subst">${totalToolsUsed}</span> action<span class="hljs-subst">${totalToolsUsed &gt; <span class="hljs-number">1</span> ? <span class="hljs-string">"s"</span> : <span class="hljs-string">""</span>}</span>.`</span>;
      }
    } <span class="hljs-keyword">else</span> {
      userFriendlyResponse =
        finalResponse ||
        <span class="hljs-string">`I understood your request about '<span class="hljs-subst">${parsedActionDetails.task}</span>' but couldn't find the right tools to complete it.`</span>;
    }

    <span class="hljs-keyword">const</span> actionLog = <span class="hljs-string">`Service: <span class="hljs-subst">${parsedActionDetails.service}</span>, Task: <span class="hljs-subst">${parsedActionDetails.task}</span>. Used <span class="hljs-subst">${totalToolsUsed}</span> tools across <span class="hljs-subst">${iteration}</span> iterations.`</span>;

    <span class="hljs-keyword">return</span> {
      toolCallRequest: {
        actionLog,
        status: totalToolsUsed &gt; <span class="hljs-number">0</span> ? <span class="hljs-string">"success"</span> : <span class="hljs-string">"acknowledged"</span>,
      },
      finalAction: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>,
        content: userFriendlyResponse,
      },
    };
  } <span class="hljs-keyword">catch</span> (error) {
    log(ERROR, <span class="hljs-string">"processing tool call with Composio:"</span>, error);
    <span class="hljs-keyword">const</span> errorMessage = error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? error.message : <span class="hljs-built_in">String</span>(error);

    <span class="hljs-keyword">return</span> {
      toolCallRequest: {
        actionLog: <span class="hljs-string">`Error during tool call (Service: <span class="hljs-subst">${parsedActionDetails.service}</span>, Task: <span class="hljs-subst">${parsedActionDetails.task}</span>). Error: <span class="hljs-subst">${errorMessage}</span>`</span>,
        status: <span class="hljs-string">"failed"</span>,
      },
      finalAction: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"REPLY_IN_THREAD"</span>,
        content: <span class="hljs-string">"Sorry, I encountered an error while processing your request."</span>,
      },
    };
  }
};
</code></pre>
<p>The part up until the first try-catch block is the same. Up until then, you're figuring out the tool the user is trying to call. Now comes the juicy part: actually handling tool calls.</p>
<p>At this point, you need to fetch the tools from Composio. Here, I’m just passing in Google Sheets as the option for demo purposes, but you could use literally anything once you authenticate yourself as shown above.</p>
<p>After fetching the tools, you enter a loop where the LLM can use them. It reviews the conversation history and decides which tools to call. You execute these calls, feed the results back, and repeat for up to 5 iterations or until the LLM gives a final answer.</p>
<p>This loop runs up to 5 times as a safeguard so the LLM doesn’t get stuck in an endless back-and-forth.</p>
<p>If tools were used, you ask the LLM to write a friendly summary for the user instead of dumping the raw JSON response. If no tools worked or none matched, just let the user know you couldn’t perform the action.</p>
<p>Now with that, you’re done with the difficult part (I mean, it was pretty easy though, right?). From here on, you just need to set up and work with the Discord API using Discord.js.</p>
<h3 id="heading-set-up-discordjs-client">Set Up Discord.js Client</h3>
<p>In this application, you’re using slash commands. To use slash commands in Discord, you need to register them first. You can do this manually, but why not automate it as well? 😉</p>
<p>Create a new file named <code>slash-deploy.ts</code> inside the <code>utils</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/utils/slash-deploy.ts</span>

<span class="hljs-keyword">import</span> { REST, Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">"discord.js"</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
<span class="hljs-keyword">import</span> { log, INFO, ERROR } <span class="hljs-keyword">from</span> <span class="hljs-string">"./logger.js"</span>;
<span class="hljs-keyword">import</span> {
  DISCORD_BOT_TOKEN,
  DISCORD_BOT_GUILD_ID,
  OPENAI_API_KEY,
  DISCORD_BOT_CLIENT_ID,
  validateEnvVars,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"./env-validator.js"</span>;

dotenv.config();

<span class="hljs-keyword">const</span> requiredEnvVars = [
  DISCORD_BOT_TOKEN,
  DISCORD_BOT_GUILD_ID,
  DISCORD_BOT_CLIENT_ID,
  OPENAI_API_KEY,
];
validateEnvVars(requiredEnvVars);

<span class="hljs-keyword">const</span> commands = [
  {
    name: <span class="hljs-string">"ask"</span>,
    description: <span class="hljs-string">"Ask the AI assistant a question or give it a command."</span>,
    options: [
      {
        name: <span class="hljs-string">"prompt"</span>,
        <span class="hljs-keyword">type</span>: <span class="hljs-number">3</span>,
        description: <span class="hljs-string">"Your question or command for the bot"</span>,
        required: <span class="hljs-literal">true</span>,
      },
    ],
  },
];

<span class="hljs-keyword">const</span> rest = <span class="hljs-keyword">new</span> REST({ version: <span class="hljs-string">"10"</span> }).setToken(
  process.env.DISCORD_BOT_TOKEN!,
);

(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    log(INFO, <span class="hljs-string">"deploying slash(/) commands"</span>);
    <span class="hljs-keyword">await</span> rest.put(
      Routes.applicationGuildCommands(
        process.env.DISCORD_BOT_CLIENT_ID!,
        process.env.DISCORD_BOT_GUILD_ID!,
      ),
      {
        body: commands,
      },
    );

    log(INFO, <span class="hljs-string">"slash(/) commands deployed"</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    log(ERROR, <span class="hljs-string">"deploying slash(/) commands:"</span>, error);
  }
})();
</code></pre>
<p>See your <code>validateEnvVars</code> function in action? Here, you’re specifying the environment variables that must be set before running the program. If any are missing and you try to run the program, you’ll get an error.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750340614800/ce0b37bc-647c-4b94-9099-2e396b0ffa93.png" alt="Command failed output for deploying slash command to Discord" class="image--center mx-auto" width="1221" height="191" loading="lazy"></p>
<p>The way you deploy the slash commands to Discord is using the <code>REST</code> API provided by <code>discord.js</code>, specifically by calling <code>rest.put</code> with your command data and target guild.</p>
<p>Now, simply run the <code>commands:deploy</code> bun script and you should have <code>/ask</code> registered as a slash command in your Discord.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750340646555/2d5b22df-cd43-4e54-b985-b64576831316.png" alt="2d5b22df-cd43-4e54-b985-b64576831316" class="image--center mx-auto" width="1080" height="165" loading="lazy"></p>
<p>At this point, you should see the <code>/ask</code> slash command available in your server. All that’s left is to create the <code>index.ts</code> file, which will be the entry point to your Discord bot.</p>
<p>Create a new file named <code>index.ts</code> inside the <code>src</code> directory and add the following lines of code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇 discord-bot-langgraph/src/index.ts</span>

<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
<span class="hljs-keyword">import</span> {
  Client,
  Events,
  GatewayIntentBits,
  EmbedBuilder,
  <span class="hljs-keyword">type</span> Interaction,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"discord.js"</span>;
<span class="hljs-keyword">import</span> { initializeGraph } <span class="hljs-keyword">from</span> <span class="hljs-string">"./graph.js"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Message <span class="hljs-keyword">as</span> ChatMessage } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/types.js"</span>;
<span class="hljs-keyword">import</span> { ERROR, INFO, log } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/logger.js"</span>;
<span class="hljs-keyword">import</span> {
  DISCORD_BOT_TOKEN,
  DISCORD_BOT_GUILD_ID,
  OPENAI_API_KEY,
  validateEnvVars,
  DISCORD_BOT_CLIENT_ID,
  COMPOSIO_API_KEY,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/env-validator.js"</span>;

dotenv.config();

<span class="hljs-keyword">const</span> requiredEnvVars = [
  DISCORD_BOT_CLIENT_ID,
  DISCORD_BOT_TOKEN,
  DISCORD_BOT_GUILD_ID,

  OPENAI_API_KEY,

  COMPOSIO_API_KEY,
];
validateEnvVars(requiredEnvVars);

<span class="hljs-keyword">const</span> graph = initializeGraph();

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

<span class="hljs-comment">// use a map to store history per channel to make it work properly with all the</span>
<span class="hljs-comment">// channels and not for one specific channel.</span>
<span class="hljs-keyword">const</span> channelHistories = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, ChatMessage[]&gt;();

client.on(Events.ClientReady, <span class="hljs-keyword">async</span> (readyClient) =&gt; {
  log(INFO, <span class="hljs-string">`logged in as <span class="hljs-subst">${readyClient.user.tag}</span>. ready to process commands!`</span>);
});

client.on(Events.InteractionCreate, <span class="hljs-keyword">async</span> (interaction: Interaction) =&gt; {
  <span class="hljs-keyword">if</span> (!interaction.isChatInputCommand()) <span class="hljs-keyword">return</span>;
  <span class="hljs-keyword">if</span> (interaction.commandName !== <span class="hljs-string">"ask"</span>) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">const</span> userPrompt = interaction.options.getString(<span class="hljs-string">"prompt"</span>, <span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> user = interaction.user;
  <span class="hljs-keyword">const</span> channelId = interaction.channelId;

  <span class="hljs-keyword">if</span> (!channelHistories.has(channelId)) channelHistories.set(channelId, []);

  <span class="hljs-keyword">const</span> messageHistory = channelHistories.get(channelId)!;

  <span class="hljs-keyword">const</span> currentUserMessage: ChatMessage = {
    author: user.username,
    content: userPrompt,
  };

  <span class="hljs-keyword">const</span> graphInput = {
    message: currentUserMessage,
    previousMessages: [...messageHistory],
  };

  messageHistory.push(currentUserMessage);
  <span class="hljs-keyword">if</span> (messageHistory.length &gt; <span class="hljs-number">20</span>) messageHistory.shift();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> interaction.reply({
      content: <span class="hljs-string">"Hmm... processing your request! 🐀"</span>,
    });

    <span class="hljs-keyword">const</span> finalState = <span class="hljs-keyword">await</span> graph.invoke(graphInput);

    <span class="hljs-keyword">if</span> (!finalState.finalAction) {
      log(ERROR, <span class="hljs-string">"no final action found"</span>);
      <span class="hljs-keyword">await</span> interaction.editReply({
        content: <span class="hljs-string">"I'm sorry, I couldn't process your request."</span>,
      });
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">const</span> userPing = <span class="hljs-string">`&lt;@<span class="hljs-subst">${user.id}</span>&gt;`</span>;
    <span class="hljs-keyword">const</span> action = finalState.finalAction;

    <span class="hljs-keyword">const</span> quotedPrompt = <span class="hljs-string">`🗣️ "<span class="hljs-subst">${userPrompt}</span>"`</span>;

    <span class="hljs-keyword">switch</span> (action.type) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">"REPLY"</span>:
        <span class="hljs-keyword">await</span> interaction.editReply({
          content: <span class="hljs-string">`<span class="hljs-subst">${userPing}</span>\n\n<span class="hljs-subst">${quotedPrompt}</span>\n\n<span class="hljs-subst">${action.content}</span>`</span>,
        });
        <span class="hljs-keyword">break</span>;

      <span class="hljs-keyword">case</span> <span class="hljs-string">"REPLY_IN_THREAD"</span>:
        <span class="hljs-keyword">if</span> (!interaction.channel || !(<span class="hljs-string">"threads"</span> <span class="hljs-keyword">in</span> interaction.channel)) {
          <span class="hljs-keyword">await</span> interaction.editReply({
            content: <span class="hljs-string">"Cannot create a thread in this channel"</span>,
          });
          <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">const</span> thread = <span class="hljs-keyword">await</span> interaction.channel.threads.create({
            name: <span class="hljs-string">`Action: <span class="hljs-subst">${userPrompt.substring(<span class="hljs-number">0</span>, <span class="hljs-number">50</span>)}</span>...`</span>,
            autoArchiveDuration: <span class="hljs-number">60</span>,
          });

          <span class="hljs-keyword">await</span> thread.send(
            <span class="hljs-string">`<span class="hljs-subst">${userPing}</span>\n\n<span class="hljs-subst">${quotedPrompt}</span>\n\n<span class="hljs-subst">${action.content}</span>`</span>,
          );
          <span class="hljs-keyword">await</span> interaction.editReply({
            content: <span class="hljs-string">`I've created a thread for you: <span class="hljs-subst">${thread.url}</span>`</span>,
          });
        } <span class="hljs-keyword">catch</span> (threadError) {
          log(ERROR, <span class="hljs-string">"failed to create or reply in thread:"</span>, threadError);
          <span class="hljs-keyword">await</span> interaction.editReply({
            content: <span class="hljs-string">`<span class="hljs-subst">${userPing}</span>\n\n<span class="hljs-subst">${quotedPrompt}</span>\n\nI tried to create a thread but failed. Here is your response:\n\n<span class="hljs-subst">${action.content}</span>`</span>,
          });
        }
        <span class="hljs-keyword">break</span>;

      <span class="hljs-keyword">case</span> <span class="hljs-string">"CREATE_EMBED"</span>: {
        <span class="hljs-keyword">const</span> embed = <span class="hljs-keyword">new</span> EmbedBuilder()
          .setColor(<span class="hljs-number">0xffa500</span>)
          .setTitle(action.title)
          .setDescription(action.description)
          .setTimestamp()
          .setFooter({ text: <span class="hljs-string">"Support System"</span> });

        <span class="hljs-keyword">const</span> rolePing = action.roleToPing ? <span class="hljs-string">`&lt;@<span class="hljs-subst">${action.roleToPing}</span>&gt;`</span> : <span class="hljs-string">""</span>;

        <span class="hljs-keyword">await</span> interaction.editReply({
          content: <span class="hljs-string">`<span class="hljs-subst">${userPing}</span> <span class="hljs-subst">${rolePing}</span>`</span>,
          embeds: [embed],
        });
        <span class="hljs-keyword">break</span>;
      }
    }
  } <span class="hljs-keyword">catch</span> (error) {
    log(ERROR, <span class="hljs-string">"generating AI response or processing graph:"</span>, error);
    <span class="hljs-keyword">const</span> errorMessage =
      <span class="hljs-string">"sorry, I encountered an error while processing your request."</span>;
    <span class="hljs-keyword">if</span> (interaction.replied || interaction.deferred) {
      <span class="hljs-keyword">await</span> interaction.followUp({ content: errorMessage, ephemeral: <span class="hljs-literal">true</span> });
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">await</span> interaction.reply({ content: errorMessage, ephemeral: <span class="hljs-literal">true</span> });
    }
  }
});

<span class="hljs-keyword">const</span> token = process.env.DISCORD_BOT_TOKEN!;
client.login(token);
</code></pre>
<p>At the core of our bot is the <code>Client</code> object from <code>discord.js</code>. This represents your bot and handles everything from connecting to Discord’s API to listening for events like user messages or interactions.</p>
<p>What’s with that intent? Discord uses intents as a way for bots to declare what kind of data they want access to. In our case:</p>
<ul>
<li><p><code>Guilds</code> lets the bot connect to servers</p>
</li>
<li><p><code>GuildMessages</code> allows it to see messages</p>
</li>
<li><p><code>MessageContent</code> gives access to the actual content of messages</p>
</li>
</ul>
<p>These are quite standard, and there are many more based on different use cases. You can always check them all out <a target="_blank" href="https://discordjs.guide/popular-topics/intents.html#privileged-intents">here</a>.</p>
<p>You also keep a <code>Map</code> to store per-channel message history so the bot can respond with context across multiple channels:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> channelHistories = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, ChatMessage[]&gt;();
</code></pre>
<p>Discord.js provides access to a few events that you can listen to. When you work with slash commands, it registers an <code>Events.InteractionCreate</code>, which is what you’re listening to.</p>
<p>With every <code>/ask</code> command, you take the user's prompt and any previous messages. If <code>channelHistories</code> does not have a key with that specific channelId, meaning it's being used for the first time, you initialize it with an empty array and feed them into the AI state.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> finalState = <span class="hljs-keyword">await</span> graph.invoke({
  message: currentUserMessage,
  previousMessages: [...messageHistory],
});
</code></pre>
<p>Depending on what the graph <code>finalAction.type</code> returns, you either:</p>
<ul>
<li><p>reply directly,</p>
</li>
<li><p>create a thread and respond there,</p>
</li>
<li><p>or send an embed (for support-type replies).</p>
</li>
</ul>
<p>If a thread can’t be created, you fall back to replying in the main channel. Message history is capped at 20 to keep things lightweight.</p>
<p>Note that we’re not really using <code>previousMessages</code> much at the moment in the application, but I’ve prepared everything you need to handle querying previous conversations. You could easily create a new LangGraph node that queries or reasons over history if the bot needs to reference past conversations. (Take this as your challenge!)</p>
<p>This project should give you a basic idea of how you can use LangGraph + Composio to build a somewhat useful bot that can already handle decent stuff. There’s a lot more you could improve. I’ll leave that up to you. ✌️</p>
<p>Here’s a quick demo of what we’ve built so far:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/aeQKN0nMGRg" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<hr>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>By now you should have a good idea of how LangGraph works and also how to power the bot with integrations using Composio.</p>
<p>This is just a fraction of what you can do. Try adding more features and more integration support to the bot to fit your workflow. This can come in really handy.</p>
<p>If you got lost somewhere while coding along, you can find the source code <a target="_blank" href="https://github.com/shricodev/discord-bot-langgraph-composio">here</a>.</p>
<p>So, that is it for this article. Thank you so much for reading! See you next time. 🫡</p>
<p>Love to build cool stuff like this? I regularly build such stuff every few weeks. Feel free to reach out to me here:</p>
<ul>
<li><p>GitHub: <a target="_blank" href="http://github.com/shricodev">github.com/shricodev</a></p>
</li>
<li><p>Portfolio: <a target="_blank" href="http://techwithshrijal.com">techwithshrijal.com</a></p>
</li>
<li><p>LinkedIn: <a target="_blank" href="http://linkedin.com/in/iamshrijal">linkedin.com/in/iamshrijal</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create an AI-Powered Bot that Can Post on Twitter/X ]]>
                </title>
                <description>
                    <![CDATA[ These days, everyone wants to be a content creator. But it can be hard to find time to create and curate content, post on social media, build engagement, and grow your brand. And I’m not an exception to this. I wanted to create more content, and had ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-an-ai-powered-bot/</link>
                <guid isPermaLink="false">680931204adb8ffdef48642a</guid>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Twitter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Arunachalam B ]]>
                </dc:creator>
                <pubDate>Wed, 23 Apr 2025 18:27:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745416845372/5eb9963d-e092-4844-99d9-01fa70032169.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>These days, everyone wants to be a content creator. But it can be hard to find time to create and curate content, post on social media, build engagement, and grow your brand.</p>
<p>And I’m not an exception to this. I wanted to create more content, and had an idea based on something I’ve observed. I subscribe to a few technology newsletters, and I read lots of updates every day about the tech ecosystem. But I’ve noticed that many of my peers often don’t seem to be aware of this news. So, I decided to post my top three news stories (especially about AI) on my Twitter/X account every day.</p>
<p>I did this for a couple of weeks, but after that I couldn’t find the time to keep it going. So, I did some research into how I could automate the process, and I found a solution. In this guide, I’ll explain the process so you can use it, too.</p>
<p>By the end of this tutorial, you’ll have created your own AI bot that:</p>
<ul>
<li><p>Fetches data from an API or crawls a webpage</p>
</li>
<li><p>Processes the data using AI</p>
</li>
<li><p>Posts the results on Twitter/X</p>
</li>
</ul>
<p>And the great thing: this entire process is automated.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-bot">How to Build the Bot</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-generate-the-twitter-api-key">Step 1: Generate the Twitter API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-generate-access-token-and-secret">Step 2: Generate Access Token and Secret</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-generate-an-api-key-in-google-gemini">Step 3: Generate an API Key in Google Gemini</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-nodejs-project-setup">Node.js Project Setup</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-fetch-data-from-the-api">Step 1: Fetch Data from the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-upload-the-data-as-a-file-to-gemini-api">Step 2: Upload the Data as a File to Gemini API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-prompt-gemini-to-get-the-latest-ai-news">Step 3: Prompt Gemini to Get the Latest AI News</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-post-using-the-twitterx-api">Step 4: Post Using the Twitter/X API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-delete-the-file-uploaded-in-the-gemini-api">Step 5: Delete the File Uploaded in the Gemini API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-result">The Result</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we begin creating a bot, you’ll need to have the following setup and tools ready to go:</p>
<ul>
<li><a target="_blank" href="https://nodejs.org/en/learn/getting-started/introduction-to-nodejs">NodeJS</a> - A simple NodeJS app to code the bot</li>
</ul>
<p>You’ll also need some API keys, secrets, and tokens. So, you’ll need to have the following accounts created:</p>
<ul>
<li><p><a target="_blank" href="https://developer.x.com/">Twitter Developer</a> – To generate the Twitter/X API keys, secrets, and tokens</p>
</li>
<li><p><a target="_blank" href="https://aistudio.google.com/">Google AI Studio</a> – To generate the Gemini API key</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-bot">How to Build the Bot</h2>
<p>There are a number of steps I’ll walk you through to build your bot.</p>
<p>We’ll start by generating an API Key and Secret so we can use the Twitter/X API. Then we’ll generate an access token and access token secret with “Read and Write” permissions that’ll be able to post in your account. After that we’ll generate an API Key in Google Gemini (we’ll be using the Gemini API to process the data).</p>
<p>With all that taken care of, we’ll start working on the Node.js app. The app will be able to fetch data from an API, process the data using AI, and then post that data in the form of tweets on Twitter/X.</p>
<p>Finally, we’ll automate the entire process and schedule it to run daily.</p>
<h3 id="heading-step-1-generate-the-twitter-api-key">Step 1: Generate the Twitter API Key</h3>
<ol>
<li><p>Navigate to <a target="_blank" href="https://developer.x.com/">Twitter Developer Website</a>.</p>
</li>
<li><p>Click on the “Developer Portal” in the top right:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745152618491/214fe6d6-b699-40bb-8ac0-533b0c72b927.png" alt="Image showing developer portal highlighted" class="image--center mx-auto" width="1865" height="945" loading="lazy"></p>
</li>
<li><p>Signup using your account.</p>
</li>
<li><p>You’ll be asked to fill out a form asking how will you use the Twitter API, and a few basic details. It may take up to 24 hours to get approved. But, it’s approved instantly for me.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745152170917/d2c2ba21-f3f5-4bc6-bdd5-58d222e203e6.png" alt="Form asking how you'll use the Twitter API" class="image--center mx-auto" width="833" height="388" loading="lazy"></p>
</li>
<li><p>After login, Navigate to "Projects and Apps" and under “Overview” click on "Create App":</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745153184830/1a731639-0df2-47e3-baf2-3633e1735a69.png" alt="Create App screen" class="image--center mx-auto" width="691" height="306" loading="lazy"></p>
</li>
<li><p>Enter a name for your app and click “Next” to proceed with creating your app. At the end, you’ll be shown your API Key and Secret. Don’t copy that now.</p>
</li>
<li><p>Click on the project you created from the left side drawer and click on the "Edit" option in “User authentication settings” section.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745153746932/002f3b38-5aaf-43ef-8d7c-0368f141524f.png" alt="Editing the user authentication settings section" class="image--center mx-auto" width="1230" height="719" loading="lazy"></p>
</li>
<li><p>Select “Read and Write” in App Permissions section, “Web App, Automated App or Bot” in Type of App section, and enter your website URL (it can be any URL including http://localhost) in the “Callback URI” and “Website URL”. Then hit “Save”.</p>
</li>
<li><p>Go to “Keys and tokens” tab.</p>
</li>
<li><p>Click on “Regenerate” button in “API Key and Secret” section.</p>
</li>
<li><p>Copy and save the API Key and Secret somewhere securely.</p>
</li>
</ol>
<h3 id="heading-step-2-generate-access-token-and-secret">Step 2: Generate Access Token and Secret</h3>
<ol>
<li><p>Go to “Keys and tokens” tab.</p>
</li>
<li><p>Click on “Generate” or “Regenerate” button in “Access Token and Secret” section.</p>
</li>
<li><p>Copy and save the Access Token and Secret somewhere securely.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745154373207/4309dbcc-1081-46b7-be71-5babf950eae0.png" alt="Generating or regenerating keys and tokens" class="image--center mx-auto" width="1230" height="629" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-step-3-generate-an-api-key-in-google-gemini">Step 3: Generate an API Key in Google Gemini</h3>
<ol>
<li><p>Navigate to <a target="_blank" href="https://aistudio.google.com/">Google AI Studio</a>.</p>
</li>
<li><p>Login to your account.</p>
</li>
<li><p>Click on “Get API Key” button at the top right.</p>
</li>
<li><p>Click on “Create API Key” button.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745154646809/54c4fa1a-097e-4bf6-8a5f-229c01845d28.png" alt="Create API screen" class="image--center mx-auto" width="1802" height="309" loading="lazy"></p>
</li>
<li><p>Copy and save the API Key somewhere securely.</p>
</li>
</ol>
<p>Alright, we are done with creating the necessary API Keys and Secrets for our project. Let’s put on our coding shoes.</p>
<h2 id="heading-nodejs-project-setup">Node.js Project Setup</h2>
<p>There are 5 major steps for this part of the project. They are:</p>
<ol>
<li><p>Fetch data from the API</p>
</li>
<li><p>Upload the data as a file to Gemini API</p>
</li>
<li><p>Prompt Gemini with the uploaded file to get the latest AI news</p>
</li>
<li><p>Post news to Twitter/X using their API</p>
</li>
<li><p>Delete the file uploaded in Gemini API</p>
</li>
</ol>
<p>These are just the snippets of code that can be assembled together to run this project.</p>
<h3 id="heading-step-1-fetch-data-from-the-api">Step 1: Fetch Data from the API</h3>
<p>In my case, I’ll be using <code>techmeme.com</code> to get the latest news. But this site does not offer an API. So, I’ll be downloading the HTML of this site.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b204531fbfda5e805202b5f5ab5aa55d">
        <script src="https://gist.github.com/arunachalam-b/b204531fbfda5e805202b5f5ab5aa55d.js"></script></div><p> </p>
<p>In the <code>User-Agent</code> header, we pass the value that mimics a browser user agent to avoid potential blocks.</p>
<h3 id="heading-step-2-upload-the-data-as-a-file-to-gemini-api">Step 2: Upload the Data as a File to Gemini API</h3>
<p>Now we need to store this HTML in a separate file. We cannot directly pass the HTML code in the prompt to the Gemini API, as it’ll result in an error. This is because Gemini accepts only a limited number of tokens in this API. The HTML code of any website will always result in huge number of tokens. So, we’ll create a separate file.</p>
<p>Upload the file to the Gemini API. Refer to the file id in the prompt to Gemini.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5ebfed570c79a0f0faa8c4e42559c673">
        <script src="https://gist.github.com/arunachalam-b/5ebfed570c79a0f0faa8c4e42559c673.js"></script></div><p> </p>
<h3 id="heading-step-3-prompt-gemini-to-get-the-latest-ai-news">Step 3: Prompt Gemini to Get the Latest AI News</h3>
<p>Let’s write a prompt to Gemini asking it to generate top news by referring to the HTML file provided. We’ll ask it to provide a headline, short description, URL, and three relevant hashtags for each tweet. We’ll also give some example data of how it should look. We’ll ask it to generate a structured response by providing the format of the JSON that we want the output to be.</p>
<p>You can use whatever model you want to, but I’ll be using the <code>gemini-2.5-pro-exp-03-25</code> model for this use case. I’m using this model because we need a thinking model that thinks and picks the correct top news – not just one that predicts the next token/word. The Gemini 2.5 Pro model best qualifies for this.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="466449de313bcbc4241eaf3b6e1646a7">
        <script src="https://gist.github.com/arunachalam-b/466449de313bcbc4241eaf3b6e1646a7.js"></script></div><p> </p>
<h3 id="heading-step-4-post-using-the-twitterx-api">Step 4: Post Using the Twitter/X API</h3>
<p>Here’s the core of our app. We need to post all the tweets we received from Gemini. We’ll be posting the tweet as a thread. This means that the first tweet will be the root tweet and subsequent tweets will be in the comments of the prior tweet. This makes it a thread.</p>
<p>To do this, we’ll take the id of each tweet after it’s posted and pass it on to the next tweet as a reference. One additional thing to note here is, after each successful tweet, we’ll give a pause of 5 seconds before posting the next tweet. There are few reasons for doing it this way.</p>
<ol>
<li><p>When any script runs, it usually runs at a much higher speed (usually in milliseconds). So, the second tweet may get posted before the first tweet was posted (maybe due to some poor internet connection). Also, I believe Twitter implements some queue system which may quickly process the second tweet before your first. So it’s always better to leave a small gap – if not 5 seconds then at least 1 second</p>
</li>
<li><p>Twitter may have implemented some rate limiting mechanism. So if there are multiple request received from a same IP within a short time frame, they may block the IP and consider your account as spam.</p>
</li>
<li><p>Since we’re using a Free tier API, we are limited to 1500 tweets per month. If you’ve paid for this API, you won’t have to worry about this (since you’ll have a higher limit and the rate limiting mechanism –refer to point #2 – might not be applicable). All of this depends on their <a target="_blank" href="https://docs.x.com/x-api/introduction#access-levels">pricing</a>, so just refer to that and make your call accordingly.</p>
</li>
</ol>
<p>I’m using the free tier, and since it’s a hobby project, having a 5 seconds wait time makes sense. I have not faced any issues so far with this.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b049fda9e567bc68c7fb33de0ce67cd3">
        <script src="https://gist.github.com/arunachalam-b/b049fda9e567bc68c7fb33de0ce67cd3.js"></script></div><p> </p>
<h3 id="heading-step-5-delete-the-file-uploaded-in-the-gemini-api">Step 5: Delete the File Uploaded in the Gemini API</h3>
<p>After posting all the tweets, it’s time to clean up the system. The only thing we need to do as a clean up is delete the uploaded file. It’s always a best practice to remove an unused file that’s no longer needed. And since we’ve already posted the tweets, we no longer need that file. So, we’ll be deleting it in this step.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="741c5b13603187c76905f7b349661293">
        <script src="https://gist.github.com/arunachalam-b/741c5b13603187c76905f7b349661293.js"></script></div><p> </p>
<p>That’s it. We’re all done. You just need to copy these blocks of code into an <code>index.js</code> file and install some dependencies into the project and you should be good to go.</p>
<p>To make this even more simple, I have created a repo and made it public. Here’s the <a target="_blank" href="https://github.com/arunachalam-b/existential-crisis-alert-bot">Github repo URL</a>. You just need to clone the repo, install the dependencies, and run the <code>post</code> command</p>
<pre><code class="lang-plaintext">git clone https://github.com/arunachalam-b/existential-crisis-alert-bot.git
cd existential-crisis-alert-bot
npm i
</code></pre>
<p>Create a .env file and update your API keys and secrets in that file:</p>
<pre><code class="lang-plaintext">GEMINI_API_KEY=
TWITTER_API_KEY=
TWITTER_API_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
</code></pre>
<p>Run the following command to post the latest AI news to your account:</p>
<pre><code class="lang-plaintext">npm run post
</code></pre>
<h3 id="heading-the-result">The Result</h3>
<p>Here’s a sample output of that command:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745169397786/14e08ef8-dba5-45e0-a5d5-f3c6135c6347.png" alt="Output" class="image--center mx-auto" width="604" height="308" loading="lazy"></p>
<p>You can modify the code/prompt to fetch data from a different API and post the top results in your Twitter account.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope you now understand how you can automate a slightly complex process using AI and some APIs. Just note that this example is not completely automated. You still have to manually run the command everyday to post the tweets.</p>
<p>But you can automate that process as well. Just drop me a message if you wish to know about that. That topic itself deserves to be a separate tutorial. Also, I would request that you give a star for my project if you enjoyed this tutorial.</p>
<p>Meanwhile, you can follow my <a target="_blank" href="https://x.com/AI_Techie_Arun">Twitter/X account</a> to receive the top AI news everyday. If you wish to learn more about automation, subscribe to my email newsletter (<a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_automated_tweet">https://5minslearn.gogosoon.com/</a>) and follow me on social media.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
