

- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
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.
- Create an application in the Discord Developer Portal, add a Bot.
- In Bot → Privileged Gateway Intents, turn on Message Content Intent. You need this to read message text.
- 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
- 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:
- Studio → Create Application File → Table
- Label: "Discord Chat"
- 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:
- Go to System Properties → Create New
- Name:
x_1468549_d_sn_r_0.discord.api_key
| Value: your generated API key | Mark as Private - Name:
x_1468549_d_sn_r_0.discord.bot_token
| Value: your Discord bot token | Mark as Private
In your bot's .env
file:
DISCORD_TOKEN
: Your bot token from Discord Developer PortalSN_INGEST_URL
:https://yourinstance.service-now.com/api/x_1468549_d_sn_r_0/discord_ingest
SN_API_KEY
: The same random string you put in ServiceNow
😎 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:
- Send "hello" in the Discord channel
- Verify a row appears in your ServiceNow Discord chat table
- 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:
- Send
!help
in Discord - you should see a bot reply - Send
!incidents
- you should get an incident count - 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.
- Go to railway.app and sign up
- Connect your GitHub account
- Click "New Project" → "Deploy from GitHub"
- Select your repository
- Important: Set Root Directory to
bot
- this tells Railway to deploy only the bot folder, not your entire ServiceNow repository - Railway automatically detects this is a Node.js app and configures:
- Build Command:
npm install
- Start Command:
npm start
(from your package.json)
- Build Command:
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:
- In your Railway project dashboard, go to the Variables tab
- Add these three environment variables exactly as they appear:
DISCORD_TOKEN
: Your Discord bot token (from Discord Developer Portal)SN_INGEST_URL
:https://yourinstance.service-now.com/api/x_1468549_d_sn_r_0/discord_ingest
SN_API_KEY
: Your generated 32-character hex API key
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:
- In your Railway project dashboard, go to the Variables tab
- Add these three environment variables exactly as they appear:
DISCORD_TOKEN
: Your Discord bot token (from Discord Developer Portal)SN_INGEST_URL
:https://yourinstance.service-now.com/api/x_1468549_d_sn_r_0/discord_ingest
SN_API_KEY
: Your generated 32-character hex API key
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:
- Send messages in Discord - they should still appear in ServiceNow
- Send
!help
and!incidents
- ServiceNow should still reply - 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
- Message Reception: Bot receives real-time message events via Discord's WebSocket Gateway
- Data Extraction: Constructs payload with message metadata (bot/index.js:25-41):
- Message ID, channel info, author details
- Content, timestamps, attachments, embeds, mentions
- 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 hasDISCORD_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,556 Views
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.