Create a ticket in Jira Service Management Cloud from Slack command


Get Started

Not the template you're looking for? Browse more.

About the template


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. Get started to learn more.

About ScriptRunner Connect


What is ScriptRunner Connect?

ScriptRunner Connect is an AI assisted code-first (JavaScript/TypeScript) integration platform (iPaaS) for building complex integrations and automations.

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


README

Scripts

TypeScriptOnSlackSlashCommand
Slash Command

README


๐Ÿ“‹ Overview

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.

๐Ÿ–Š๏ธ Setup

  • Configure API Connections and Event Listeners by creating connectors for JSM Cloud, Jira Cloud and Slack, or use existing ones.
  • Go to Parameters and configure parameters to meet you needs.
  • Change the PROJECT_KEY to appropriate value.
  • Make sure users who may want to create issues from Slack have matching accounts (by email) in JSM Cloud instance and have appropriate permissions to create new requests/issues.
  • Make sure the Slack bot has user:read and user:read:email permissions.

๐Ÿš€ Usage

  • Trigger a modal from Slack slash command (/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.
  • Run PurgeCache script to purge cached data, for example when the list of Request Types changes in JSM side.

API Connections


TypeScriptOnSlackSlashCommand

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;
}
TypeScriptOnSlackViewSubmission

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;
}
TypeScriptPurgeCache

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');
}
TypeScriptUtils

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;
}
Documentation ยท Support ยท Suggestions & feature requests