<?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>Mon, 25 May 2026 10:49:38 +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 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>
