Discord Bots

Build a Discord Bot That Queries Databases Using OpenAI Functions

Tech Setup4 min read
TS

Tech Setup

Published August 17, 2026 · Editorial policy

Build a Discord Bot That Queries Databases Using OpenAI Functions

Your developer community keeps dropping database query requests into a dedicated Discord channel, and you are tired of writing custom SQL snippets for them every time someone needs user metrics or audit logs. Building a custom Discord bot that leverages OpenAI function calling bridges this gap, allowing non-technical teammates or team members on mobile to fetch live database records using natural language. By the end of this guide, you will have a working Node.js bot that safely translates human chat into parameterized SQL queries against a PostgreSQL database without exposing raw shell access or risking prompt injection disasters.

Architectural Overview & Quick Setup

To make this work securely, your Discord bot acts as an intermediary. It listens to messages, hands the user's intent and your database schema to the OpenAI Chat Completions API, receives a structured JSON object containing arguments for a specific function, executes that function against your database, and replies in the Discord thread.

Initialize your project workspace by running the following commands in your terminal:

mkdir discord-sql-bot
cd discord-sql-bot
npm init -y
npm install discord.js openai pg dotenv
npm install --save-dev typescript @types/node tsx
npx tsc --init

Create a .env file in the root directory of your project. This file stores your sensitive credentials. Never commit this file to version control.

DISCORD_TOKEN=your_discord_bot_token_here
DISCORD_CLIENT_ID=your_discord_application_id_here
OPENAI_API_KEY=sk-proj-your_openai_api_key_here
DATABASE_URL=postgresql://db_user:db_password@localhost:5432/your_database_name

To ensure your TypeScript configuration is optimized for modern Node.js execution via tsx, update your tsconfig.json to include these compiler options:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Configuring the PostgreSQL Connection and Database Schema

Before writing the bot logic, you need a reliable database client configuration that handles connection pooling properly. Create a file named db.ts to manage your PostgreSQL connection using the pg package.

import pkg from 'pg';
const { Pool } = pkg;
import dotenv from 'dotenv';

dotenv.config();

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export async function queryDatabase(text: string, params: any[]) {
  const start = Date.now();
  try {
    const res = await pool.query(text, params);
    const duration = Date.now() - start;
    console.log('Executed query', { text, duration, rows: res.rowCount });
    return res.rows;
  } catch (error) {
    console.error('Database query error:', { text, error });
    throw error;
  }
}

For the sake of this implementation, assume your database contains a table named users with the following schema: id (SERIAL PRIMARY KEY), username (VARCHAR), email (VARCHAR), status (VARCHAR), and created_at (TIMESTAMP).

Defining OpenAI Functions for Database Lookups

OpenAI function calling does not execute code directly; instead, it analyzes the user's prompt and returns a structured JSON payload telling your application which function to run and what arguments to pass.

Create a file named tools.ts to define both the OpenAI tool definitions and the concrete execution handlers. Restricting the bot to pre-written parameterized queries protects your database against SQL injection.

import { queryDatabase } from './db.js';

export const aiTools = [
  {
    type: "function",
    function: {
      name: "get_user_by_email",
      description: "Retrieve user account details by searching for their email address.",
      parameters: {
        type: "object",
        properties: {
          email: {
            type: "string",
            description: "The exact or partial email address of the user to look up."
          }
        },
        required: ["email"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "get_users_by_status",
      description: "Retrieve a list of users filtered by their account status.",
      parameters: {
        type: "object",
        properties: {
          status: {
            type: "string",
            enum: ["active", "suspended", "pending"],
            description: "The account status to filter users by."
          }
        },
        required: ["status"]
      }
    }
  }
];

export async function executeToolCall(name: string, args: any) {
  if (name === "get_user_by_email") {
    const sql = "SELECT id, username, email, status, created_at FROM users WHERE email ILIKE $1";
    const rows = await queryDatabase(sql, [`%${args.email}%`]);
    return JSON.stringify(rows);
  }
  
  if (name === "get_users_by_status") {
    const sql = "SELECT id, username, email, status, created_at FROM users WHERE status = $1 LIMIT 10";
    const rows = await queryDatabase(sql, [args.status]);
    return JSON.stringify(rows);
  }

  throw new Error(`Unknown tool execution requested: ${name}`);
}

Building the Discord Bot Event Loop

Now, wire everything together inside an entry point file called bot.ts. This script boots up discord.js, instantiates the OpenAI client, listens for incoming messages, manages conversation state with the model, and loops back when the model triggers a function call.

import { Client, GatewayIntentBits, Message } from 'discord.js';
import OpenAI from 'openai';
import dotenv from 'dotenv';
import { aiTools, executeToolCall } from './tools.js';

dotenv.config();

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

client.once('ready', () => {
  console.log(`Logged in as ${client.user?.tag}!`);
});

client.on('messageCreate', async (message: Message) => {
  // Ignore messages from bots or messages not mentioning the bot
  if (message.author.bot) return;
  if (!message.mentions.has(client.user!)) return;

  // Strip the bot mention from the prompt text
  const prompt = message.content.replace(/<@!?\d+>/g, '').trim();
  if (!prompt) return;

  await message.channel.sendTyping();

  try {
    const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
      {
        role: "system",
        content: "You are a helpful database assistant for an engineering team. Use the provided tools to query the database and answer user questions accurately. Format code blocks or lists cleanly for Discord output."
      },
      {
        role: "user",
        content: prompt
      }
    ];

    // First API call to OpenAI to determine if a function should be invoked
    const response = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      messages: messages,
      tools: aiTools,
      tool_choice: "auto",
    });

    const responseMessage = response.choices[0].message;
    messages.push(responseMessage);

    // Check if the model decided to call a function
    if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
      for (const toolCall of responseMessage.tool_calls) {
        const functionName = toolCall.function.name;
        const functionArgs = JSON.parse(toolCall.function.arguments);

        console.log(`Executing function ${functionName} with args:`, functionArgs);

        // Execute the database query via our tools module
        const toolResult = await executeToolCall(functionName, functionArgs);

        // Append the tool execution result back to the conversation history
        messages.push({
          role: "tool",
          tool_call_id: toolCall.id,
          content: toolResult,
        });
      }

      // Second API call to OpenAI to synthesize the raw data into a human-readable response
      const secondResponse = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: messages,
      });

      const finalReply = secondResponse.choices[0].message.content;
      await message.reply(finalReply || "Query executed, but no response text was generated.");
    } else {
      // The model answered directly without needing a database lookup
      await message.reply(responseMessage.content || "I couldn't understand that request.");
    }
  } catch (error) {
    console.error("Error processing message:", error);
    await message.reply("An error occurred while processing your database request.");
  }
});

client.login(process.env.DISCORD_TOKEN);

Run your bot during development using tsx:

npx tsx bot.ts

Expected terminal output upon successful boot:

Logged in as DatabaseBot#1234!

Head over to your Discord server, mention the bot in a channel, and type: @DatabaseBot Find the user whose email is alice@example.com. You will see the structured argument extraction logged to your console, followed by a clean markdown summary posted directly back into your Discord channel.

Troubleshooting Common Failures

1. Bot ignores messages completely

  • Cause: The Message Content Intent is disabled in the Discord Developer Portal.
  • Fix: Go to your application dashboard at discord.com/developers/applications, select your bot, navigate to the Bot tab, scroll down to Privileged Gateway Intents, and toggle on Message Content Intent. Save changes and restart your bot.

2. OpenAI returns invalid JSON arguments for functions

  • Cause: The model is hallucinating parameters because the JSON schema descriptions in tools.ts are ambiguous.
  • Fix: Make your tool descriptions and property descriptions hyper-specific. Explicitly state data types, formatting requirements, and constraints inside the parameter object descriptions.

3. Database connection drops under load

  • Cause: Unhandled connection exhaustion or missing connection timeouts in the pg pool configuration.
  • Fix: Pass explicit connection limits and idle timeouts when instantiating your pool:
    export const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 10,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    });
    

Security Best Practices

Exposing database queries to a chat interface requires strict safety boundaries. Never allow OpenAI to generate raw SQL strings dynamically and run them via pool.query(generatedSql). This practice leaves your application vulnerable to catastrophic prompt injection attacks, where a malicious user could trick the model into executing DROP TABLE users;.

Always stick to deterministic function calling where the parameters are extracted by the model, but the SQL statement structure is hardcoded and parameterized in your application code.