Template Content
Scripts
About the template
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
Template Content
Scripts
This template demonstrates how to trigger a BlockKit modal in Slack, which then asks for additional information from the user and based on that information creates a ticket in Jira Service Management Cloud.
Parameters
and configure parameters to meet you needs.user:read
and user:read:email
permissions./command_name
), input data in into the modal and submit the form, after successful processing you should receive a message containing the new request/issue key.PurgeCache
script to purge cached data, for example when the list of Request Types changes in JSM side.import JSMCloud from './api/jsm/cloud';
import Slack from './api/slack';
import { SlashCommandEvent } from '@sr-connect/slack/events';
import { RecordStorage } from '@sr-connect/record-storage';
import { SlackError } from '@managed-api/slack-core/common';
import { getEnvVars, RecordStorageKeys } from './Utils';
import { getServiceDeskId } from './OnSlackViewSubmission';
/**
* When triggered, this function triggers a modal to be displayed in Slack.
* Data for JSM request types is cached using Record Storage due to the Slack's hard limit on needing to respond within 3 seconds.
*
* @param event Object that holds Slash Command event data
* @param context Object that holds function invocation context data
*/
export default async function (event: SlashCommandEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { BlockKitFields, PROJECT_KEY } = getEnvVars(context);
try {
// Ask Slack to open a modal view
await Slack.View.openView({
trigger_id: event.trigger_id,
view: JSON.stringify({
type: 'modal',
title: {
type: 'plain_text',
text: 'Create Ticket in JSM'
},
submit: {
type: 'plain_text',
text: 'Create'
},
blocks: [
{
type: 'input',
label: {
type: 'plain_text',
text: 'Request Type'
},
element: {
type: 'static_select',
action_id: BlockKitFields.REQUEST_TYPE,
placeholder: {
type: 'plain_text',
text: 'Select request type',
},
options: await getRequestTypesBlockKitOptions(PROJECT_KEY) // Construct dropdown for JSM request types
}
},
{
type: 'input',
element: {
type: 'plain_text_input',
action_id: BlockKitFields.SUMMARY,
placeholder: {
type: 'plain_text',
text: 'Summary for the ticket'
}
},
label: {
type: 'plain_text',
text: 'Summary'
},
},
{
type: 'input',
optional: true,
element: {
type: 'plain_text_input',
action_id: BlockKitFields.DESCRIPTION,
multiline: true,
placeholder: {
type: 'plain_text',
text: 'Description for the ticket'
}
},
label: {
type: 'plain_text',
text: 'Description'
}
}
]
})
})
} catch (e) {
// When something goes wrong, check if the error is indicating that we timed out (3 second limit)
if (e instanceof SlackError && e.message === 'expired_trigger_id') {
// If so then log it out
console.warn('Response timed out (3 seconds)');
// And PM the user about it
await Slack.Chat.postMessage({
body: {
channel: event.user_id,
text: 'Oops, it took too long to respond, please try again.'
}
});
} else {
// If something else went wrong, then log it out
console.error('Error while responding to create ticket slash command', e, {
event
});
// And PM the user about it
await Slack.Chat.postMessage({
body: {
channel: event.user_id,
text: 'Oops, something went front, please try again.'
}
});
}
}
}
/**
* Function that constructs BlockKit dropdown options for JSM request types
*/
async function getRequestTypesBlockKitOptions(projectKey: string) {
const requestTypes = await getRequestTypes(projectKey);
return requestTypes.map(rt => ({
text: {
type: 'plain_text',
text: rt.name,
},
value: rt.id
}));
}
/**
* Function that tries to retrieve JSM request types from the cache (using RecordStorage), if not found from the cache, gets it from JSM and stores in the cache
*/
async function getRequestTypes(projectKey: string) {
const storage = new RecordStorage();
let requestTypes = await storage.getValue<RequestType[]>(RecordStorageKeys.REQUEST_TYPES);
const servicedeskId = await getServiceDeskId(projectKey);
if (!requestTypes) {
requestTypes = ((await JSMCloud.Request.Type.getTypes({
serviceDeskId: [+servicedeskId]
})).values ?? []).filter(rt => rt.id && rt.name).map(rt => ({
id: rt.id,
name: rt.name
} as RequestType));
await storage.setValue(RecordStorageKeys.REQUEST_TYPES, requestTypes);
}
return requestTypes;
}
interface RequestType {
id: string;
name: string;
}
import JSMCloud from './api/jsm/cloud';
import JiraCloud from './api/jira/cloud';
import Slack from './api/slack';
import { ViewSubmissionEvent } from '@sr-connect/slack/events';
import { getEnvVars } from './Utils';
/**
* When triggered, this function extracts data from the submitted form and tries to create a request/issue with provided data in JSM Cloud.
* Slack user is matched to JSM user by email.
*
* @param event Object that holds View Submission event data
* @param context Object that holds function invocation context data
*/
export default async function (event: ViewSubmissionEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { BlockKitFields, PROJECT_KEY } = getEnvVars(context);
try {
// Find the user who submitted the form
const slackUser = await Slack.User.getInfo({
user: event.user.id
});
// Get the requst type value from the submitted form
const requestTypeValue = getFieldValue(event, BlockKitFields.REQUEST_TYPE).selected_option.value;
// Get the summary value from the submitted form
const summary = getFieldValue(event, BlockKitFields.SUMMARY).value;
// Get the description value from the submitted form
const description = getFieldValue(event, BlockKitFields.DESCRIPTION).value;
// Get the Slack's user email
const slackUserEmail = slackUser.user.profile.email ?? '';
// Try to find a user from Jira with matching email
const jiraUser = await getJiraUser(slackUserEmail);
// Check if the user was found
if (!jiraUser) {
// If not, log it out
console.warn(`No matching Jira user found with email: ${slackUserEmail}`);
// And PM the user to let them know about it
await Slack.Chat.postMessage({
body: {
channel: event.user.id,
text: `User not found in JSM with email: ${slackUserEmail}. Please contact your JSM admin to sort it out.`
}
});
// And finally halt the script execution
return;
}
// Get servicedesk Id
const servicedeskId = await getServiceDeskId(PROJECT_KEY);
// Create a new request in JSM
const request = await JSMCloud.Request.createRequest({
body: {
serviceDeskId: servicedeskId,
requestTypeId: requestTypeValue,
raiseOnBehalfOf: jiraUser.accountId,
requestFieldValues: {
summary,
description
}
}
});
// And PM the user with the newly created request/issue key
await Slack.Chat.postMessage({
body: {
channel: event.user.id,
text: `New request key: ${request.issueKey}`
}
});
} catch (e) {
// If something went wrong, log it out
console.error('Error while creating new request', e, {
event
});
// And PM the user to let them know about it
await Slack.Chat.postMessage({
body: {
channel: event.user.id,
text: 'Oops, something went wrong while creating a new request.'
}
});
}
}
/**
* Function that extracts the from value from submitted form
*/
function getFieldValue(event: ViewSubmissionEvent, actionId: string) {
const block = (event.view.blocks ?? []).find(b => b.element?.action_id === actionId);
if (!block) {
throw Error(`Block not found for action_id: ${actionId}`);
}
return event.view.state?.values?.[block.block_id][actionId];
}
/**
* Function that finds the user form Jira Cloud with matching email (traverses paginated results)
*/
async function getJiraUser(email: string) {
const maxResults = 50;
let startAt = 0;
do {
const users = await JiraCloud.User.getUsers({
startAt,
maxResults
});
const user = users.find(u => u.emailAddress?.toLocaleLowerCase() === email.toLocaleLowerCase());
if (user) {
return user;
}
if (users.length === maxResults) {
startAt += maxResults;
} else {
startAt = 0
}
} while (startAt > 0);
}
// Function that returns servicedesk Id
export async function getServiceDeskId(projectKey: string) {
// Get all the servicedesk projects
const servicedesks = await JSMCloud.Servicedesk.getServicedesks();
// Find the servicedesk project
const servicedesk = servicedesks.values?.find(sd => sd.projectKey === projectKey);
// Check if the project was found
if (!servicedesk) {
// If not, then throw an error
throw Error(`Service desk not found with PROJECT KEY: ${projectKey}`)
}
return servicedesk.id;
}
import { RecordStorage } from '@sr-connect/record-storage';
import { RecordStorageKeys } from './Utils';
/**
* This script purges all cached data.
*/
export default async function(event: any, context: Context): Promise<void> {
const storage = new RecordStorage();
await storage.deleteValue(RecordStorageKeys.REQUEST_TYPES);
console.log('Purged cached request types');
}
export const RecordStorageKeys = {
REQUEST_TYPES: 'REQUEST_TYPES'
}
interface EnvVars {
PROJECT_KEY: string;
BlockKitFields: {
REQUEST_TYPE: string,
SUMMARY: string,
DESCRIPTION: string
}
}
export function getEnvVars(context: Context) {
return context.environment.vars as EnvVars;
}