ServiceNow & Jira — Bidirectional Integration
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
an hour ago
Introduction — ServiceNow and Jira
ServiceNow:
ServiceNow is a platform used by IT teams to manage incidents, requests, and services in one place, helping organisations keep their operations running smoothly.
Jira:
Jira is a tool mainly used by development teams to track bugs, manage tasks, and plan their work using agile methods like Scrum
Why integrate ServiceNow and Jira?
In many organisations, IT teams and development teams use different tools, which can create communication gaps. Integrating ServiceNow and Jira helps both teams stay aligned, share updates easily, and resolve issues faster without switching platforms.
Prerequisites — Login and Setup
ServiceNow — Personal Developer Instance (PDI)
To follow this guide, you need access to a ServiceNow instance. If you are using a PDI:
- Go to https://developer.servicenow.com and sign in or create a free account
- Request a Personal Developer Instance
- Note your instance URL: https://devXXXXXX.service-now.com
- Login with admin credentials
Jira — Free Cloud Account
- Go to https://atlassian.net and sign up for free
- Create a new Jira Software project (Team-managed)
- Note your project KEY (e.g., KAN, SN, IT)
- Generate an API token: Profile → Manage Account → Security → API Tokens
- Copy and store the token securely — it is shown only once
----——————Let’s get started with the ServiceNow–Jira integration.————-------
1. System Properties
Store your Jira credentials securely in System Properties rather than hardcoding them.
In service now, Go to System Properties > All Properties and create these one by one:
Property Name
| Value |
x_jira.instance_url | |
x_jira.username | your-jira-login@email.com |
x_jira.api_token | your-api-token-from-atlas s i an |
x_jira.project_key | KAN (your Jira project key) |
Using System Properties instead of hardcoded credentials means you can update credentials without touching scripts. It also keeps sensitive tokens out of source code — a security best practice.
2. Script Include — JiraIntegrationUtils
We use a Script Include to keep all Jira logic in one place instead of repeating it in multiple Business Rules, making it easier to maintain and update.
This script acts as a utility that handles Jira operations like creating tickets, updating status, adding comments, and managing API calls—serving as a reusable bridge between ServiceNow and Jira.
Go to System Definitions > Script Include and create one -
var JiraIntegrationUtils = Class.create();
JiraIntegrationUtils.prototype = {
initialise: function() {
this.instanceUrl = gs .getProperty('x_jira.instance_url');
this.username = g s.getProperty('x_jira.username');
this.apiToken = g s.getProperty('x_jira.api_token');
this.projectKey = g s.getProperty('x_jira.project_key');
this.authHeader = 'Basic ' +
GlideStringUtil.base64Encode(this.username + ':' + this.apiToken);
},
// ─────────────────────────────────────────
// CREATE JIRA TICKET
// ─────────────────────────────────────────
createJiraTicket: function(incidentGR) {
try {
if (!incidentGR || !incidentGR.isValidRecord()) {
gs.error('Invalid incident record');
return null;
}
var endpoint = this.instanceUrl + '/rest/api/3/issue';
// 🔹 Priority Mapping
var priorityMap = {
'1': 'Highest',
'2': 'High',
'3': 'Medium',
'4': 'Low',
'5': 'Lowest'
};
var jiraPriority = priorityMap[incidentGR.getValue('priority')] || 'Medium';
// 🔹 Issue Type Mapping
var issueTypeMap = {
'hardware': 'Task',
'software': 'Task',
'network': 'Task',
'inquiry': 'Task',
'database': 'Task'
};
var issueType = issueTypeMap[incidentGR.getValue('category')] || 'Task';
// 🔹 Data Extraction
var incNumber = incidentGR.getValue('number');
var shortDesc = incidentGR.getValue('short_description') || 'No short description';
var description = incidentGR.getValue('description') || 'No description';
var caller = incidentGR.caller_id.getDisplayValue() || 'N/A';
var category = incidentGR.category.getDisplayValue() || 'N/A';
var priority = incidentGR.priority.getDisplayValue() || 'N/A';
// 🔹 Payload (ADF format)
var payload = {
fields: {
project: { key: this.projectKey },
summary: '[' + incNumber + '] ' + shortDesc,
description: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'ServiceNow Incident: ' + incNumber }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Caller: ' + caller }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Category: ' + category }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Priority: ' + priority }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Description: ' + description }]
}
]
},
issuetype: { name: issueType },
priority: { name: jiraPriority }
}
};
// 🔹 REST Call
var request = new s n _ w s. RESTMessageV2();
request.setEndpoint(endpoint);
request.setHttpMethod('POST');
request.setRequestHeader('Authorisation', this.authHeader);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('Accept', 'application/json');
request.setRequestBody(JSON.stri n gify(payload));
request.setHttpTimeout(30000);
var response = request.execute();
var statusCode = response.getStatusCode();
var body = response.getBody();
gs.info('Jira Create Response [' + statusCode + ']: ' + body);
if (statusCode == 201) {
var parsed = JSON.parse(body);
return parsed.key;
}
g s.error('Jira creation failed: ' + body);
return null;
} catch (e) {
gs.error('createJiraTicket ERROR: ' + e.message);
return null;
}
},
// ─────────────────────────────────────────
// GET TRANSITIONS
// ─────────────────────────────────────────
getTransitions: function(jiraKey) {
try {
var endpoint = this.instanceUrl + '/rest/api/3/issue/' + jiraKey + '/transitions';
var request = new s n_ w s .RESTMessageV2();
request.setEndpoint(endpoint);
request.setHttpMethod('GET');
request.setRequestHeader('Authorisation', this.authHeader);
request.setRequestHeader('Accept', 'application/json');
var response = request.execute();
if (response.getStatusCode() == 200) {
return JSON.parse(response.getBody()).transitions || [];
}
return [];
} catch (e) {
gs.error('getTransitions ERROR: ' + e.message);
return [];
}
},
// ─────────────────────────────────────────
// UPDATE STATUS
// ─────────────────────────────────────────
updateJiraStatus: function(jiraKey, transitionName) {
try {
var transitions = this.getTransitions(jiraKey);
var transitionId = null;
for (var i = 0; i < transitions.length; i++) {
if (transitions[i].name.toLowerCase() === transitionName.toLowerCase()) {
transitionId = transitions[i].id;
break;
}
}
if (!transitionId) {
gs.warn('Transition not found: ' + transitionName);
return false;
}
var endpoint = this.instanceUrl + '/rest/api/3/issue/' + jiraKey + '/transitions';
var payload = {
transition: { id: transitionId }
};
var request = new s n_w s.RESTMessageV2();
request.setEndpoint(endpoint);
request.setHttpMethod('POST');
request.setRequestHeader('Authorisation', this.authHeader);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestBody(JSON.s trin gify(payload));
var response = request.execute();
return response.getStatusCode() == 204;
} catch (e) {
gs.error('updateJiraStatus ERROR: ' + e.message);
return false;
}
},
// ─────────────────────────────────────────
// ADD COMMENT
// ─────────────────────────────────────────
addComment: function(jiraKey, commentText) {
try {
var endpoint = this.instanceUrl + '/rest/api/3/issue/' + jiraKey + '/comment';
var payload = {
body: {
type: 'doc',
version: 1,
content: [{
type: 'paragraph',
content: [{ type: 'text', text: commentText }]
}]
}
};
var request = new s n_ w s.RESTMessageV2();
request.setEndpoint(endpoint);
request.setHttpMethod('POST');
request.setRequestHeader('Authorisation', this.authHeader);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestBody(JSON. str ing if y(payload));
var response = request.execute();
return response.getStatusCode() == 201;
} catch (e) {
gs.error('addComment ERROR: ' + e.message);
return false;
}
},
type: 'JiraIntegrationUtils'
};
3. Business Rules — Outbound Sync (ServiceNow → Jira)
i. Business Rule 1 — Create Jira Ticket on Incident Insert
Purpose: When a new incident is created in ServiceNow, this rule automatically creates a corresponding Jira ticket and stores the Jira ticket key (e.g., KAN-7) in the u_jira_ticket_key field on the incident.
Configuration:
Setting | Value |
Table | incident |
When | after |
Insert | ✅ Checked |
Update | ❌ Unchecked |
Filter Condition | priority = 1 or 2 |
Active | Yes |
(function executeRule(current, previous) {
try {
// ─────────────────────────────────────────
// GUARD 1: Skip if already linked to Jira
// ─────────────────────────────────────────
if (current.getValue('u_jira_ticket_key')) {
gs.info('JIRA BR: Already linked, skipping. Key: ' +
current.getValue('u_jira_ticket_key'));
return;
}
// ─────────────────────────────────────────
// GUARD 2: Skip if created by web hook
// (prevents infinite loop when Jira creates SN incident)
// ─────────────────────────────────────────
if (g s. getUserName() === 'system' ||
g s.getUserName() === 'admin' &&
current.getValue('category') === 'software' &&
current.getValue('short_description').indexOf('[Jira') === 0) {
gs.info('JIRA BR: Skipping web hook-created incident');
return;
}
gs.info('JIRA BR Insert: Processing incident ' + current.number);
// ─────────────────────────────────────────
// CREATE JIRA TICKET
// ─────────────────────────────────────────
var jira = new JiraIntegrationUtils();
var jiraKey = jira.createJiraTicket(current);
gs.info('JIRA BR Insert: Jira key returned = ' + jiraKey);
if (!jiraKey) {
g s.error('JIRA BR Insert: Failed to get Jira key for ' +
current.number);
return;
}
// ─────────────────────────────────────────
// SAVE JIRA KEY BACK TO INCIDENT
// Use separate GlideRecord — NOT current.update()
// ─────────────────────────────────────────
var g r = new GlideRecord('incident');
g r.addQuery('number', current.number);
gr.query();
if (g r.next()) {
g r.setValue('u_jira_ticket_key', jiraKey);
g r.setValue('work_notes',
'🔗 Jira ticket auto-created: ' + jiraKey +
'\nView in Jira: ' +
g s.getProperty('x_jira.instance_url') +
'/browse/' + jiraKey
);
gr.setWorkflow(false);
gr.autoSysFields(false);
var saved = g r.update();
if (saved) {
gs.info('JIRA BR Insert: ✅ Saved ' + jiraKey +
' to incident ' + current.number);
} else {
g s.error('JIRA BR Insert: ❌ Failed to save Jira key to ' +
current.number);
}
} else {
g s.error('JIRA BR Insert: ❌ Could not find incident ' +
current.number + ' to update');
}
} catch(e) {
gs.error('JIRA BR Insert EXCEPTION: ' + e.message);
}
})(current, previous);
ii. Business Rule 2 — Sync State Change to Jira
Purpose: When an agent changes the state of an incident in ServiceNow (e.g., from New to Resolved), this BR automatically moves the corresponding Jira ticket to the matching status and adds a comment.
Configuration:
Setting | Value |
Table | incident |
When | after |
Insert | ❌ Unchecked |
Update | ✅ Checked |
Condition | current.state.changes() && u_jira_ticket_key != empty |
Active | Yes |
(function executeRule(current, previous) {
try {
var jiraKey = current.getValue('u_jira_ticket_key');
// Exit if no Jira ticket linked
if (!jiraKey || jiraKey.trim() === '') {
return;
}
// Exit if state didn't actually change
if (!current.state.changes()) {
return;
}
var newState = current.getValue('state');
var oldState = previous.getValue('state');
gs.info('JIRA BR Update: State changed from ' +
oldState + ' to ' + newState +
' for ' + current.number);
// Map ServiceNow state → Jira transition name
// Adjust these transition names to match YOUR Jira project workflow
var transitionMap = {
'1': 'To Do', // New
'2': 'In Progress', // In Progress
'3': 'In Progress', // On Hold → keep In Progress in Jira
'4': 'In Progress', // Awaiting User Info
'5': 'In Progress', // Awaiting Problem
'6': 'Done', // Resolved
'7': 'Done' // Closed
};
var transition = transitionMap[newState];
if (!transition) {
g s.warn('JIRA BR Update: No transition mapped for state ' +
newState);
return;
}
var jira = new JiraIntegrationUtils();
var success = jira.updateJiraStatus(jiraKey, transition);
if (success) {
// Add comment to Jira about the state change
var stateDisplay = current.state.getDisplayValue();
jira.addComment(
jiraKey,
'ServiceNow incident ' + current.number +
' state changed to: ' + stateDisplay +
'\nUpdated by: ' + g s.getUserDisplayName()
);
gs.info('JIRA BR Update: Successfully synced state to Jira ' +
jiraKey);
}
} catch(e) {
g s.error('JIRA BR Update error for ' +
current.number + ': ' + e.message);
}
})(current, previous);
4. Custom Field — u_jira_ticket_key
A custom field is added to the Incident table to store the linked Jira ticket key (e.g., KAN-7). This field is the bridge between the two systems — every lookup and sync operation uses this key to find the matching record on both sides.
- Table: incident
- Column name: u_jira_ticket_key
- Type: String, Max length: 40
5. Scripted REST API — Inbound Sync (Jira → ServiceNow)
What is a Scripted REST API?
It’s a way in ServiceNow to create your own custom API endpoint where you control what happens when an external system (like Jira) sends a request.
Why use it instead of a standard API?
Because it lets you handle Jira’s web hook data in your own way — process events, map fields properly, avoid loops, and respond cleanly — instead of just exposing raw table data.
System Web Services > Scripted REST APIs > New
| Name | Jira Web hook Receiver |
| API ID | jira_web hook |
| Active | ✅ |
Then add a Resource:
| Name | Receive Jira Update |
| HTTP Method | POST |
| Relative Path | /update |
| Requires Authentication |
6. How the Jira Web hook Works
Jira web hooks are configured in Jira Settings > System > Web hooks. When an issue event occurs in Jira, it sends an HTTP POST request to your ServiceNow endpoint with a JSON payload containing all issue details including the event type, issue key, current status, priority, summary, and description.
6.4 Endpoint Configuration
| API Name | Jira Web hook Receiver |
| API ID | jira_web hook |
| Resource Name | Receive Jira Update |
| HTTP Method | POST |
| Relative Path | /update |
| Full Endpoint URL | https://devXXXXXX.service-now.com/api/[namespace]/jira_webhook/update |
Conclusion
The ServiceNow-Jira bidirectional integration demonstrated here is a production-grade solution that eliminates the operational gap between ITSM and development teams. By leveraging ServiceNow's native capabilities — Script Includes, Business Rules, and Scripted REST APIs — alongside Jira's web hook system, we have built a seamless sync without any third-party middleware.
Rather than two teams working in silos, both tools now function as a unified system — incidents flow automatically, statuses stay in sync, and every change is logged with a full audit trail.
- 95 Views
