Template Content
Scripts
Not the template you're looking for? Browse more.
About the template
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
Template Content
Scripts
This template demonstrates how you can set up a basic sync between Microsoft Dynamics 365 and Jira Cloud. This template will create a new issue in Jira Cloud when a new case is created in Microsoft Dynamics, copying over some basic fields. Comments are synced bi-directionally. When attachment in Microsoft Dynamics side is included in a comment, this will be copied over as well. When issue is transitioned in Jira Cloud side the corresponding case in Dynamics 365 will be resolved. This is a very basic example and is not intended to be out-of-the-box template to be used, but mainly exists for learning purposes.
Go to Azure Portal https://portal.azure.com/#home
Select the menu in the far top left
Access Entra
In the Menu item on the lefthand side, select "App registrations"
Select 'New Registration'
Give it a name
Select 'Accounts in any organizational directory (Any Azure AD directory Multitenant)'
Come back to ScriptRunner Connect, and get the URL from the Oauth/GetAccessToken Generic Event Listener.
Select 'Web' in the dropdown for Redirect URI
In the endpoint field, enter the URL from the Generic Event Listener
Get the Client ID and Tenant ID field from the application, and enter them into the parameters.
Go to 'Certificates & Secrets'
Select to create 'New client secret'
Give it a description, and give the secret a suitable length.
Copy the value, and enter that into the CLIENT_SECRET parameter field (Be careful, once you leave this page the value will be obscured)
Go to 'API permissions'
Select to 'Add a permission'
Select 'Dynamics CRM'
Select 'Delegated permissions' and 'user_impersonation'
'Add permissions'
Setup with the Scheduled Trigger to run every hour
Configure the Scheduled Trigger to execute the OAuth/RefreshAccessToken
Do not enable the trigger yet.
Change type: Added
Table name: Cases
Scope: Organisation (this can be changed for your needs)
URI: Enter the URL from the Generic Event Listener
Method: POST
Headers: Content*Type & application/json
Body:
" {
" "caseTitle": @{triggerOutputs()?['body/title']},
" "caseId": @{triggerOutputs()?['body/incidentid']},
" "caseDescription": @{triggerOutputs()?['body/description']},
" "caseReporter": @{triggerOutputs()?['body/_createdby_value']},
" "caseDropdown": @{triggerOutputs()?['body/new_customsingleselect']},
" "caseDueDate": @{triggerOutputs()?['body/resolveby']}
" }
Change type: Added
Table name: Notes
Scope: Organisation (this can be changed for your needs)
URI: Enter the URL from the Generic Event Listener
Method: POST
Headers: Content*Type & application/json
Body:
" {
" "commentID": @{triggerOutputs()?['body/annotationid']},
" "commentTitle": @{triggerOutputs()?['body/subject']},
" "commentText": @{triggerOutputs()?['body/notetext']},
" "commentCreator": @{triggerOutputs()?['body/_createdby_value']},
" "incidentId": @{triggerOutputs()?['body/_objectid_value']},
" "containsAttachment": @{triggerOutputs()?['body/isdocument']},
" "commentDocument": @{triggerOutputs()?['body/documentbody']},
" "commentFileName": @{triggerOutputs()?['body/filename']},
" "commentMimeType": @{triggerOutputs()?['body/mimetype']}
" }
Follow the instructions, and add the Webhook to the transition that closes the issue.
Follow the instructions. Optionally add a JQL query to filter the amount of events sent to SRC.
We need to trigger the oauth process for this template. Manually execute the 'GenerateUrl' script and copy the URL from the console. Ensure you are logged in as the user that you want to use to communicate with 365 Navigate the to the URL you have copied and Accept permissions You will see 'Access & refresh token saved in Record Storage.' in the console log. You now have the access token stored in secure local Storage Now we will need to keep the token refreshing, so now we can enable the scheduled trigger
export default async function(event: any, context: Context<EV>): Promise<void> {
const { CALLBACK_URL, CLIENT_ID, TENANT_ID, SCOPES} = context.environment.vars.Dynamics365OAuth;
const tenantId = encodeURIComponent(TENANT_ID);
const clientId = encodeURIComponent(CLIENT_ID);
const callbackUrl = encodeURIComponent(CALLBACK_URL);
const scope = encodeURIComponent(SCOPES.join(' '));
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${callbackUrl}&response_mode=query&scope=${scope}`
console.log(url);
}
import { HttpEventRequest } from '@sr-connect/generic-app/events/http';
import { RecordStorage } from '@sr-connect/record-storage';
export default async function (event: HttpEventRequest, context: Context<EV>): Promise<void> {
const { CALLBACK_URL, CLIENT_ID, CLIENT_SECRET, TENANT_ID } = context.environment.vars.Dynamics365OAuth;
const tenantId = encodeURIComponent(TENANT_ID);
const clientId = encodeURIComponent(CLIENT_ID);
const clientSecret = encodeURIComponent(CLIENT_SECRET);
const callbackUrl = encodeURIComponent(CALLBACK_URL);
const code = event.queryStringParams.code;
// Check if code query param is included in response
if (!code) {
throw new Error('No code in event payload.');
}
const storage = new RecordStorage({ secure: true });
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('code', code);
params.append('redirect_uri', CALLBACK_URL);
console.log(callbackUrl)
const response = await fetch<OAuthCallbackResponse>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
if (!response.ok) {
throw Error(`Unexpected error while processing Dynamics 365 OAuth callback: ${response.text()} - ${await response.text()}`);
}
const body = await response.json();
storage.setValue('dyanmics_365_access_token', body.access_token);
storage.setValue('dyanmics_365_refresh_token', body.refresh_token);
console.log('Access & refresh token saved in Record Storage.');
}
interface OAuthCallbackResponse {
readonly access_token: string;
readonly refresh_token: string;
}
import { RecordStorage } from '@sr-connect/record-storage';
export default async function (event: any, context: Context<EV>): Promise<void> {
const { CLIENT_ID, CLIENT_SECRET, TENANT_ID } = context.environment.vars.Dynamics365OAuth;
const storage = new RecordStorage({ secure: true })
const refreshToken = await storage.getValue('dyanmics_365_refresh_token') as string;
const tenantId = encodeURIComponent(TENANT_ID);
const clientId = encodeURIComponent(CLIENT_ID);
const clientSecret = encodeURIComponent(CLIENT_SECRET);
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('refresh_token', refreshToken);
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
const response = await fetch<OAuthRefreshResponse>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
})
if (!response.ok) {
throw Error(`Unexpected response while refreshing Dynamics 365 OAuth access token: ${response.status} - ${await response.text()}`);
}
const body = await response.json()
storage.setValue('dyanmics_365_access_token', body.access_token);
storage.setValue('dyanmics_365_refresh_token', body.refresh_token);
console.log('Access & refresh token saved in Record Storage.');
}
interface OAuthRefreshResponse {
readonly access_token: string;
readonly refresh_token: string;
}
import { HttpEventRequest, isJSON } from '@sr-connect/generic-app/events/http';
import { DynamicsCase } from './Utils/Dynamics365';
import { createJiraIssueFromDynamicsCase } from './Utils/Jira';
export default async function(event: HttpEventRequest, context: Context<EV>): Promise<void> {
if (!isJSON(event)) {
throw Error('Incoming event is not in JSON format');
}
const caseData: DynamicsCase = event.body;
await createJiraIssueFromDynamicsCase(context, caseData);
}
import JiraCloud from './api/jira/cloud';
import { HttpEventRequest, isJSON } from '@sr-connect/generic-app/events/http';
import { DynamicsAttachment, copyAttachmentFromDynamicsToJira, getCaseJiraTicketId, getUserEmailAddress } from './Utils/Dynamics365';
import { createCommentParagraph } from './Utils/Jira';
export default async function (event: HttpEventRequest, context: Context<EV>): Promise<void> {
if (!isJSON(event)) {
throw Error('Incoming event is not in JSON format');
}
const commentData: CommentEvent = event.body;
const dynamicsConnectorEmail = context.environment.vars.Connector.CONNECTOR_EMAIL_DYNAMICS;
const creatorEmail = await getUserEmailAddress(context, commentData.commentCreator);
if (creatorEmail === dynamicsConnectorEmail) {
console.log("Ignoring comment to avoid event-loop.");
return;
}
const issueKey = await getCaseJiraTicketId(context, commentData.incidentId);
await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: issueKey,
body: {
body: createCommentParagraph(commentData.commentTitle, stripHtmlTags(commentData.commentText))
}
});
if (commentData.containsAttachment) {
const newAttachment: DynamicsAttachment = {
documentbody: commentData.commentDocument,
filename: commentData.commentFileName,
mimetype: commentData.commentMimeType
}
await copyAttachmentFromDynamicsToJira(newAttachment, issueKey)
}
}
function stripHtmlTags(input: string): string {
// Use a regular expression to remove HTML tags
return input.replace(/<\/?[^>]+(>|$)/g, "").trim();
}
interface CommentEvent {
readonly commentID: string;
readonly commentTitle: string;
readonly commentText: string;
readonly commentCreator: string;
readonly incidentId: string;
readonly containsAttachment: boolean;
readonly commentDocument: string;
readonly commentFileName: string;
readonly commentMimeType: string;
}
import { IssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import { addCommentToCase } from './Utils/Dynamics365';
export default async function (event: IssueCommentCreatedEvent, context: Context<EV>): Promise<void> {
const customFieldId = context.environment.vars.JIRA_SYNC_FIELD_ID;
const jiraConnectorEmail = context.environment.vars.Connector.CONNECTOR_EMAIL_JIRA;
if (event.comment.author.emailAddress === jiraConnectorEmail) {
console.log("Ignoring comment to avoid event-loop.")
return
}
await addCommentToCase(context, event.issue.key, customFieldId, {
commentTitle: "Comment from Jira",
commentBody: event.comment.body
});
}
import { IssueTransitionedEvent } from '@sr-connect/jira-cloud/events';
import { resolveCase } from './Utils/Dynamics365';
export default async function(event: IssueTransitionedEvent, context: Context<EV>): Promise<void> {
const syncCustomFieldId = context.environment.vars.JIRA_SYNC_FIELD_ID;
const caseId = event.issue.fields[syncCustomFieldId];
if (!caseId) {
console.log('No case ID found, skipping...');
}
await resolveCase(context, caseId, (event as any)['comment']);
}
import JiraCloud from '../api/jira/cloud';
import { Convert } from '@sr-connect/convert';
import { RecordStorage } from '@sr-connect/record-storage';
export async function getUserEmailAddress(context: Context<EV>, userId: string): Promise<string> {
const oAuthDetails = await getOAuthDetails(context);
const response = await fetch<GetUserEmailResponse>(`${oAuthDetails.baseUrl}/systemusers(${userId})?$select=internalemailaddress`, {
headers: {
"Authorization": `Bearer ${oAuthDetails.token}`,
"Accept": "application/json"
}
});
if (!response.ok) {
throw Error(`Unexpected response while getting Dynamics 365 user email address: ${response.status} - ${await response.text()}`);
}
const body = await response.json();
return body.internalemailaddress;
}
export async function updateCase(context: Context<EV>, caseId: string, updateBody: CaseUpdateBody) {
const oAuthDetails = await getOAuthDetails(context);
const response = await fetch(`${oAuthDetails.baseUrl}/incidents(${caseId})`, {
method: 'PATCH',
headers: {
"Authorization": `Bearer ${oAuthDetails.token}`,
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
},
body: JSON.stringify(updateBody)
});
if (!response.ok) {
throw Error(`Unexpected response while updating Dynamics 365 case: ${response.status} - ${await response.text()}`)
}
}
export async function getCaseJiraTicketId(context: Context<EV>, incidentId: string): Promise<string> {
const oAuthDetails = await getOAuthDetails(context);
const response = await fetch<GetIncidentResponse>(`${oAuthDetails.baseUrl}/incidents(${incidentId})`, {
headers: {
"Authorization": `Bearer ${oAuthDetails.token}`,
"Accept": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
}
});
if (!response.ok) {
throw Error(`Unexpected response while getting incident from Dynamics 365 (${incidentId}): ${response.status} - ${await response.text()}`);
}
const body = await response.json();
return body.new_jiraticketid;
}
export async function addCommentToCase(context: Context<EV>, jiraTicketKey: string, syncFieldId: string, commentData: CaseComment) {
const oAuthDetails = await getOAuthDetails(context);
const issueData = await JiraCloud.Issue.getIssue({
issueIdOrKey: jiraTicketKey
});
const caseId = issueData.fields?.[syncFieldId];
const response = await fetch(`${oAuthDetails.baseUrl}/annotations`, {
method: 'POST',
headers: {
"Authorization": `Bearer ${oAuthDetails.token}`,
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
},
body: JSON.stringify({
"subject": commentData.commentTitle,
"notetext": commentData.commentBody,
"objectid_incident@odata.bind": `/incidents(${caseId})`
})
})
if (!response.ok) {
throw Error(`Unexpected response while adding comment to Dynamics 365 case: ${response.status} - ${await response.text()}`);
}
}
export async function resolveCase(context: Context<EV>, caseId: string, comment: string) {
const oAuthDetails = await getOAuthDetails(context);
const response = await fetch(`${oAuthDetails.baseUrl}/CloseIncident`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${oAuthDetails.token}`,
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
},
body: JSON.stringify({
IncidentResolution: {
subject: "Case Resolved",
'incidentid@odata.bind': `/incidents(${caseId})`,
description: comment,
timespent: 10 // Time spent on resolution in minutes
},
Status: 5
})
});
if (!response.ok) {
throw Error(`Unexpected response while resolving Dynamics 365 case: ${response.status} - ${await response.text()}`);
}
}
export async function copyAttachmentsFromDynamicsToJira(context: Context<EV>, caseId: string) {
const oAuthDetails = await getOAuthDetails(context);
const response = await fetch<GetDynamicsAttachmentsResponse>(`${oAuthDetails.baseUrl}/annotations?$filter=_objectid_value eq ${caseId}`, {
headers: {
'Authorization': `Bearer ${oAuthDetails.token}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
throw Error(`Unexpected response while fetching attachments from Dynamics (${caseId}): ${response.status} - ${await response.text()}`);
}
const body = await response.json();
const issueId = await getCaseJiraTicketId(context, caseId);
for (const attachment of body.value) {
try {
await copyAttachmentFromDynamicsToJira(attachment, issueId);
} catch (e) {
console.error(`Failed to copy attachment for Dynamics case: ${caseId}`, e, attachment);
}
}
}
export async function copyAttachmentFromDynamicsToJira(attachment: DynamicsAttachment, issueId: string) {
await JiraCloud.Issue.Attachment.addAttachments({
issueIdOrKey: issueId,
body: [{
content: Convert.base64ToBuffer(attachment.documentbody),
fileName: attachment.filename
}]
});
}
export async function getOAuthDetails(context: Context<EV>): Promise<OAuthDetails> {
const storage = new RecordStorage({ secure: true })
const accessToken = await storage.getValue('dyanmics_365_access_token')
const setupData: OAuthDetails = {
baseUrl: `${context.environment.vars.BASE_URL}/api/data/v9.0`,
token: accessToken
}
return setupData
}
interface OAuthDetails {
readonly baseUrl: string,
readonly token: any
}
interface GetUserEmailResponse {
readonly internalemailaddress: string;
}
interface CaseUpdateBody {
readonly new_jiraticketid: string;
}
interface GetIncidentResponse {
readonly new_jiraticketid: string;
}
interface CaseComment {
readonly commentTitle: string;
readonly commentBody: string;
readonly commentOwner?: string;
}
interface GetDynamicsAttachmentsResponse {
readonly value: DynamicsAttachment[];
}
export interface DynamicsAttachment {
readonly documentbody: string;
readonly filename: string;
readonly mimetype: string;
}
export interface DynamicsCase {
readonly caseId: string;
readonly caseTitle: string;
readonly caseDescription: string;
readonly caseDropdown: string;
readonly caseDueDate: string;
readonly caseReporter: string;
}
import JiraCloud from '../api/jira/cloud';
import { DynamicsCase, getUserEmailAddress, updateCase } from './Dynamics365';
export async function createJiraIssueFromDynamicsCase(context: Context<EV>, caseItem: DynamicsCase) {
const { JIRA_SYNC_FIELD_ID, JIRA_ISSUE_TYPE, JIRA_PROJECT_KEY } = context.environment.vars;
const reporterEmail = await getUserEmailAddress(context, caseItem.caseReporter);
const users = await JiraCloud.User.Search.findUsers({
query: `${reporterEmail}`
});
let reporterAccountId: string | undefined;
let description = '';
if (users.length > 0) {
reporterAccountId = users[0]?.accountId;
} else {
const currentUser = await JiraCloud.Myself.getCurrentUser();
reporterAccountId = currentUser.accountId;
description = `Unable to find reporter in Jira with email: ${reporterEmail} - `
}
if (caseItem.caseDescription) {
description += caseItem.caseDescription;
} else {
description += 'Issue created from Dyanmic 365 Customer Service Desk'
}
const issue = await JiraCloud.Issue.createIssue({
body: {
fields: {
issuetype: {
name: JIRA_ISSUE_TYPE
},
project: {
key: JIRA_PROJECT_KEY
},
summary: caseItem.caseTitle,
description: createDescriptionParagraph(description),
reporter: reporterAccountId ? {
accountId: reporterAccountId
} : undefined,
[JIRA_SYNC_FIELD_ID]: caseItem.caseId,
duedate: caseItem.caseDueDate
}
}
});
await updateCase(context, caseItem.caseId, {
new_jiraticketid: issue.key ?? ''
});
}
function createDescriptionParagraph(text: string) {
return {
type: 'doc' as const,
version: 1 as const,
content: [
{
type: 'paragraph' as const,
content: [
{
type: 'text' as const,
text: text
}
]
}
]
}
}
export function createCommentParagraph(title: string, body: string) {
return {
version: 1 as const,
type: 'doc' as const,
content: [
{
type: 'paragraph' as const,
content: [
{
type: 'text' as const,
text: title,
marks: [
{
type: 'strong' as const
}
]
}
]
},
{
type: 'paragraph' as const,
content: [
{
type: 'text' as const,
text: body
}
]
}
]
}
}
© 2025 ScriptRunner · Terms and Conditions · Privacy Policy · Legal Notice · Cookie Preferences