Sync ServiceNow with Jira Cloud


Get Started

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

About the integration


How does the integration logic work?

When an incident is created in ServiceNow, a corresponding issue is created in Jira Cloud, and vice versa. Updates on both sides will be kept in sync as well. Get started to learn more.

Which fields are being synced?

  • Short description (ServiceNow) โ†” Summary (Jira Cloud)
  • Urgency (ServiceNow) โ†” Priority (Jira Cloud)
  • State (ServiceNow) โ†” Status (Jira Cloud)
  • Caller (ServiceNow) โ†” Reporter (Jira Cloud)
  • Description
  • Comments
  • Attachments

Can I configure which fields are being synced?

Yes. You can change which fields are being synced and control quite many other things via the configuration.

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

TypeScriptOnJiraCloudCommentCreated
Comment Created

README


๐Ÿ“‹ Overview

This template creates a two-way sync between Jira Cloud issues and ServiceNow incidents. Setting it up will take a little bit of patience, so buckle up!

Out of the box, this templates supports exchanging the following data between ServiceNow and Jira Cloud:

  • short description โ†” summary
  • description โ†” description
  • urgency โ†” priority
  • state โ†” status
  • caller โ†” reporter
  • comments โ†” comments
  • attachments โ†” attachments

๐Ÿ–Š๏ธ Setup

  • Create a custom field in Jira Cloud in order to store the number of the corresponding ServiceNow incident.

    • Open your Jira Cloud instance and click on the cog icon next to your avatar.
    • Select Issues, then navigate to Custom fields and click on Create custom field.
    • Select the short text (plain text only) option.
    • Enter ServiceNow incident number into the Name input. If you want to use another name, you need to also specify it under parameters.
    • Check the issue screens that are relevant to the issue type you wish to sync.
    • Click on Update.
  • Create a custom column in ServiceNow for your Incident table in order to store the corresponding Jira Cloud issue key.

    • Open your ServiceNow instance and navigate to the Incident table.
    • Right-click on the column header, select Configure and then Table. Then click on New.
    • Select String as the Type.
    • Enter an appropriate name as the Column label and write Jira Cloud issue key as the Column label. If you want to use another label, you will also need to specify it under parameters.
    • Make sure that the Column name defaults to u_jira_cloud_issue_key. If you want to use another name here, beware that you will need to change your business rule scripts slightly.
    • Enter 20 as the Max length, make sure the Active checkbox is selected and then click on Submit.
  • Please do not edit either of these custom fields or columns throught the UI, or you will risk breaking the integration.

  • Create and authorize connectors for JiraCloud and ServiceNow or use existing ones.

  • Make sure that the accounts used to setup the connectors have all necessary permissions to create, edit and delete all the fields and resources covered by this integration.

  • Configure and save all API connections (only needed in advanced view) and event listeners. Keep in mind though that while setting up an event listener for ServiceNow you will need to tweak a few steps:

    • Event Listener that uses OnServiceNowIncidentCreated:

      • Select the incident table.
      • Only check the Insert checkbox.
      • Use default payload parameters.
    • Event Listener that uses OnServiceNowIncidentUpdated:

      • Select the incident table.
      • Only check the Update checkbox.
      • Keep the default payload parameters but add the following custom ones:
            KEY:                    VALUE:
            short_description       short_description
            description             description
            urgency                 urgency
            state                   state
            caller_id               caller_id
            comments                comments
        
      • After pasting the generated script to the Code Editor, add the following code after line that contains sn_ws.RESTMessageV2:
            "   var gru = GlideScriptRecordUtil.get(current);\n" +
            "   var short_description = '';\n" +
            "   var description = '';\n" +
            "   var state = '';\n" +
            "   var urgency = '';\n" +
            "   var caller_id = '';\n" +
            "   var comments = '';\n" +
            "   // Returns an arrayList of changed field elements with database names\n" +
            "   var changedFieldNames = gru.getChangedFieldNames();\n" +
            "   //Returns an arrayList of all change values from changed fields\n" +
            "   var changes = gru.getChanges();\n" +
            "   // Convert to JavaScript Arrays\n" +
            "   gs.include('j2js');\n" +
            "   changedFieldNames = j2js(changedFieldNames);\n" +
            "   changes = j2js(changes);\n" +
            "   for (var i = 0; i < changedFieldNames.length; i++) {\n" +
            "      switch (changedFieldNames[i]) {\n" +
            "         case 'short_description':\n" +
            "            short_description = changes[i];\n" +
            "            break;\n" +
            "         case 'description':\n" +
            "         description = changes[i];\n" +
            "         break;\n" +
            "         case 'urgency':\n" +
            "            urgency = changes[i];\n" +
            "            break;\n" +
            "         case 'state':\n" +
            "            state = changes[i];\n" +
            "            break;\n" +
            "         case 'caller_id':\n" +
            "            caller_id = changes[i];\n" +
            "            break;\n" +
            "         case 'comments':\n" +
            "            var journalGR = new GlideRecord('sys_journal_field');\n" +
            "            journalGR.addQuery('element', 'comments');\n" +
            "            journalGR.addQuery('element_id', current.sys_id);\n" +
            "            journalGR.orderByDesc('sys_created_on');\n" +
            "            journalGR.setLimit(1);\n" +
            "            journalGR.query();\n" +
            "            comments = journalGR.next() ? journalGR.getValue('value') : '';\n" +
            "            break;\n" +
            "         default:\n" +
            "            break;\n" +
            "      }\n" +
            "   }\n" +
        
    • Event Listener that uses OnServiceNowIncidentDeleted:

      • Select the incident table.
      • Only check the Delete checkbox.
      • Keep the default payload parameters but add the following custom ones:
            KEY:                    VALUE:
            jira_cloud_issue_key    current.u_jira_cloud_issue_key
        
      • Note that if you used another name for your custom column, you will need to tweak the custom parameter value:
        current.<YOUR COLUMN NAME>
        
    • Event Listener that uses OnServiceNowIncidentAttachmentCreated:

      • Select the sys_attachment table.
      • Only check the Insert checkbox.
      • Keep the default payload parameters and add incident from the dropdown.
      • Add the following custom payload parameter:
            KEY:                    VALUE:
            attachment_id           current.sys_id
        
      • After pasting the generated script to the Code Editor, add the following code after line brGr.active = true:
        brGr.condition = 'current.table_name == "incident"';
        
    • Event Listener that uses OnServiceNowIncidentAttachmentDeleted:

      • Select the sys_attachment table.
      • Only check the Delete checkbox.
      • Keep the default payload parameters and add incident from the dropdown.
      • Add the following custom payload parameter:
            KEY:                    VALUE:
            attachment_id           current.sys_id
        
      • After pasting the generated script to the Code Editor, add the following code after line brGr.active = true:
        brGr.condition = 'current.table_name == "incident"';
        
  • When Adding Webhooks in Jira Cloud, make sure to add in the Issue related events field the project key you want to listen. Example: project = TEST, so you would only listen to projects you're interested in.

  • In ScriptRunner Connect, navigate to Parameters and fill them in according to your configuration and preference.

๐Ÿš€ Usage

  • This is a two-way sync template that keeps ServiceNow incidents in sync with Jira Cloud issues. Create an issue in Jira Cloud or an incident in ServiceNow to see the integration in effect.
  • Updates in the UI by the user who has authorised the connectors will be ignored to avoid infinite update loops.

๐Ÿ“… Changelog

8 March 2025

  • Update README to fix OnServiceNowIncidentUpdated event listener instructions.
  • Update OnServiceNowIncidentUpdated script to check if comment has a value.

API Connections


TypeScriptOnJiraCloudCommentCreated

import JiraCloud from "./api/jira/cloud";
import ServiceNow from './api/servicenow';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import { Incident } from "./Types";
import { getEnvVars, getRecordFromArrayOfOne } from "./Utils";

/**
 * This function creates a comment in ServiceNow when a comment is created on an issue in Jira Cloud.
 *
 * @param event Object that holds Comment Created event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueCommentCreatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant to this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME
        }
    } = getEnvVars(context);

    // If 'comments' is not among the synced fields, log out a message and stop the operation
    if (!SYNCED_FIELDS.includes('comments')) {
        console.log('Not syncing comments');
        return;
    }

    try {
        // Get the current user
        const myself = await JiraCloud.Myself.getCurrentUser();

        // If the comment was created by the current user, log out a message, and stop the operation to avoid infinite loops
        if (myself.accountId === event.comment.author.accountId) {
            console.log('Same user, skipping sync');
            return;
        }

        // Get the issue in Jira Cloud which triggered the event
        const issue = await JiraCloud.Issue.getIssue({
            issueIdOrKey: event.issue.key,
        });

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields({});

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Get the number of the corresponding ServiceNow incident from the Jira Cloud issue that triggered the event
        const incidentNumber = issue.fields?.[customField.key];

        // If the incident number is missing, log out a warning and stop the operation
        if (!incidentNumber) {
            console.warn(`No corresponding ServiceNow incident found for Jira Cloud issue ${event.issue.key}`);
            return;
        }

        // Use this number to query for the correct ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${incidentNumber}`,
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Copy the the comment to the correct ServiceNow incident
        await ServiceNow.Table.updateRecord({
            tableName: 'incident',
            sys_id: incident.sys_id,
            body: {
                comments: `${event.comment.author.displayName}: ${event.comment.body}`,
            },
        });

        // If successful, log out the incident number
        console.log(`Comment created in ServiceNow for incident ${incidentNumber}`)

    } catch (error) {
        // If something goes wrong, log out the error
        console.error('Error creating a comment in ServiceNow', error);
    }
}
TypeScriptOnJiraCloudCommentUpdated

import JiraCloud from "./api/jira/cloud";
import ServiceNow from './api/servicenow';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { Incident } from "./Types";
import { getEnvVars, getRecordFromArrayOfOne } from "./Utils";

/**
 * This function creates a new comment for ServiceNow Incident when a comment is updated in Jira Cloud.
 *
 * @param event Object that holds Comment Updated event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueCommentUpdatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant to this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME
        }
    } = getEnvVars(context);

    // If 'comments' is not among the synced fields, log out a message and stop the operation
    if (!SYNCED_FIELDS.includes('comments')) {
        console.log('Not syncing comments');
        return;
    }

    try {
        // Get the current user
        const myself = await JiraCloud.Myself.getCurrentUser();

        // If the comment was updated by the current user, log out a message, and stop the operation to avoid infinite loops
        if (myself.accountId === event.comment.updateAuthor.accountId) {
            console.log('Same user, skipping sync');
            return;
        }

        // Get the issue in Jira Cloud which triggered the event
        const issue = await JiraCloud.Issue.getIssue({
            issueIdOrKey: event.issue.key,
        });

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields({});

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Get the number of the corresponding ServiceNow incident from the Jira Cloud issue that triggered the event
        const incidentNumber = issue.fields?.[customField?.key];

        // If the incident number is missing, log out a warning and stop the operation
        if (!incidentNumber) {
            console.warn(`No corresponding ServiceNow incident found for Jira Cloud issue ${event.issue.key}`);
            return;
        }

        // Use this number to query for the correct ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${incidentNumber}`,
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Copy the the comment to the ServiceNow incident
        await ServiceNow.Table.updateRecord({
            tableName: 'incident',
            sys_id: incident.sys_id,
            body: {
                comments: `${event.comment.author.displayName} (update): ${event.comment.body}`,
            },
        });

        // If successful, log out the incident number
        console.log(`Comment created in ServiceNow for incident ${incidentNumber}`)

    } catch (error) {
        // If something goes wrong, log out the error
        console.error('Error creating a comment in ServiceNow', error)
    }
}
TypeScriptOnJiraCloudIssueCreated

import JiraCloud from './api/jira/cloud';
import ServiceNow from './api/servicenow';
import { IssueCreatedEvent } from '@sr-connect/jira-cloud/events';
import { RecordStorage } from '@sr-connect/record-storage';
import { CreateIncidentRequestBody, IncidentColumn, ServiceNowUser } from './Types';
import { getEnvVars, getRecordFromArrayOfOne } from './Utils';

/**
 * This function creates a new ServiceNow incident when an issue is created in Jira Cloud.
 *
 * @param event Object that holds Issue Created event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueCreatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            INCIDENT_DEFAULT_SHORT_DESCRIPTION,
            SERVICENOW_ADMIN_EMAIL,
        },
        FieldMappings: {
            STATUS_TO_STATE,
            PRIORITY_TO_URGENCY,
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME,
            SERVICENOW_CUSTOM_COLUMN_NAME,
        },
    } = getEnvVars(context)

    // Start keep track of when a corresponding incident is created in ServiceNow
    let incidentCreated = false;

    try {
        // Get the current user
        const myself = await JiraCloud.Myself.getCurrentUser();

        // If the issue was created by the current user, log out a message, and stop the operation to avoid infinite loops
        if (myself.accountId === event.user.accountId) {
            console.log('Same user, skipping sync');
            return;
        }

        // Get the custom column where the corresponding Jira Cloud issue key is stored
        const columnRecords = await ServiceNow.Table.getRecords({
            tableName: 'sys_dictionary',
            sysparm_query: `name=incident^sys_name=${SERVICENOW_CUSTOM_COLUMN_NAME}`,
        });
        const column = getRecordFromArrayOfOne<IncidentColumn>(columnRecords.result);

        // Define the request body for the new ServiceNow incident, also adding the key of the current Jira Cloud issue to a custom column in ServiceNow
        const requestBody: CreateIncidentRequestBody = {
            short_description: INCIDENT_DEFAULT_SHORT_DESCRIPTION, // Overwrite later if 'summary' is amonged the synced fields
            [column.element]: event.issue.key,
        };

        // Extract the issue fields from the Jira Cloud issue that triggered the event
        const issueFields = event.issue.fields;

        // If 'summary' is among the synced fields, assign it as the short description for the new ServiceNow incident
        if (SYNCED_FIELDS.includes('summary')) {
            // If no summary is found for the Jira Cloud issue that triggered the event, throw an error
            if (!issueFields.summary) {
                throw Error(`No summary found for the Jira Cloud issue ${event.issue.key}`);
            }
            // Assign it as the summary on the new ServiceNow incident
            requestBody.short_description = issueFields.summary;
        }

        // If 'reporter' is among the synced fields, try to assign a corresponding caller to the new ServiceNow incident
        if (SYNCED_FIELDS.includes('reporter')) {
            try {
                // Check if the Jira Cloud issue reporter's email matches with that of a ServiceNow user
                const usersRecord = await ServiceNow.Table.getRecords({
                    tableName: 'sys_user',
                    sysparm_query: `email=${issueFields.reporter?.emailAddress}`,
                    headers: {
                        'Content-type': 'application/json',
                    },
                });
                const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result)

                // Assign it as the caller on the new ServiceNow incident
                requestBody.caller_id = user.sys_id;

            } catch (error) {

                // If no such user exists in ServiceNow, log out a warning
                console.warn(`Corresponding user with email ${issueFields.reporter?.emailAddress} was not found from ServiceNow, using the specified admin user instead as the caller`)

                // Find the specified admin user in ServiceNow
                const usersRecordWithAdminEmail = await ServiceNow.Table.getRecords({
                    tableName: 'sys_user',
                    sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
                    headers: {
                        'Content-type': 'application/json',
                    }
                });
                const adminUser = getRecordFromArrayOfOne<ServiceNowUser>(usersRecordWithAdminEmail.result);

                // Assign it to the new ServiceNow incident as the reporter
                requestBody.caller_id = adminUser.sys_id;
            }
        } else {
            // If 'reporter' is not among the synced fields, always assign the specified admin user as the caller
            const usersRecordWithAdminEmail = await ServiceNow.Table.getRecords({
                tableName: 'sys_user',
                sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
                headers: {
                    'Content-type': 'application/json',
                }
            });
            const adminUser = getRecordFromArrayOfOne<ServiceNowUser>(usersRecordWithAdminEmail.result);

            // Assign the admin user to the new ServiceNow incident as the reporter
            requestBody.caller_id = adminUser.sys_id;
        }

        // If 'priority' is among the synced fields, add a corresponding urgency to the new ServiceNow incident
        if (SYNCED_FIELDS.includes('priority')) {

            // If no priority can be found for the Jira Cloud issue, throw an error
            if (!issueFields.priority) {
                throw Error(`No priority found for the Jira Cloud issue ${event.issue.key}`)
            }

            // For the Jira Cloud issue priority, find a corresponding incident urgency name in ServiceNow
            const urgencyLabel = PRIORITY_TO_URGENCY[issueFields.priority.name];

            // If no such urgency can be found, throw an error
            if (!urgencyLabel) {
                throw Error(`Corresponding urgency not found in ServiceNow for the Jira Cloud priority ${issueFields.priority.name}`)
            }

            // Obtain more data about the urgency record from ServiceNow
            const urgencyRecords = await ServiceNow.Table.getRecords({
                tableName: 'sys_choice',
                sysparm_query: `element=urgency^label=${urgencyLabel}`
            });
            const urgency = getRecordFromArrayOfOne<IncidentColumn>(urgencyRecords.result);

            // Add the urgency value to the new ServiceNow incident
            requestBody.urgency = urgency.value;
        }

        // If 'status' is among the synced field, add a corresponding state to the new ServiceNow incident
        if (SYNCED_FIELDS.includes('status')) {

            // Get the issue that triggered the event, because if status was updated, it is not reflected in the initial creation event
            const issue = await JiraCloud.Issue.getIssue({
                issueIdOrKey: event.issue.key
            });

            // Get the name of the new status
            const statusName = issue.fields?.status?.name

            // If no status can be found for the Jira Cloud issue, throw an error
            if (!statusName) {
                throw Error(`No status found for Jira Cloud issue ${event.issue.key}`)
            }

            // For the Jira Cloud issue status, find a corresponding state record for the ServiceNow incident
            const stateLabel = STATUS_TO_STATE[statusName];

            // If no such state is found in ServiceNow, throw an error
            if (!stateLabel) {
                throw Error(`Corresponding incident state not found in ServiceNow for the Jira Cloud workflow status ${statusName}`)
            }

            // Obtain more data about the state record from ServiceNow
            const stateRecords = await ServiceNow.Table.getRecords({
                tableName: 'sys_choice',
                sysparm_query: `element=state^name=incident^label=${stateLabel}`
            });
            const state = getRecordFromArrayOfOne<IncidentColumn>(stateRecords.result);

            // Add the state value to the new ServiceNow incident
            requestBody.state = state.value;
        }

        // If 'description' is among the synced fields and one exists on the Jira Cloud issue, add it to the new ServiceNow incident as well
        if (SYNCED_FIELDS.includes('description')) {

            // Add the description to the new ServiceNow incident
            requestBody.description = issueFields.description?.toString() ?? undefined;
        }

        // Create a new Incident in ServiceNow and store the corresponding Jira issue key in its custom field
        const incident = await ServiceNow.Table.addRecord({
            tableName: 'incident',
            body: requestBody,
        });

        // Keep track that a corresponding incident has been created
        incidentCreated = true;

        // Try to extract the number of the newly created ServiceNow incident
        const incidentNumber = incident.result?.number;

        // If no such key can be obtained, throw an error
        if (!incidentNumber) {
            throw Error('Failed to retrieve incident number');
        }

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields();

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Add the number of the new ServiceNow incident to the Jira Cloud issue
        await JiraCloud.Issue.editIssue(
            {
                issueIdOrKey: event.issue.key,
                body: {
                    fields: {
                        [customField?.key ?? '']: incidentNumber
                    },
                }
            },
        );

        // If 'attachment' is among the synced fields and there are some on the Jira Cloud issue, add them to the new ServiceNow incident as well
        if (SYNCED_FIELDS.includes('attachments') && issueFields.attachment?.length) {

            // Define a variable for storing attachment ids
            const attachmentIdPairs: Record<string, number> = {};

            // Loop through all attachments on the issue
            for (const attachment of issueFields.attachment) {

                // Get the metadata for each attachment
                const metadata = await JiraCloud.Issue.Attachment.Metadata.getMetadata({ id: String(attachment.id) });

                // Find the content link from attachment metadata and store its content in SRC using the large attachment feature (In case the attachment exceeds 100MB)
                const storedAttachment = await JiraCloud.fetch(metadata.content ?? '', {
                    headers: {
                        'x-stitch-store-body': 'true'
                    }
                });

                // Check if the attachment content was fetched successfully
                if (!storedAttachment.ok) {
                    // If not, then throw an error
                    throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
                }

                // Obtain the id of the stored attachment from a response header
                const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');

                // If no id can be found, throw an error
                if (!storedAttachmentId) {
                    throw new Error('The attachment stored body was not returned');
                }

                // Upload the attachment to ServiceNow, using the same feature
                const attachmentResponse = await ServiceNow.fetch('/api/now/v1/attachment/upload', {
                    method: 'POST',
                    headers: {
                        'x-stitch-stored-body-id': storedAttachmentId,
                        'x-stitch-stored-body-form-data-file-name': `${metadata.filename}`,
                        'x-stitch-stored-body-form-data-file-identifier': 'uploadFile',
                        'x-stitch-stored-body-form-data-additional-fields': `table_name:incident;table_sys_id:${incident.result?.sys_id};`,
                    }
                });

                // Obtain the attachment from the response
                const uploadedAttachment = await attachmentResponse.json();

                // If an error is returned, log it out and throw an error
                if (uploadedAttachment.error) {
                    console.error(uploadedAttachment.error);
                    throw Error('Error uploading attachment to ServiceNow');
                }

                // Extract the id of the newly created attachment in ServiceNow
                const serviceNowAttachmentId = uploadedAttachment.result?.sys_id;

                // Should this id not be found, throw an error
                if (!serviceNowAttachmentId) {
                    throw Error(`Failed to retrieve attachment data from ServiceNow`);
                }

                // Push the id-pair into the variable
                attachmentIdPairs[serviceNowAttachmentId] = attachment.id
            }
            // Define a storage object for storing attachment ids
            const storage = new RecordStorage();

            // Store the attachment ids of in record storage as key-value pairs
            await storage.setValue(incidentNumber, attachmentIdPairs)
        }

        // If the operation is successful, log out the number of the created ServiceNow incident
        console.log(`Incident ${incidentNumber} created in ServiceNow`);

    } catch (e) {
        // If something goes wrong, create an appropriate error message
        const errorMessage = incidentCreated
            ? 'Incident created in ServiceNow, but syncing all fields has failed'
            : 'Error creating incident in ServiceNow';

        // Log out the error
        console.error(errorMessage, e);
    }
}


TypeScriptOnJiraCloudIssueDeleted

import JiraCloud from './api/jira/cloud';
import ServiceNow from './api/servicenow';
import { IssueDeletedEvent } from '@sr-connect/jira-cloud/events';
import { RecordStorage } from '@sr-connect/record-storage';
import { Incident } from './Types';
import { getEnvVars, getRecordFromArrayOfOne } from './Utils';

/**
 * This function deletes a corresponding ServiceNow incident when an issue in Jira Cloud is deleted.
 *
 * @param event Object that holds Issue Deleted event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueDeletedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the name of the custom field in Jira Cloud where the number of the corresponding ServiceNow incident is stored
    const { CustomFieldConfiguration: { JIRA_CLOUD_CUSTOM_FIELD_NAME } } = getEnvVars(context);

    try {
        // Get the current user
        const myself = await JiraCloud.Myself.getCurrentUser();

        // If the issue was created by the current user, log out a message, and stop the operation to avoid infinite loops
        if (myself.accountId === event.user.accountId) {
            console.log('Same user, skipping sync');
            return;
        }

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields();

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Obtain the number of the corresponding ServiceNow incident from the event
        const incidentNumber = event.issue.fields[customField.key];

        // If no such key exists in the event, log out a message and stop the operation
        if (!incidentNumber) {
            console.warn(`Number of the corresponding ServiceNow incident not stored on Jira Cloud issue ${event.issue.key}`);
            return;
        }

        // If it exists, use it to query for more data about the ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${incidentNumber}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Delete the corresponding ServiceNow incident
        await ServiceNow.All.deleteRecord({
            sys_id: incident.sys_id, tableName: 'incident'
        });

        // Delete all attachment ids from record storage if any exist for this issue-incident pair
        const storage = new RecordStorage();
        await storage.deleteValue(incidentNumber);

        // If the operation was successful, log out the incident number
        console.log(`Successfully deleted ServiceNow incident ${incidentNumber}`);
    } catch (error) {

        // If something goes wrong, log out the error
        console.error('Error deleting ServiceNow incident', error);
    }
}
TypeScriptOnJiraCloudIssueUpdated

import JiraCloud from "./api/jira/cloud";
import ServiceNow from './api/servicenow';
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { RecordStorage } from "@sr-connect/record-storage";
import { Incident, IncidentColumn, ServiceNowUser, UpdateIncidentRequestBody } from "./Types";
import { getEnvVars, getRecordFromArrayOfOne } from "./Utils";

/**
 * This function updates a corresponding ServiceNow incident when an issue in Jira Cloud is updated.
 *
 * @param event Object that holds Issue Updated event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueUpdatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            SERVICENOW_ADMIN_EMAIL,
        },
        FieldMappings: {
            STATUS_TO_STATE,
            PRIORITY_TO_URGENCY,
        },
        ServiceNowCloseNotesConfiguration: {
            ADD_INCIDENT_CLOSE_NOTES,
            INCIDENT_RESOLVED_STATE,
            INCIDENT_CLOSE_NOTES,
            INCIDENT_CLOSE_CODE,
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME,
        }
    } = getEnvVars(context);

    try {
        // Get the current user
        const myself = await JiraCloud.Myself.getCurrentUser();

        // If the issue was created by the current user, log out a message, and stop the operation to avoid infinite loops
        if (myself.accountId === event.user.accountId) {
            console.log('Same user, skipping sync');
            return;
        }

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields();

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Extract the number of the corresponding Service Now incident from the event
        const incidentNumber = event.issue.fields[customField.key];

        // Should the incident number be missing, log out a warning and stop the operation
        if (!incidentNumber) {
            console.warn(`Number of a corresponding ServiceNow incident not found on Jira Cloud issue ${event.issue.key}`);
            return;
        }

        // Use this number to query for the correct ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${incidentNumber}`,
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Get the updated issue fields in Jira Cloud
        const changedItems = event.changelog?.items ?? [];

        // Filter for those items that are included in the sync
        const changedSyncedItems = changedItems.filter(i => SYNCED_FIELDS.includes(i.field.toLocaleLowerCase()));

        // Define the request body for updating the ServiceNow incident
        const requestBody: UpdateIncidentRequestBody = {}; // Overwrite later

        // Loop through all the fields that have changed
        for (const item of changedSyncedItems) {
            switch (item.field) {
                case 'summary':
                    //Assign the issue summary as the short description on the incident
                    requestBody.short_description = item.toString ?? ''
                    break;
                case 'description':
                    // Assign the description to the incident
                    requestBody.description = item.toString ?? ''
                    break;
                case 'priority':
                    // Get the priority for the Jira Cloud issue
                    const priority = item.toString;

                    // Find the corresponding urgency record in ServiceNow
                    const urgencyLabel = priority && PRIORITY_TO_URGENCY[priority];

                    // If no such urgency is found, throw an error
                    if (!urgencyLabel) {
                        throw Error(`Corresponding urgency not found in ServiceNow for Jira Cloud priority: ${priority}`)
                    }

                    // Obtain more data about the urgency record from ServiceNow
                    const urgencyRecords = await ServiceNow.Table.getRecords({
                        tableName: 'sys_choice',
                        sysparm_query: `element=urgency^label=${urgencyLabel}`
                    });
                    const urgency = getRecordFromArrayOfOne<IncidentColumn>(urgencyRecords.result);

                    // Finally assign the urgency value to the request body
                    requestBody.urgency = urgency.value;

                    break;
                case 'status':
                    // Get the status for the Jira Cloud issue
                    const status = item.toString;

                    // Find the corresponding state value in ServiceNow
                    const stateLabel = status && STATUS_TO_STATE[status];

                    // If no such state is found, throw an error
                    if (!stateLabel) {
                        throw Error(`Corresponding state not found in ServiceNow for Jira Cloud status: ${status}`)
                    }

                    // Obtain more data about the state record from ServiceNow
                    const stateRecords = await ServiceNow.Table.getRecords({
                        tableName: 'sys_choice',
                        sysparm_query: `element=state^name=incident^label=${stateLabel}`
                    });
                    const state = getRecordFromArrayOfOne<IncidentColumn>(stateRecords.result);

                    // Finally assign the state value to the request body
                    requestBody.state = state.value

                    // The 'Resolved' state in ServiceNow by default requires close notes and a resolution code unless specified otherwise
                    if (ADD_INCIDENT_CLOSE_NOTES) {

                        // Check if the new state is the 'Resolved' state and if yes, add close notes and a resolution code to the request body
                        if (stateLabel === INCIDENT_RESOLVED_STATE) {
                            requestBody.close_notes = INCIDENT_CLOSE_NOTES;
                            requestBody.close_code = INCIDENT_CLOSE_CODE;
                        }
                    }
                    break;
                case 'reporter':
                    // Get the reporter id for the Jira Cloud issue
                    const newReporterId = item.to;

                    // Find the new reporter's user in Jira Cloud
                    const jiraUser = await JiraCloud.User.getUser({
                        accountId: newReporterId ?? ''
                    });

                    // Define the caller for the new ServiceNow incident
                    let caller: ServiceNowUser;

                    try {
                        // Check if the reporter's email matches with that of a ServiceNow user
                        const usersRecord = await ServiceNow.Table.getRecords({
                            tableName: 'sys_user',
                            sysparm_query: `email=${jiraUser.emailAddress}`,
                        });
                        caller = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

                    } catch (error) {

                        // If such a user does not exist log out a warning
                        console.warn(`Jira Cloud issue reporter does not have a corresponding user in ServiceNow, using the specified admin user instead as the incident caller`);

                        // Find the specified admin user in ServiceNow, 
                        const usersRecordWithMyEmail = await ServiceNow.Table.getRecords({
                            tableName: 'sys_user',
                            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
                            headers: {
                                'Content-type': 'application/json',
                            }
                        });
                        caller = getRecordFromArrayOfOne<ServiceNowUser>(usersRecordWithMyEmail.result)
                    }

                    // Assign the appropriate user to the incident as the caller
                    requestBody.caller_id = caller.sys_id
                    break;
                default:
                    break;
            }
        }

        // Update the incident
        if (requestBody) {
            await ServiceNow.Table.updateRecord({
                tableName: 'incident',
                sys_id: incident.sys_id,
                body: requestBody,
            });
        }

        // Check if 'attachment' is among the synced fields
        if (SYNCED_FIELDS.includes('attachments')) {

            // Check if attachments have been modified
            const change = changedItems.find(i => i.field === 'Attachment');

            // If yes, sync the attachments
            if (change) {

                // Define a storage object to obtain and store attachment ids
                const storage = new RecordStorage();

                // Check if the change on the Jira Cloud side has been the addition of an attachment
                if (change.to) {
                    // If yes, get the metadata for the new attachment
                    const metadata = await JiraCloud.Issue.Attachment.Metadata.getMetadata({ id: change.to });

                    // Find the content link from attachment metadata and store its content in SRC using the large attachment feature (In case the attachment exceeds 100MB)
                    const storedAttachment = await JiraCloud.fetch(metadata.content ?? '', {
                        headers: {
                            'x-stitch-store-body': 'true'
                        }
                    });

                    // Check if the attachment content was fetched successfully
                    if (!storedAttachment.ok) {
                        // If not, then throw an error
                        throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
                    }

                    // Obtain the id of the stored attachment from a response header
                    const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');

                    // If no id can be found, throw an error
                    if (!storedAttachmentId) {
                        throw new Error('The attachment stored body was not returned');
                    }

                    // Upload the attachment to ServiceNow, using the same feature
                    const attachmentResponse = await ServiceNow.fetch('/api/now/v1/attachment/upload', {
                        method: 'POST',
                        headers: {
                            'x-stitch-stored-body-id': storedAttachmentId,
                            'x-stitch-stored-body-form-data-file-name': `${metadata.filename}`,
                            'x-stitch-stored-body-form-data-file-identifier': 'uploadFile',
                            'x-stitch-stored-body-form-data-additional-fields': `table_name:incident;table_sys_id:${incident.sys_id};`,
                        }
                    });

                    // Obtain the attachment from the response
                    const attachment = await attachmentResponse.json();

                    // If an error is returned, log it out and throw an error
                    if (attachment.error) {
                        console.error(attachment.error);
                        throw Error('Error uploading attachment to ServiceNow');
                    }

                    // Extract the attachment id from the response
                    const attachmentId = (attachment as any).result?.sys_id;

                    // If no id cannot be found, throw an error
                    if (!attachmentId) {
                        throw Error(`Attachment created in ServiceNow, but failed to retrieve attachment data`);
                    }

                    // Get all stored attachment ids for the current issue - incident pair
                    const syncedAttachments = await storage.getValue<Record<string, string>>(incidentNumber);

                    // Store both attachment ids in record storage as a key-value pair
                    await storage.setValue(incidentNumber, { ...syncedAttachments, [attachmentId]: change.to });

                } else {
                    // Get all stored attachment ids for the current issue - incident pair
                    const syncedAttachments = await storage.getValue<Record<string, string>>(incidentNumber);

                    // If the change in Jira Cloud side has been the deletion of an attachment, find the corresponding file on the ServiceNow side, using record storage
                    const attachmentId = syncedAttachments && Object.keys(syncedAttachments).find(i => syncedAttachments[i] === change.from);

                    // If no corresponding attachment was found in ServiceNow, log out a warning and stop the operation
                    if (!attachmentId) {
                        console.warn(`No attachment found in ServiceNow for Jira Cloud issue attachment ${change.from}`);
                        return;
                    }

                    // Delete the ServiceNow attachment
                    await ServiceNow.Attachment.deleteAttachment({ sys_id: attachmentId });

                    // If other attachments are stored on this issue-incident pair, only remove the ids of the deleted attachments from the storage object
                    if (Object.keys(syncedAttachments).length > 1) {
                        await storage.setValue(incidentNumber, { ...syncedAttachments, [attachmentId]: undefined });
                    } else {
                        // If it was the last remaining attachment, delete the whole entry for this issue-incident pair
                        await storage.deleteValue(incidentNumber)
                    }
                }
            }
        }

        // If the operation is successful, log out a message
        console.log(`ServiceNow incident ${incidentNumber} synced with Jira Cloud issue ${event.issue.key}`);

    } catch (error) {
        // If something goes wrong, log out the error
        console.error('Error updating ServiceNow incident', error);
    }
}
TypeScriptOnServiceNowIncidentAttachmentCreated

import JiraCloud from './api/jira/cloud';
import ServiceNow from './api/servicenow';
import { RecordStorage } from '@sr-connect/record-storage';
import { Incident, IncidentAttachment, IncidentAttachmentCreatedEvent, IncidentColumn, ServiceNowUser } from './Types';
import { getEnvVars, getRecordFromArrayOfOne } from './Utils';
import { AddIssueAttachmentsResponseOK } from '@managed-api/jira-cloud-v3-core/types/issue/attachment';
import { AttachmentAsResponse } from '@managed-api/jira-cloud-v3-core/definitions/AttachmentAsResponse';

/**
 * This function uploads an attachment to the corresponding Jira Cloud issue when an attachment is created in ServiceNow
 *
 * @param event Object that holds Generic Event event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IncidentAttachmentCreatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            SERVICENOW_ADMIN_EMAIL,
        },
        CustomFieldConfiguration: {
            SERVICENOW_CUSTOM_COLUMN_NAME
        }
    } = getEnvVars(context);

    // If 'attachments' is not among the synced fields, log out a message and stop the operation
    if (!SYNCED_FIELDS.includes('attachments')) {
        console.log('Not syncing attachments');
        return;
    }

    // Keep track of when the corresponding attachment is created in Jira Cloud
    let attachmentCreated = false;

    try {
        // Get the user who authorised the ServiceNow connector.
        const usersRecord = await ServiceNow.Table.getRecords({
            tableName: 'sys_user',
            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

        // If the attachment was uploaded by this user, log out a message and stop the operation to avoid infinite loops
        if (user.sys_id === event.user) {
            console.log('Same user, skipping sync');
            return;
        }

        // Use the incident number from the event to obtain more data for the ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident', sysparm_query: `sys_id=${event.incident}`,
            headers: {
                'Content-type': 'application/json',
            },
            // For new incidents, ServiceNow fires this event before the incident has even been created, 
            // which will result in an error, so to avoid that we will need to explicitly return an empty result here
            errorStrategy: {
                handleHttp404Error: () => ({}),
            }
        });

        // If the query did not fetch any incidents, log out a message and stop the operation
        if (!incidentRecords?.result?.length) {
            console.log(`Incident not found or hasn't been created yet`);
            return;
        }

        // Extract the incident from query results
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Get the custom column where the corresponding Jira Cloud issue key is stored
        const columnRecords = await ServiceNow.Table.getRecords({
            tableName: 'sys_dictionary',
            sysparm_query: `name=incident^sys_name=${SERVICENOW_CUSTOM_COLUMN_NAME}`,
        });
        const column = getRecordFromArrayOfOne<IncidentColumn>(columnRecords.result);

        // Try to extract the corresponding Jira Cloud issue key from the incident
        const issueKey = incident[column.element];

        // If no such key exists, throw an erro
        if (!issueKey) {
            throw Error('No corresponding Jira Cloud issue key found on the ServiceNow incident');
        }

        // Use the attachment id from the event to fetch more data about the attachment that triggered the event
        const attachmentRecords = await ServiceNow.Attachment.getAttachments({
            sysparm_query: `sys_id=${event.attachment_id}`
        });
        const attachment = getRecordFromArrayOfOne<IncidentAttachment>(attachmentRecords.result);

        // Use a download link to fetch the content of the ServiceNow attachment and store its content in SRC using the large attachment feature (In case the attachment exceeds 100MB)
        const response = await ServiceNow.fetch(`api/now/v1/attachment/${event.attachment_id}/file`, {
            headers: {
                'x-stitch-store-body': 'true'
            }
        });

        // Get the stored attachment id from a response header
        const storedAttachmentId = response.headers.get('x-stitch-stored-body-id');

        // If no attachment id can be found, throw an error
        if (!storedAttachmentId) {
            throw new Error('The attachment stored body was not returned');
        }

        // Upload the attachment to Jira Cloud using the same feature
        const uploadedAttachmentResponse = await JiraCloud.fetch(`/rest/api/3/issue/${issueKey}/attachments`, {
            method: 'POST',
            headers: {
                'X-Atlassian-Token': 'no-check',
                'x-stitch-stored-body-id': storedAttachmentId,
                'x-stitch-stored-body-form-data-file-name': attachment.file_name,
            }
        });

        attachmentCreated = true;

        // Obtain the uploaded attachment from the response
        const jiraAttachmentRecords: AddIssueAttachmentsResponseOK = await uploadedAttachmentResponse.json();
        const jiraAttachment = getRecordFromArrayOfOne<AttachmentAsResponse>(jiraAttachmentRecords);

        // Define a storage object for storing attachment ids
        const storage = new RecordStorage();

        // Fetch all attachment ids for the current issue-incident pai
        const syncedAttachments = await storage.getValue<Record<string, string>>(event.incident);

        // Add the new attachment ids to record storage as a key-value pair
        await storage.setValue(event.incident, { ...syncedAttachments ?? {}, [event.attachment_id]: jiraAttachment.id });

        // If the operation is successful, log out the name of the uploaded file
        console.log(`Attachment ${attachment.file_name} uploaded to Jira Cloud issue ${issueKey}`);

    } catch (error) {
        // If something goes wrong, create an appropriate error message
        const errorMessage = attachmentCreated
            ? 'Attachment has been uploaded to Jira Cloud issue, but storing attachment ids has failed'
            : 'Error deleting attachment in Jira Cloud';

        //Log out the error
        console.error(errorMessage, error);
    }
}
TypeScriptOnServiceNowIncidentAttachmentDeleted

import JiraCloud from './api/jira/cloud';
import ServiceNow from './api/servicenow';
import { RecordStorage } from '@sr-connect/record-storage';
import { Incident, IncidentAttachmentDeletedEvent, IncidentColumn, ServiceNowUser } from './Types';
import { getEnvVars, getRecordFromArrayOfOne } from './Utils';

/**
 * This function deletes the corresponding attachment in Jira Cloud when an attachment is deleted in ServiceNow.
 *
 * @param event Object that holds Generic Event event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IncidentAttachmentDeletedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            SERVICENOW_ADMIN_EMAIL,
        },
        CustomFieldConfiguration: {
            SERVICENOW_CUSTOM_COLUMN_NAME,
        }
    } = getEnvVars(context);

    // If 'attachments' is not among the synced fields, log out a message and stop the operation
    if (!SYNCED_FIELDS.includes('attachments')) {
        console.log('Not syncing attachments');
        return;
    }

    try {
        // Get the user who has authorised the ServiceNow connector
        const usersRecord = await ServiceNow.Table.getRecords({
            tableName: 'sys_user',
            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

        // If the attachment was deleted by this user, log out a message and stop the operation to avoid infinite loops
        if (user.sys_id === event.user) {
            console.log('Same user, skipping sync');
            return;
        }

        // Use the incident number from the event to obtain more data for the ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident', sysparm_query: `sys_id=${event.incident}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Get the custom column where the corresponding Jira Cloud issue key is stored
        const columnRecords = await ServiceNow.Table.getRecords({
            tableName: 'sys_dictionary',
            sysparm_query: `name=incident^sys_name=${SERVICENOW_CUSTOM_COLUMN_NAME}`,
        });
        const column = getRecordFromArrayOfOne<IncidentColumn>(columnRecords.result);

        // Try to extract the corresponding Jira Cloud issue key from the incident
        const issueKey = incident[column.element];

        // If no such key exists, throw an error
        if (!issueKey) {
            throw Error('No corresponding Jira Cloud issue key found on the ServiceNow incident');
        }

        // Define a storage object to obtain the id of a corresponding attachment in Jira Cloud
        const storage = new RecordStorage();

        // Fetch all attachment ids for the current issue-incident pair
        const syncedAttachments = await storage.getValue<Record<string, string>>(event.incident) ?? {};

        // Find the id for the corresponding attachment in Jira Cloud, using record storage
        const jiraAttachmentId = syncedAttachments[event.attachment_id] ?? '';

        // Obtain more data about the corresponding attachment in Jira Cloud
        const attachmentMetadata = await JiraCloud.Issue.Attachment.Metadata.getMetadata({
            id: jiraAttachmentId,
            // ServiceNow sometimes fires this event multiple times in a row, resulting in an error,
            // so in order to avoid that, we need to explicitly return null here
            errorStrategy: {
                handleHttp404Error: () => null
            }
        });

        // If the attachment cannot be found in Jira Cloud, log out a message and stop the operation
        if (!attachmentMetadata) {
            console.log('Attachment not found or has already been deleted');
            return;
        }

        // Delete the attachment
        await JiraCloud.Issue.Attachment.deleteAttachment({ id: jiraAttachmentId, });

        // If other attachments are stored on this issue-incident pair, only remove the ids of the deleted attachments from the storage object
        if (Object.keys(syncedAttachments).length > 1) {
            await storage.setValue(event.incident, { ...syncedAttachments, attachmentId: undefined });
        } else {
            // If it was the last remaining attachment, delete the whole entry for this issue-incident pair
            await storage.deleteValue(event.incident)
        }

        // If the operation is successful, log out the name of the deleted file
        console.log(`Attachment ${attachmentMetadata.filename} deleted from Jira Cloud issue ${issueKey}`);

    } catch (error) {
        // If something goes wrong, log out the error
        console.error('Error while deleting attachment', error);
    }
}
TypeScriptOnServiceNowIncidentCreated

import JiraCloud from "./api/jira/cloud";
import ServiceNow from './api/servicenow';
import { IssueFieldsCreate } from "@managed-api/jira-cloud-v3-core/definitions/IssueFields";
import { UserAsResponse } from "@managed-api/jira-cloud-v3-core/definitions/UserAsResponse";
import { Incident, IncidentColumn, IncidentCreatedEvent, ServiceNowUser } from "./Types";
import { createParagraphInADF, getEnvVars, getRecordFromArrayOfOne } from "./Utils";

/**
 * This function creates a new issue in Jira Cloud when an incident is created in ServiceNow
 *
 * @param event Object that holds Generic Event event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IncidentCreatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            SERVICENOW_ADMIN_EMAIL,
            JIRA_CLOUD_PROJECT_KEY,
            JIRA_CLOUD_ISSUE_TYPE,
        },

        FieldMappings: {
            STATE_TO_STATUS,
            URGENCY_TO_PRIORITY,
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME,
            SERVICENOW_CUSTOM_COLUMN_NAME,
        }
    } = getEnvVars(context);

    // Keep track of when the corresponding issue is created in Jira Cloud
    let issueCreated = false;

    try {
        // Get the user who authorised the ServiceNow connector
        const adminUsersRecord = await ServiceNow.Table.getRecords({
            tableName: 'sys_user',
            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const adminUser = getRecordFromArrayOfOne<ServiceNowUser>(adminUsersRecord.result);

        // If the incident was created by this user, log out a message and stop the operation to avoid infinite loops
        if (adminUser.sys_id === event.user) {
            console.log('Same user, skipping sync');
            return;
        }

        // Use the incident number from the event to query for the correct ServiceNow incident
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${event.trigger}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields();

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Define the fields for creating the new Jira issue, also passing the incident number into a custom field on Jira Cloud side
        const issueFields: IssueFieldsCreate = {
            issuetype: {
                name: JIRA_CLOUD_ISSUE_TYPE
            },
            project: {
                key: JIRA_CLOUD_PROJECT_KEY,
            },
            summary: '', // Overwrite later
            [customField.key]: event.trigger,
        }

        // If 'summary' is among the synced fields, assign it to the short description property on the corresponding ServiceNow incident
        if (SYNCED_FIELDS.includes('summary')) {
            issueFields.summary = incident.short_description;
        }

        // If 'reporter' is among the synced fields, find a user in Jira Cloud who will be added to that role on the new issue
        if (SYNCED_FIELDS.includes('reporter')) {

            // Get the user in ServiceNow who created the incident
            const serviceNowUser = await ServiceNow.Table.getRecord({
                tableName: 'sys_user',
                sys_id: incident.caller_id.value,
            });

            // Search for a user in Jira cloud with a matching email
            const jiraUserRecords = await JiraCloud.User.Search.findUsers({ query: serviceNowUser.result?.email });
            const jiraUser = jiraUserRecords.length === 1 ? getRecordFromArrayOfOne<UserAsResponse>(jiraUserRecords) : undefined;

            // If such a user exists in Jira Cloud, add them to the request body as the reporter 
            if (jiraUser?.accountId) {
                issueFields.reporter = { accountId: jiraUser.accountId };
            } else {
                // If no such user is found, log out a warning and use the current user instead
                console.warn(`User with email ${serviceNowUser.result?.email} does not exist in Jira Cloud, adding the current user as the reporter instead`);

                // Get the current user
                const myself = await JiraCloud.Myself.getCurrentUser();

                // Should there be a problem with fetching the current user's account id, throw an error
                if (!myself.accountId) {
                    throw Error('Account id not found for the current user in Jira Cloud');
                }

                // Add the user to the Jira Cloud issue as the reporter
                issueFields.reporter = { accountId: myself.accountId };
            }
        }

        // If 'priority' is among the synced fields, find the name of a priority in Jira Cloud corresponding to the urgency of the ServiceNow incident that triggered the event
        if (SYNCED_FIELDS.includes('priority')) {

            // Obtain more data about the urgency record from ServiceNow
            const urgencyRecords = await ServiceNow.Table.getRecords({
                tableName: 'sys_choice',
                sysparm_query: `element=urgency^value=${incident.urgency}`
            });
            const urgency = getRecordFromArrayOfOne<IncidentColumn>(urgencyRecords.result);

            // Using the urgency label, find a corresponding priority for Jira Cloud
            const priorityName = URGENCY_TO_PRIORITY[urgency.label];

            // If no corresponding priority is found, throw an error
            if (!priorityName) {
                throw Error(`No corresponding priority found in Jira Cloud for the ServiceNow incident urgency ${urgency.label}`)
            }

            // Add the priority to the new Jira Cloud issue
            issueFields.priority = { name: priorityName }
        }

        // If 'description' is among the synced fields, check if there is one added to the corresponding ServiceNow incident
        if (SYNCED_FIELDS.includes('description')) {
            if (incident.description) {
                // Add the description to the new Jira Cloud issue
                issueFields.description = createParagraphInADF(incident.description);
            }
        }

        // Create the new Issue in Jira Cloud
        const newIssue = await JiraCloud.Issue.createIssue({
            body: {
                fields: issueFields,
            }
        });
        issueCreated = true;

        // Should the newly created issue not have a key, throw an error
        if (!newIssue.key) {
            throw Error('No key found for the newly created Jira Cloud issue')
        }

        // Get the custom column where the corresponding Jira Cloud issue key will be stored
        const columnRecords = await ServiceNow.Table.getRecords({
            tableName: 'sys_dictionary',
            sysparm_query: `name=incident^sys_name=${SERVICENOW_CUSTOM_COLUMN_NAME}`,
        });
        const column = getRecordFromArrayOfOne<IncidentColumn>(columnRecords.result);

        // Add the Jira Cloud issue key to a custom field of the ServiceNow incident that triggered the event
        await ServiceNow.Table.updateRecord({
            tableName: 'incident',
            sys_id: incident.sys_id,
            body: {
                [column.element]: newIssue.key,
            }
        });

        // Only continue the operation if the synced fields include 'comment', 'status' or 'attachment'
        if (SYNCED_FIELDS.some((f) => f === 'comments' || f === 'status' || f === 'attachments')) {

            // Obtain more data for the new issue in Jira Cloud
            const issue = await JiraCloud.Issue.getIssue({
                issueIdOrKey: newIssue.key,
            });

            // If 'status' is among the synced fields, transition the Jira Cloud issue to the corresponding workflow status
            if (SYNCED_FIELDS.includes('status')) {

                // Obtain more data about the label record from ServiceNow
                const stateRecords = await ServiceNow.Table.getRecords({
                    tableName: 'sys_choice',
                    sysparm_query: `element=state^name=incident^value=${incident.state}`
                });
                const state = getRecordFromArrayOfOne<IncidentColumn>(stateRecords.result);

                // Using the state label, find a corresponding status name for the Jira Cloud issue
                const statusName = STATE_TO_STATUS[state.label];

                // If no corresponding status is found, throw an error
                if (!statusName) {
                    throw Error(`No status id found in Jira Cloud for the corresponding ServiceNow incident state ${state.label}`)
                }

                // Check if the newly created Jira Cloud issue already has the correct status
                if (issue.fields?.status?.name !== statusName) {

                    // If not, find an appropriate workflow transition in order to change the status
                    const transitions = await JiraCloud.Issue.Transition.getTransitions({
                        issueIdOrKey: newIssue.key,
                        expand: 'transition.fields'
                    });
                    const transition = transitions.transitions?.find(t => t.to?.name === statusName);

                    if (!transition?.id) {
                        throw Error(`No transition found for assigning the workflow status ${statusName} to the Jira Cloud issue`)
                    }

                    // Perform the workflow transition in order to match the status of the Jira Cloud issue to the state of the ServiceNow incident
                    await JiraCloud.Issue.Transition.performTransition({
                        issueIdOrKey: newIssue.key,
                        body: {
                            transition: {
                                id: transition.id
                            },
                        }
                    });
                }
            }

            // If 'attachments' is among the synced fields and there are some on the ServiceNow incident, add them to the Jira Cloud issue as well
            if (SYNCED_FIELDS.includes('attachments')) {

                // Get all attachments on the newly created ServiceNow incident
                const attachments = await ServiceNow.Attachment.getAttachments({
                    sysparm_query: `table_name=incident^table_sys_id=${incident.sys_id}`
                });

                if (attachments.result.length) {
                    // If there are any, then define the request body for adding files to the Jira Cloud issue
                    const requestBody = [];

                    // Loop through all the attachments on the ServiceNow incident
                    for (const attachment of attachments.result) {

                        // Fetch the attachment content
                        const content = await ServiceNow.fetch(attachment.download_link ?? '');

                        // Turn the file content into an array buffer
                        const arrayBuffer = await content.arrayBuffer();

                        // Add the array buffer to the request body along with the file name
                        requestBody.push({ content: arrayBuffer, fileName: attachment.file_name ?? '' });
                    }

                    // Upload all found attachments to the Jira Cloud issue
                    await JiraCloud.Issue.Attachment.addAttachments({
                        issueIdOrKey: newIssue.key,
                        body: requestBody
                    });

                }
            }

            // If the synced fields include 'comments', check if the ServiceNow incident has any
            if (SYNCED_FIELDS.includes('comments')) {

                // Get comments from the ServiceNow incident that triggered the event
                const comments = await ServiceNow.Table.getRecords({
                    tableName: 'sys_journal_field',
                    sysparm_query: `element=comments^element_id=${incident.sys_id}`,
                    headers: {
                        'Content-type': 'application/json',
                    },
                    // ServiceNow throws a 404 error if no comments are found,
                    // so we explicitly need to return an empty result ourselves in this scenario
                    errorStrategy: {
                        handleHttp404Error: () => ({ result: [] }),
                    }
                });

                // If any comments were found on the incident, add them to the Jira Cloud issue
                if (comments && comments.result.length) {

                    // Get the user who triggered the event
                    const usersRecord = await ServiceNow.Table.getRecords({
                        tableName: 'sys_user',
                        sysparm_query: `sys_id=${event.user}`,
                        headers: {
                            'Content-type': 'application/json',
                        }
                    });
                    const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

                    // Loop through all the found comments
                    for (const comment of comments.result) {

                        // Add the comment to the Jira Cloud issue
                        await JiraCloud.Issue.Comment.addComment({
                            issueIdOrKey: newIssue.key,
                            body: {
                                body: createParagraphInADF(`${user.name}: ${comment.value}`)
                            }
                        });
                    }
                }
            }

            // If the operation was successful, log out the key for the new Jira Cloud issue
            console.log(`Issue ${newIssue.key} created in Jira Cloud`);
        }
    } catch (error) {
        // If something goes wrong, create an appropriate error message
        const errorMessage = issueCreated
            ? 'Issue created in Jira Cloud, but syncing all fields has failed'
            : 'Error creating Jira Cloud issue';

        // Log out the error
        console.error(errorMessage, error);
    }
}
TypeScriptOnServiceNowIncidentDeleted

import JiraCloud from './api/jira/cloud';
import ServiceNow from './api/servicenow';
import { IncidentDeletedEvent, ServiceNowUser } from './Types';
import { getEnvVars, getRecordFromArrayOfOne } from './Utils';

/**
 * This function deletes a corresponding issue in Jira Cloud when an incident is deleted in ServiceNow.
 *
 * @param event Object that holds Generic Event event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IncidentDeletedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant for this script
    const {
        BasicConfiguration: {
            SERVICENOW_ADMIN_EMAIL,
        },
    } = getEnvVars(context);

    try {
        // Get the user who authorised the ServiceNow connector
        const usersRecord = await ServiceNow.Table.getRecords({
            tableName: 'sys_user',
            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

        // If the incident was deleted by this user, log out a message and stop the operation to avoid infinite loops
        if (user.sys_id === event.user) {
            console.log('Same user, skipping sync');
            return;
        }

        // Try to obtain the corresponding Jira Cloud issue key from the event
        const issueKey = event.jira_cloud_issue_key;

        // If such a key was not found, log out a message and stop the operation
        if (!issueKey) {
            throw Error('Corresponding issue key not stored on the ServiceNow incident');
        }

        // If the key exists, use it to delete the corresponding Jira issue
        await JiraCloud.Issue.deleteIssue({ issueIdOrKey: issueKey });

        // If the operation was successful, log out the key of the deleted Jira Cloud issue
        console.log(`Issue with key ${issueKey} deleted in Jira Cloud`);
    } catch (error) {

        // If something goes wrong, log out the error
        console.error('Error deleting issue', error);
    }
}
TypeScriptOnServiceNowIncidentUpdated

import JiraCloud from "./api/jira/cloud";
import ServiceNow from './api/servicenow';
import { IssueFieldsUpdate } from '@managed-api/jira-cloud-v3-core/definitions/IssueFields';
import { UserAsResponse } from "@managed-api/jira-cloud-v3-core/definitions/UserAsResponse";
import { Incident, IncidentColumn, IncidentUpdatedEvent, ServiceNowUser } from "./Types";
import { createParagraphInADF, getEnvVars, getRecordFromArrayOfOne } from "./Utils";

/**
 * This function updates an issue in Jira Cloud when an incident is updated in ServiceNow.
 *
 * @param event Object that holds Generic Event event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IncidentUpdatedEvent, context: Context): Promise<void> {
    if (context.triggerType === 'MANUAL') {
        console.error('This script is designed to be triggered externally. If you need to trigger the script manually, consider emulating the event with a test payload instead.');
        return;
    }

    // Get the environment variables relevant to this script
    const {
        BasicConfiguration: {
            SYNCED_FIELDS,
            SERVICENOW_ADMIN_EMAIL,
        },
        FieldMappings: {
            STATE_TO_STATUS,
            URGENCY_TO_PRIORITY
        },
        CustomFieldConfiguration: {
            JIRA_CLOUD_CUSTOM_FIELD_NAME,
            SERVICENOW_CUSTOM_COLUMN_NAME,
        }
    } = getEnvVars(context);

    try {
        // Get the user who authorised the ServiceNow connector
        const adminUsersRecord = await ServiceNow.Table.getRecords({
            tableName: 'sys_user',
            sysparm_query: `email=${SERVICENOW_ADMIN_EMAIL}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const adminUser = getRecordFromArrayOfOne<ServiceNowUser>(adminUsersRecord.result);

        // If the incident was deleted by this user, log out a message and stop the operation to avoid infinite loops
        if (adminUser.sys_id === event.user) {
            console.log('Same user, skipping sync');
            return;
        }

        // Use the incident number from the event to query for the ServiceNow incident that triggered the event
        const incidentRecords = await ServiceNow.Table.getRecords({
            tableName: 'incident',
            sysparm_query: `number=${event.trigger}`,
            headers: {
                'Content-type': 'application/json',
            }
        });
        const incident = getRecordFromArrayOfOne<Incident>(incidentRecords.result);

        // Get the custom column where the corresponding Jira Cloud issue key is stored
        const columnRecords = await ServiceNow.Table.getRecords({
            tableName: 'sys_dictionary',
            sysparm_query: `name=incident^sys_name=${SERVICENOW_CUSTOM_COLUMN_NAME}`,
        });
        const column = getRecordFromArrayOfOne<IncidentColumn>(columnRecords.result);

        // Extract the corresponding Jira Cloud issue key from the incident
        const issueKey = incident[column.element];

        // If no such key exists, throw an error
        if (!issueKey) {
            throw Error('No corresponding Jira Cloud issue key stored on the ServiceNow incident');
        }

        // Get all custom issue fields in Jira Cloud
        const customFields = await JiraCloud.Issue.Field.getFields();

        // Find the custom field where the ServiceNow incident number is stored
        const customField = customFields.find(f => f.name === JIRA_CLOUD_CUSTOM_FIELD_NAME);

        // If no such field is found, throw an error
        if (!customField?.key) {
            throw Error(`Custom field ${JIRA_CLOUD_CUSTOM_FIELD_NAME} not found in Jira Cloud`);
        }

        // Store the request body or updating the corresponding issue, while also passing the Jira Cloud issue key into a custom field on the ServiceNow side
        let issueFields: IssueFieldsUpdate = { [customField.key]: event.trigger };

        // If 'summary' is among the synced fields, and if short_description has changed on the incident, add it to the Jira Cloud issue as the summary
        if (SYNCED_FIELDS.includes('summary') && event.short_description) {
            issueFields.summary = event.short_description;
        }

        // If 'description' is among the synced fields and it has changed on the incident, convert it to ADF format and add it to the Jira Cloud issue
        if (SYNCED_FIELDS.includes('description') && event.description) {
            issueFields.description = createParagraphInADF(event.description);
        }

        // If 'priority' is among the synced fields and if the incident urgency has changed, assign a corresponding priority to the Jira Cloud issue
        if (SYNCED_FIELDS.includes('priority') && event.urgency) {

            // Obtain more data about the urgency record from ServiceNow
            const urgencyRecords = await ServiceNow.Table.getRecords({
                tableName: 'sys_choice',
                sysparm_query: `element=urgency^value=${event.urgency}`
            });
            const urgency = getRecordFromArrayOfOne<IncidentColumn>(urgencyRecords.result);

            // Using the urgency label, find a corresponding priority for Jira Cloud
            const priorityName = URGENCY_TO_PRIORITY[urgency.label];

            // If no such priority is found, throw an error
            if (!priorityName) {
                throw Error(`Corresponding priority not found in Jira Cloud for ServiceNow urgency ${urgency.label}`)
            }

            // Assign the priority to the issue in Jira Cloud
            issueFields.priority = { name: priorityName };
        }

        // If 'reporter' is among the synced fields and if the incident caller has changed, assign a corresponding user to the Jira Cloud issue as the reporter
        if (SYNCED_FIELDS.includes('reporter') && event.caller_id) {

            // Get the user in ServiceNow who updated the incident
            const caller = await ServiceNow.Table.getRecord({
                tableName: 'sys_user',
                sys_id: event.caller_id,
            });

            // Search for a user in Jira cloud with a matching email
            const jiraUserRecords = await JiraCloud.User.Search.findUsers({ query: caller.result?.email });
            const jiraUser = getRecordFromArrayOfOne<UserAsResponse>(jiraUserRecords);

            // If such a user is found, obtain the user id
            if (jiraUser?.accountId) {

                // Assign the user to the Jira Cloud issue as a reporter
                issueFields.reporter = { accountId: jiraUser.accountId };
            } else {
                // If no such user is found, log out a warning
                console.warn(`User with email ${caller.result?.email} does not exist in Jira Cloud, adding the current user as the reporter instead`);

                // Get the current user
                const myself = await JiraCloud.Myself.getCurrentUser();

                // Should the account id not be available for the current user, throw an error
                if (!myself.accountId) {
                    throw Error(`Account id missing for the current user in Jira Cloud`);
                }

                // Assign the current user to the Jira Cloud issue as a reporter
                issueFields.reporter = { accountId: myself.accountId };
            }
        }

        // Update the issue Jira Cloud with all the mapped fields
        await JiraCloud.Issue.editIssue({
            issueIdOrKey: issueKey,
            body: {
                fields: issueFields,
            }
        });

        // If 'status' is among the synced fields and the incident state has changed, assign a corresponding status to the issue in Jira Cloud
        if (SYNCED_FIELDS.includes('status') && event.state) {

            // Obtain more data about the label record from ServiceNow
            const stateRecords = await ServiceNow.Table.getRecords({
                tableName: 'sys_choice',
                sysparm_query: `element=state^name=incident^value=${event.state}`
            });
            const state = getRecordFromArrayOfOne<IncidentColumn>(stateRecords.result);

            // Using the state label, find a corresponding status name for the Jira Cloud issue
            const statusName = STATE_TO_STATUS[state.label];

            // If no such status is found, throw an error
            if (!statusName) {
                throw Error(`Corresponding status not found in Jira Cloud for ServiceNow state: ${state.label}`);
            }

            // In order to update the status of the Jira Issue, find the necessary workflow transition
            const transitions = await JiraCloud.Issue.Transition.getTransitions({
                issueIdOrKey: issueKey,
            });
            const transition = transitions.transitions?.find(t => t.to?.name === statusName);

            // If no such transition is found throw an error
            if (!transition?.id) {
                throw Error(`No transition found for assigning the workflow status ${statusName} to the Jira Cloud issue`);
            }

            // Finally perform the workflow transition to match the status of the Jira Cloud issue to that of the ServiceNow incident
            await JiraCloud.Issue.Transition.performTransition({
                issueIdOrKey: issueKey,
                body: {
                    transition: {
                        id: transition.id
                    }
                }
            });
        }

        // If 'comment' is among the synced fields and one has been added to the incident, copy it to the issue in Jira Cloud
        if (SYNCED_FIELDS.includes('comments') && event.comments) {

            // Get the user who created the comment
            const usersRecord = await ServiceNow.Table.getRecords({
                tableName: 'sys_user',
                sysparm_query: `sys_id=${event.user}`,
                headers: {
                    'Content-type': 'application/json',
                }
            });
            const user = getRecordFromArrayOfOne<ServiceNowUser>(usersRecord.result);

            // Add the comment to the Jira Cloud issue
            await JiraCloud.Issue.Comment.addComment({
                issueIdOrKey: issueKey,
                body: {
                    body: createParagraphInADF(`${user.name}: ${event.comments}`)
                }
            });
        }

        // If the operation is successful, log out a message
        console.log(`ServiceNow incident ${event.trigger} synced with Jira Cloud issue ${issueKey}`);

    } catch (error) {
        // If something goes wrong, log out the error
        console.error('Error updating Jira Cloud issue', error);
    }
}
TypeScriptTypes

export interface IncidentCreatedEvent {
    eventType: string;
    title: string;
    user: string;
    trigger: string;
}

export interface IncidentUpdatedEvent {
    eventType: string;
    title: string;
    user: string;
    trigger: string;
    short_description: string;
    description: string;
    urgency: string;
    state: string;
    caller_id: string;
    comments: string;
}

export interface IncidentDeletedEvent {
    eventType: string;
    title: string;
    user: string;
    trigger: string;
    jira_cloud_issue_key: string;
}

export interface IncidentAttachmentCreatedEvent {
    eventType: string;
    title: string;
    user: string;
    attachment_id: string;
    incident: string;
}

export interface IncidentAttachmentDeletedEvent {
    eventType: string;
    title: string;
    user: string;
    attachment_id: string;
    incident: string;
}

export interface CreateIncidentRequestBody {
    short_description: string;
    description?: string;
    state?: string;
    urgency?: string;
    caller_id?: string;
    [x: string]: any;
}

export interface UpdateIncidentRequestBody {
    short_description?: string;
    description?: string;
    state?: number | string;
    urgency?: string;
    caller_id?: string;
    close_notes?: string;
    close_code?: string;
}

export interface Incident {
    caller_id: {
        value: string;
    };
    description?: string;
    priority: string;
    short_description: string;
    state: string;
    sys_id: string;
    urgency: string;
    [x: string]: any;
}

export interface IncidentAttachment {
    file_name: string;
}

export interface IncidentColumn {
    element: string;
    label: string;
    value: string;
}

export interface ServiceNowUser {
    email: string;
    name: string;
    sys_id: string;
}

export interface EnvVars {
    BasicConfiguration: {
        SYNCED_FIELDS: string[];
        JIRA_CLOUD_PROJECT_KEY: string;
        JIRA_CLOUD_ISSUE_TYPE: string;
        SERVICENOW_ADMIN_EMAIL: string;
        INCIDENT_DEFAULT_SHORT_DESCRIPTION: string;
    };
    FieldMappings: {
        STATUS_TO_STATE: Record<string, string>;
        STATE_TO_STATUS: Record<string, string>;
        PRIORITY_TO_URGENCY: Record<string, string>;
        URGENCY_TO_PRIORITY: Record<string, string>;
    };
    CustomFieldConfiguration: {
        JIRA_CLOUD_CUSTOM_FIELD_NAME: string;
        SERVICENOW_CUSTOM_COLUMN_NAME: string;
    };
    ServiceNowCloseNotesConfiguration: {
        ADD_INCIDENT_CLOSE_NOTES: boolean;
        INCIDENT_RESOLVED_STATE: string;
        INCIDENT_CLOSE_NOTES: string;
        INCIDENT_CLOSE_CODE: string;
    };
}
TypeScriptUtils

import { EnvVars } from "./Types";

// Function that constructs a paragraph in ADF (Atlassian Document Format)
export const createParagraphInADF = (text: string) => {
    return {
        type: 'doc' as const,
        version: 1 as const,
        content: [
            {
                type: 'paragraph' as const,
                content: [
                    {
                        type: 'text' as const,
                        text
                    }
                ]
            }
        ]
    };
};

// Function that checks if an array of query results only consists of one record and if yes, returns it, if not, throws an error
export function getRecordFromArrayOfOne<T>(records: Record<string, any>[]): T {
    if (!records.length) {
        throw Error('No records found.');
    }
    if (records.length !== 1) {
        throw Error('Unique record not found.')
    }
    return records[0] as T;
}

// Function to retrieve environment variables from the context object
export function getEnvVars(context: Context) {
    return context.environment.vars as EnvVars;
}
Documentation ยท Support ยท Suggestions & feature requests