Earl Duque
Administrator
Administrator

Building a Discord-ServiceNow Chat Bot: Complete Walkthrough

 

 

This walkthrough shows you exactly how to create a Discord bot on Node.js that sends every chat in a channel to a ServiceNow table, then on that ServiceNow table a business rule watches for certain chat messages (like !incidents) and sends a chat back to the channel. Finally, we'll deploy the Node app to Railway so it can be alive 24/7.

 

This guide is for if you're a ServiceNow developer (even a novice) but have never done anything with Node, npm, Railway, etc.


Understanding the key technologies

 

Before we dive in, here's what each piece does:

 

Node.js

  • What it is: A runtime for running JavaScript outside of a web browser.
  • Why we used it: To run the Discord bot code on a server so it can listen to messages and send them to ServiceNow.

discord.js

  • What it is: A popular Node.js library for interacting with the Discord API.
  • Why we used it: To make it easy to log in as your bot, listen for messages in channels, and work with Discord data.

dotenv

  • What it is: A small library that loads environment variables from a .env file into your app.
  • Why we used it: So we can store secrets (like your bot token) in a file that isn't committed to GitHub.

Railway

  • What it is: A cloud hosting platform for running apps, bots, and APIs.
  • Why we used it: To run the Discord bot 24/7 without keeping your own computer on.

Discord Bot Token

  • What it is: A unique secret that lets code log in as your bot account on Discord.
  • Why we used it: The bot needs it to authenticate with Discord's servers and read/send messages.

ServiceNow Scripted REST API (SRAPI)

  • What it is: A way to create your own REST endpoints inside ServiceNow, using server-side JavaScript.
  • Why we used it: So the Discord bot can send chat messages into your ServiceNow instance as HTTP requests.

 

I've skipped explaining what a ServiceNow table and business rule, as I expect most novice ServiceNow developers to know those are.


1) What you'll build

  • A Discord bot that reads every message in a specific channel and forwards it to ServiceNow.
  • A ServiceNow table that stores those messages.
  • An onInsert Business Rule that watches the stored text for commands like !incidents and posts a reply back into the same Discord channel.

 

We'll use Node.js with discord.js for the bot and a Scripted REST API for ingestion. For replying from ServiceNow, we'll send back to the exact channel where the message came from instead of using webhooks.


2) Discord setup

First, let's set up your Discord bot and get the credentials you'll need.

  1. Create an application in the Discord Developer Portal, add a Bot.
  2. In Bot → Privileged Gateway Intents, turn on Message Content Intent. You need this to read message text.
  3. In OAuth2 → URL Generator, pick bot scope, then permissions like:
    • Read Messages/View Channels
    • Send Messages
    • Use Slash Commands (optional here)
      Note: if you're unable to generate the URL and are seeing something about a redirect URI, that's because you need to pick Bot as the only scope first, and then
  4. Invite the bot to your server with the generated URL.

 

Environment variables you'll need on the bot host:

DISCORD_TOKEN=your-bot-token
SN_INGEST_URL=https://yourinstance.service-now.com/api/x_1468549_d_sn_r_0/discord_ingest
SN_API_KEY=some-shared-secret-or-token

 

The DISCORD_TOKEN comes from the Discord Developer Portal (Bot section → Token). The SN_API_KEY isn't from Discord - you'll create this yourself as a shared secret between your bot and ServiceNow. You can generate one with https://www.browserling.com/tools/random-hex or in a terminal using:

openssl rand -hex 32

3) ServiceNow Studio and Source Control Setup

We'll build this in ServiceNow Studio for two main reasons: it's the modern development interface that doesn't require jumping around the system, and in Zurich release and newer, it has built-in source control integration with GitHub.

 

Setting up source control (optional but recommended):

If you want to save your ServiceNow app to GitHub (great for version control and collaboration), you can set up source control integration directly in Studio. This lets you commit your ServiceNow changes right from the Studio interface.

 

For a complete walkthrough of setting up source control in ServiceNow Studio, see: Source Control in ServiceNow Studio: Complete Walkthrough


4) ServiceNow data model

Create a custom table in ServiceNow Studio. Either in a new app or an existing one. This is where Discord messages will be stored.

 

Step-by-step:

  1. Studio → Create Application File → Table
  2. Label: "Discord Chat"
  3. The system will auto-generate a table name like x_1468549_d_sn_r_0_discord_chat (this is my example's table name, your table's name will be different, but it's important so note this down)

 

Suggested fields:

  • message_id (String, 64)
  • channel_id (String, 64)
  • channel_name (String, 128)
  • author_id (String, 64)
  • author_name (String, 128)
  • content (String, 4000)
  • message_ts (Date/Time)
  • raw_json (String, 8000)
  • webhook_url (String, 1024) - Optional field for webhook-based responses

5) ServiceNow Scripted REST API (ingestion)

 

This creates the endpoint where Discord messages come into ServiceNow.

 

Application: your scoped app
API Name: discord_ingest
Resource: POST /message

Security
Use a simple header like X-API-Key: <SN_API_KEY>.

 

Resource Script (this is the actual working code from our implementation):

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
  // Simple API key check
  var apiKey = request.getHeader("X-API-Key");
  if (gs.getProperty('x_1468549_d_sn_r_0.discord.api_key') != apiKey) {
    response.setStatus(401);
    return {error: 'Unauthorized'};
  }

  var body = request.body.dataString;
  var data;
  try {
    data = JSON.parse(body);
  } catch (e) {
    response.setStatus(400);
    return {error: 'Invalid JSON'};
  }

  // Expect fields from the bot
  // { message_id, channel_id, channel_name, author_id, author_name, content, message_ts, raw }
  var gr = new GlideRecord('x_1468549_d_sn_r_0_discord_chat');
  gr.initialize();
  gr.message_id   = data.message_id || '';
  gr.channel_id   = data.channel_id || '';
  gr.channel_name = data.channel_name || '';
  gr.author_id    = data.author_id || '';
  gr.author_name  = data.author_name || '';
  gr.content      = data.content || '';
  gr.message_ts   = data.message_ts || new GlideDateTime();
  gr.raw_json     = JSON.stringify(data.raw || {});
  gr.insert();

  response.setStatus(200);
  return {status: 'ok'};
})(request, response);

 

Important: Replace x_1468549_d_sn_r_0_discord_chat with your actual table name.

Set the property x_1468549_d_sn_r_0.discord.api_key to match SN_API_KEY.


6) Discord bot (Node.js with discord.js v14)

 

Commit your ServiceNow app changes. Then clone your GitHub repository to you computer and open it in your IDE of choice (like VS Code).

 

Now let's create the actual bot. If you're using the monorepo structure, put this in the bot/ folder.

Install Node.js LTS from nodejs.org (version 18 or newer). Verify it works:

node -v
npm -v

 

Create your project:

mkdir bot
cd bot
npm init -y
npm install discord.js node-fetch@2 dotenv

 

package.json:

{
  "name": "bot",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "discord.js": "^14.21.0",
    "dotenv": "^17.2.1",
    "node-fetch": "^2.7.0"
  }
}

 

.env (never commit this to GitHub, you'll create a .gitignore file soon to make sure this doesn't happen):

DISCORD_TOKEN=your-bot-token-from-discord-dev-portal
SN_INGEST_URL=https://yourinstance.service-now.com/api/x_1468549_d_sn_r_0/discord_ingest
SN_API_KEY=your-generated-random-string

 

index.js (this is the actual working bot code):

require('dotenv').config();
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const fetch = require('node-fetch');

const token = process.env.DISCORD_TOKEN;
const ingestUrl = process.env.SN_INGEST_URL;
const snApiKey = process.env.SN_API_KEY;

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

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

client.on('messageCreate', async (message) => {
  if (message.author.bot) return;

  const payload = {
    message_id: message.id,
    channel_id: message.channel.id,
    channel_name: message.channel?.name || '',
    author_id: message.author.id,
    author_name: message.author.username,
    content: message.content || '',
    message_ts: new Date(message.createdTimestamp).toISOString(),
    raw: {
      attachments: message.attachments?.map?.(a => ({ url: a.url, name: a.name })),
      embeds: message.embeds,
      mentions: {
        users: message.mentions.users.map(u => u.id),
        roles: message.mentions.roles.map(r => r.id)
      }
    }
  };

  try {
    const res = await fetch(ingestUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': snApiKey
      },
      body: JSON.stringify(payload)
    });
    if (!res.ok) {
      const text = await res.text();
      console.error('ServiceNow ingest failed:', res.status, text);
    }
  } catch (err) {
    console.error('Ingest error:', err);
  }
});

client.login(token);

7) Connect all the environment variables and ServiceNow system properties

 

Now let's wire up all the credentials and test the integration.

 

In ServiceNow, create system properties:

  1. Go to System Properties → Create New
  2. Name: x_1468549_d_sn_r_0.discord.api_key | Value: your generated API key | Mark as Private
  3. Name: x_1468549_d_sn_r_0.discord.bot_token | Value: your Discord bot token | Mark as Private

 

In your bot's .env file:


😎 Test to see if messages come into the instance

 

Let's test the integration locally first.

 

Run the bot:

cd bot
npm start

 

You should see "Logged in as YourBotName#1234" in the console.

 

Test the flow:

  1. Send "hello" in the Discord channel
  2. Verify a row appears in your ServiceNow Discord chat table
  3. If you get errors, check:
    • Table name matches exactly in the Scripted REST API script
    • API key matches between bot .env and ServiceNow property
    • Message Content Intent is enabled in Discord Developer Portal

 

Every chat message should flow into your ServiceNow table within a second or two.


😎 ServiceNow Business Rule to reply back (via replying to the channel it came from)

 

Instead of using webhooks, we'll send replies back to the exact channel where the message came from using Discord's Bot API.

 

Business Rule

  • Table: x_1468549_d_sn_r_0_discord_chat (your Discord chat table)
  • When: after
  • Insert: true
  • Name: Reply

 

Script (this is the actual working Business Rule code):

(function executeRule(current, previous) {
  var content = (current.content + '').trim();
  if (!content.startsWith('!')) return; // Only commands

  var token = gs.getProperty('x_1468549_d_sn_r_0.discord.bot_token');
  var channelId = current.channel_id + '';
  if (!channelId || !token) return;

  if (content.startsWith('!incidents')) {
    sendReply(channelId, getIncidentSummary());
  } else if (content.startsWith('!help')) {
    sendReply(channelId, "**Commands:**\n`!incidents` Open incidents summary\n`!help` This help");
  } else {
    sendReply(channelId, `You said ${content}`);
  }

  function getIncidentSummary() {
    var open = 0;
    var ga = new GlideAggregate('incident');
    ga.addAggregate('COUNT');
    ga.addQuery('active', true);
    ga.query();
    if (ga.next()) open = parseInt(ga.getAggregate('COUNT'), 10) || 0;
    return `**Incident snapshot:**\nOpen: ${open}\n(generated ${gs.nowDateTime()})`;
  }

  function sendReply(channelId, message) {
    try {
      var r = new sn_ws.RESTMessageV2();
      r.setEndpoint('https://discord.com/api/v10/channels/' + channelId + '/messages');
      r.setHttpMethod('post');
      r.setRequestHeader('Authorization', 'Bot ' + token);
      r.setRequestHeader('Content-Type', 'application/json');
      r.setRequestBody(JSON.stringify({ content: message }));
      var res = r.execute();
      if (res.getStatusCode() >= 300) {
        gs.warn('Discord send failed: ' + res.getStatusCode() + ' ' + res.getBody());
      }
    } catch (e) {
      gs.error('Discord send error: ' + e.message);
    }
  }
})(current, previous);

 

Test the reply functionality:

  1. Send !help in Discord - you should see a bot reply
  2. Send !incidents - you should get an incident count
  3. Send !anything - you should get an echo

9) ServiceNow Business Rule to reply back (via replying to the channel it came from)

 

Now let's get this running 24/7. Railway is perfect because it can deploy directly from your GitHub repo.

 

Commit your work so far:

Commit your ServiceNow app changes, then Commit your node.js app changes. Preferably in this order.

 

Repository Structure and .gitignore Setup:

If you're using source control integration, ServiceNow Studio controls the root of your repository. This creates a unique structure where your Discord bot lives alongside your ServiceNow app files:

/                     # Root controlled by ServiceNow Studio  
├─ sys_app_*.xml      # ServiceNow app files
├─ update/            # ServiceNow metadata
├─ .gitignore         # Critical for security!
├─ bot/               # Your Discord bot folder
│  ├─ package.json
│  ├─ index.js
│  └─ .env (never committed!)

 

Critical .gitignore setup:

Create a .gitignore file in your repository root. This is essential for security - it prevents sensitive files from being committed to your public repository:

# Node.js dependencies (can be large and regenerated)
node_modules

# Environment files containing secrets
.env

# Optional: Other common exclusions
*.log
.DS_Store

 

Why this matters: Your .env file contains your Discord bot token and API keys. If these get committed to a public repository, anyone can see them and potentially use your bot maliciously. The .gitignore file ensures these secrets stay local to your development environment.

 

Deploy to Railway:

Railway is a cloud platform that makes deploying Node.js apps incredibly easy. It's similar to other platforms like Heroku, but with simpler setup and great GitHub integration.

  1. Go to railway.app and sign up
  2. Connect your GitHub account
  3. Click "New Project" → "Deploy from GitHub"
  4. Select your repository
  5. Important: Set Root Directory to bot - this tells Railway to deploy only the bot folder, not your entire ServiceNow repository
  6. Railway automatically detects this is a Node.js app and configures:
    • Build Command: npm install
    • Start Command: npm start (from your package.json)

 

Railway Platform Benefits:

  • Auto-deployment on every GitHub push
  • Built-in logging and monitoring
  • Automatic restarts if your bot crashes
  • Free tier available for small projects
  • Simple environment variable management

10) Deploy the node app to Railway

 

Why Railway needs separate environment variables:

Remember that .env file you created locally? It's not deployed to Railway (and shouldn't be - that's the whole point of .gitignore). Railway needs its own copy of these environment variables to run your bot in the cloud.

 

Setting up variables in Railway:

  1. In your Railway project dashboard, go to the Variables tab
  2. Add these three environment variables exactly as they appear:

 

Important notes:

  • These must match exactly what your index.js file expects (case-sensitive)
  • Railway encrypts these variables - they're secure and not visible to others
  • Any changes to variables will trigger a new deployment

 

Click Deploy. Railway's build time is usually under a minute for a small Node.js bot like this.

 

Common deployment issues:

  • If deployment fails, check that all three environment variables are set
  • Verify the Root Directory is set to bot
  • Check Railway's deployment logs for specific error messages

11) Environment Variable Management in Railway

 

Why Railway needs separate environment variables:

Remember that .env file you created locally? It's not deployed to Railway (and shouldn't be - that's the whole point of .gitignore). Railway needs its own copy of these environment variables to run your bot in the cloud.

 

Setting up variables in Railway:

  1. In your Railway project dashboard, go to the Variables tab
  2. Add these three environment variables exactly as they appear:

 

Important notes:

  • These must match exactly what your index.js file expects (case-sensitive)
  • Railway encrypts these variables - they're secure and not visible to others
  • Any changes to variables will trigger a new deployment

 

Click Deploy. Railway's build time is usually under a minute for a small Node.js bot like this.

 

Common deployment issues:

  • If deployment fails, check that all three environment variables are set
  • Verify the Root Directory is set to bot
  • Check Railway's deployment logs for specific error messages

12) Test again without running it locally

 

Now stop your local bot (Ctrl+C) and test the full cloud integration:

  1. Send messages in Discord - they should still appear in ServiceNow
  2. Send !help and !incidents - ServiceNow should still reply
  3. Check Railway logs to see the bot activity

 

Go to Railway's Logs tab and look for "Logged in as YourBot#1234".

You're done! Your Discord-ServiceNow integration is live and running 24/7.


How the bot monitors Discord messages

 

The bot uses WebSockets (not REST polling) to connect to Discord's Gateway API in real-time:

 

Architecture & Technologies

Discord.js Library: Uses the official Discord.js v14 client library with WebSocket-based real-time event monitoring.

 

Gateway Events: The bot connects to Discord's Gateway API via WebSockets using these intents:

  • GuildMessages - receives message events
  • MessageContent - accesses message text content
  • Guilds - basic server information

 

Event-Driven: Listens for the messageCreate event (bot/index.js:22) which fires whenever a new message is posted in any channel the bot can access.

 

Data Flow

  1. Message Reception: Bot receives real-time message events via Discord's WebSocket Gateway
  2. Data Extraction: Constructs payload with message metadata (bot/index.js:25-41):
    • Message ID, channel info, author details
    • Content, timestamps, attachments, embeds, mentions
  3. ServiceNow Ingestion: POSTs JSON payload to ServiceNow via REST API (bot/index.js:44-51)
    • Uses custom ServiceNow ingest endpoint with API key authentication
    • Sends structured data for storage/processing

 

Key Technologies

  • WebSockets: Discord Gateway connection for real-time events (not REST polling)
  • REST API: HTTP POST to ServiceNow for data ingestion
  • Node.js: Runtime environment with discord.js and node-fetch libraries
  • JSON: Data serialization format between systems

 

The bot filters out other bots' messages (bot/index.js:23) and includes comprehensive message metadata including attachments and user mentions in the ServiceNow payload.


Troubleshooting

 

Bot won't start locally:

  • "Cannot find module 'dotenv'": Run npm install dotenv in the bot directory
  • "Token is not provided": Check your .env file has DISCORD_TOKEN

 

ServiceNow integration issues:

  • 500 error "invalid table name": Copy the exact table name from ServiceNow into your Scripted REST API
  • 401 Unauthorized: API key mismatch between bot .env and ServiceNow property

 

Railway deployment issues:

  • Build takes forever: Make sure Root Directory is set to bot
  • App crashes: Check all environment variables are set in Railway
  • "Module not found": Ensure package.json is in the bot directory

 

No bot replies:

  • Check Business Rule is active and on the correct table
  • Verify bot token is stored in ServiceNow system properties
  • Make sure bot has "Send Messages" permission in Discord

You now have a fully functional Discord-ServiceNow integration running 24/7! The bot captures every Discord message, stores it in ServiceNow, and ServiceNow can intelligently respond back to the same channel based on the message content.

1 Comment