Hi everyone,
I've built a Forge app with a jira:issueAction
module. When the action button is clicked, it triggers a function that performs several async tasks and returns a response at the end. The function completes successfully — I can see the final logs, and there are no errors or unhandled exceptions.
However, the issue I'm facing is that the Jira Action button keeps spinning and doesn't return to its normal state unless I manually refresh the page. I've tried returning different response types (string, JSON object, etc.), but the behavior remains the same.
Has anyone experienced this? I'd really appreciate any guidance or ideas on what might be causing the action to hang like this.
Thanks in advance!
> I've tried returning different response types (string, JSON object, etc.), but the behavior remains the same.
Could you share which function you're referring these values from? Your `jira:issueAction` object should have a `resource` attribute that specifies a UI Kit or Custom UI resource that provides the UI for your app, which will be displayed in a modal that appears after your issue action is selected. If this is a UI Kit resource (i.e. a `.jsx` file) your function should be returning JSX components — not JSON or a string.
Would it be possible to share your manifest and the function that is backing your `jira:issueAction` module? That would help us diagnose what is going on.
cheers,
Tim
Thanks Tim, Let me share my manifest and function.
modules:
webtrigger:
- key: sms-reply-web-trigger
function: sms_reply_module
response:
type: static
outputs:
- key: status-ok
statusCode: 200
contentType: application/json
body: '{"body": "Allowed static response"}'
- key: status-error
statusCode: 403
contentType: text/plain
body: 'Error: Forbidden'
trigger:
- key: send_sms_message
function: main
events:
- avi:jira:updated:issue
jira:issueAction:
- key: send-sms-action
function: sms_action
title: Send SMS Update
icon:
url: https://cdn-icons-png.flaticon.com/512/455/455705.png
action:
response:
type: text
function:
- key: main
handler: send_sms.run
- key: sms_reply_module
handler: reply_sms.run
- key: sms_action
handler: sms_action.run
permissions:
external:
fetch:
backend:
- 1740-2607-fea8-3c5f-8020-2451-a975-30a9-118a.ngrok-free.app
- https://afdd-2607-fea8-3c5f-8020-39d3-a087-b162-fc00.ngrok-free.app
- https://sms-app-102569449897222.us-central1.run.app
scopes:
- read:jira-work
- write:jira-work
app:
id: ari:cloud:ecosystem::app/cf5b0c85-3dbe-4073-aaec-3ae342439160
runtime:
name: nodejs20.x
import api, { route, webTrigger } from "@forge/api";
export async function run(event, context) {
try {
console.log('SMS Action - Event received:', JSON.stringify(event, null, 2));
console.log('SMS Action - Context received:', JSON.stringify(context, null, 2));
let issue = null;
if (event.context?.extension?.issue) {
issue = event.context.extension.issue;
} else if (event.context?.platformContext?.issueKey) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${event.context.platformContext.issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
issue = { key: event.context.platformContext.issueKey, ...data };
} catch (error) {
console.error('Error fetching issue by key:', error);
return { body: { content: "Error fetching issue by key." } };
}
}
if (!issue) {
console.error('No issue found in context');
return { body: { content: "No issue found in event context." } };
}
const issueKey = issue.key;
console.log(`Manual SMS trigger for issue ${issueKey}...`);
const fieldsResponse = await api.asApp().requestJira(route`/rest/api/3/field`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const fields = await fieldsResponse.json();
const phoneNumberField = fields.find(field => field.name === "Phone Number");
if (!phoneNumberField) {
console.error('Phone Number field not found');
return { body: { content: "Phone Number field not found in Jira configuration." } };
}
console.log(`Phone Number field ID: ${phoneNumberField.id}`);
const issueResponse = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const issueData = await issueResponse.json();
const phoneNumber = issueData.fields[phoneNumberField.id];
if (!phoneNumber) {
console.error('Phone number not present in issue');
return { body: { content: "No phone number found in the issue." } };
}
const formattedPhoneNumber = '+' + phoneNumber.toString().replace(/^\+/, '');
console.log('Formatted phone number:', formattedPhoneNumber);
const taskName = issueData.fields.summary;
const taskDescription = issueData.fields.description && issueData.fields.description.content
? issueData.fields.description.content.map(p => p.content?.map(c => c.text).join(' ')).join('\n')
: (typeof issueData.fields.description === 'string' ? issueData.fields.description : '');
const smsBody = `Please provide some updates on this task: ${taskName}`;
const siteResponse = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const siteInfo = await siteResponse.json();
const siteName = siteInfo.baseUrl;
const webtriggerUrl = await webTrigger.getUrl('sms-reply-web-trigger');
console.log('Webtrigger URL:', webtriggerUrl);
const SMS_API_KEY = process.env.SMS_API_KEY;
const smsPayload = {
body: smsBody,
to_number: formattedPhoneNumber,
issue_key: issueKey,
site_name: siteName,
webtrigger_url: webtriggerUrl,
summary: taskName,
description: taskDescription
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 seconds
try {
console.log('Sending SMS request to API...');
const smsResponse = await api.fetch('https://1740-2607-fea8-3c5f-8020-2451-a975-30a9-118a.ngrok-free.app/send_sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': SMS_API_KEY
},
body: JSON.stringify(smsPayload),
signal: controller.signal
});
clearTimeout(timeout);
if (smsResponse.status === 200) {
console.log(`SMS sent successfully to ${formattedPhoneNumber}`);
return {
body: {
content: `SMS sent successfully to ${formattedPhoneNumber}`
}
};
} else {
const errorText = await smsResponse.text();
console.error(`SMS failed with status ${smsResponse.status}:`, errorText);
return {
body: {
content: `Failed to send SMS. Status: ${smsResponse.status}`
}
};
}
} catch (err) {
clearTimeout(timeout);
console.error("SMS API call failed:", err);
return {
body: {
content: "Error sending SMS. Timeout or API failure."
}
};
}
} catch (error) {
console.error("Unhandled error in SMS action:", error);
return {
body: {
content: "Unexpected error. Please check logs."
}
};
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks Tim, here is my manifest:
modules:
webtrigger:
- key: sms-reply-web-trigger
function: sms_reply_module
response:
type: static
outputs:
- key: status-ok
statusCode: 200
contentType: application/json
body: '{"body": "Allowed static response"}'
- key: status-error
statusCode: 403
contentType: text/plain
body: 'Error: Forbidden'
trigger:
- key: send_sms_message
function: main
events:
- avi:jira:updated:issue
jira:issueAction:
- key: send-sms-action
function: sms_action
title: Send SMS Update
icon:
url: https://cdn-icons-png.flaticon.com/512/455/455705.png
action:
response:
type: text
function:
- key: main
handler: send_sms.run
- key: sms_reply_module
handler: reply_sms.run
- key: sms_action
handler: sms_action.run
permissions:
external:
fetch:
backend:
- 1740-2607-fea8-3c5f-8020-2451-a975-30a9-118a.ngrok-free.app
- https://afdd-2607-fea8-3c5f-8020-39d3-a087-b162-fc00.ngrok-free.app
- https://sms-app-102569449897222.us-central1.run.app
scopes:
- read:jira-work
- write:jira-work
app:
id: ari:cloud:ecosystem::app/cf5b0c85-3dbe-4073-aaec-3ae342439160
runtime:
name: nodejs20.x
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
and the function:
import api, { route, webTrigger } from "@forge/api";
export async function run(event, context) {
try {
console.log('SMS Action - Event received:', JSON.stringify(event, null, 2));
console.log('SMS Action - Context received:', JSON.stringify(context, null, 2));
let issue = null;
if (event.context?.extension?.issue) {
issue = event.context.extension.issue;
} else if (event.context?.platformContext?.issueKey) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${event.context.platformContext.issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
issue = { key: event.context.platformContext.issueKey, ...data };
} catch (error) {
console.error('Error fetching issue by key:', error);
return { body: { content: "Error fetching issue by key." } };
}
}
if (!issue) {
console.error('No issue found in context');
return { body: { content: "No issue found in event context." } };
}
const issueKey = issue.key;
console.log(`Manual SMS trigger for issue ${issueKey}...`);
const fieldsResponse = await api.asApp().requestJira(route`/rest/api/3/field`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const fields = await fieldsResponse.json();
const phoneNumberField = fields.find(field => field.name === "Phone Number");
if (!phoneNumberField) {
console.error('Phone Number field not found');
return { body: { content: "Phone Number field not found in Jira configuration." } };
}
console.log(`Phone Number field ID: ${phoneNumberField.id}`);
const issueResponse = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const issueData = await issueResponse.json();
const phoneNumber = issueData.fields[phoneNumberField.id];
if (!phoneNumber) {
console.error('Phone number not present in issue');
return { body: { content: "No phone number found in the issue." } };
}
const formattedPhoneNumber = '+' + phoneNumber.toString().replace(/^\+/, '');
console.log('Formatted phone number:', formattedPhoneNumber);
const taskName = issueData.fields.summary;
const taskDescription = issueData.fields.description && issueData.fields.description.content
? issueData.fields.description.content.map(p => p.content?.map(c => c.text).join(' ')).join('\n')
: (typeof issueData.fields.description === 'string' ? issueData.fields.description : '');
const smsBody = `Please provide some updates on this task: ${taskName}`;
const siteResponse = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const siteInfo = await siteResponse.json();
const siteName = siteInfo.baseUrl;
const webtriggerUrl = await webTrigger.getUrl('sms-reply-web-trigger');
console.log('Webtrigger URL:', webtriggerUrl);
const SMS_API_KEY = process.env.SMS_API_KEY;
const smsPayload = {
body: smsBody,
to_number: formattedPhoneNumber,
issue_key: issueKey,
site_name: siteName,
webtrigger_url: webtriggerUrl,
summary: taskName,
description: taskDescription
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 seconds
try {
console.log('Sending SMS request to API...');
const smsResponse = await api.fetch('https://1740-2607-fea8-3c5f-8020-2451-a975-30a9-118a.ngrok-free.app/send_sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': SMS_API_KEY
},
body: JSON.stringify(smsPayload),
signal: controller.signal
});
clearTimeout(timeout);
if (smsResponse.status === 200) {
console.log(`SMS sent successfully to ${formattedPhoneNumber}`);
return {
body: {
content: `SMS sent successfully to ${formattedPhoneNumber}`
}
};
} else {
const errorText = await smsResponse.text();
console.error(`SMS failed with status ${smsResponse.status}:`, errorText);
return {
body: {
content: `Failed to send SMS. Status: ${smsResponse.status}`
}
};
}
} catch (err) {
clearTimeout(timeout);
console.error("SMS API call failed:", err);
return {
body: {
content: "Error sending SMS. Timeout or API failure."
}
};
}
} catch (error) {
console.error("Unhandled error in SMS action:", error);
return {
body: {
content: "Unexpected error. Please check logs."
}
};
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Function Part1:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
import api, { route, webTrigger } from "@forge/api";
export async function run(event, context) {
try {
console.log('SMS Action - Event received:', JSON.stringify(event, null, 2));
console.log('SMS Action - Context received:', JSON.stringify(context, null, 2));
let issue = null;
if (event.context?.extension?.issue) {
issue = event.context.extension.issue;
} else if (event.context?.platformContext?.issueKey) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${event.context.platformContext.issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
issue = { key: event.context.platformContext.issueKey, ...data };
} catch (error) {
console.error('Error fetching issue by key:', error);
return { body: { content: "Error fetching issue by key." } };
}
}
if (!issue) {
console.error('No issue found in context');
return { body: { content: "No issue found in event context." } };
}
const issueKey = issue.key;
console.log(`Manual SMS trigger for issue ${issueKey}...`);
const fieldsResponse = await api.asApp().requestJira(route`/rest/api/3/field`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const fields = await fieldsResponse.json();
const phoneNumberField = fields.find(field => field.name === "Phone Number");
if (!phoneNumberField) {
console.error('Phone Number field not found');
return { body: { content: "Phone Number field not found in Jira configuration." } };
}
console.log(`Phone Number field ID: ${phoneNumberField.id}`);
const issueResponse = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const issueData = await issueResponse.json();
const phoneNumber = issueData.fields[phoneNumberField.id];
if (!phoneNumber) {
console.error('Phone number not present in issue');
return { body: { content: "No phone number found in the issue." } };
}
const formattedPhoneNumber = '+' + phoneNumber.toString().replace(/^\+/, '');
console.log('Formatted phone number:', formattedPhoneNumber);
const taskName = issueData.fields.summary;
const taskDescription = issueData.fields.description && issueData.fields.description.content
? issueData.fields.description.content.map(p => p.content?.map(c => c.text).join(' ')).join('\n')
: (typeof issueData.fields.description === 'string' ? issueData.fields.description : '');
const smsBody = `Please provide some updates on this task: ${taskName}`;
const siteResponse = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const siteInfo = await siteResponse.json();
const siteName = siteInfo.baseUrl;
const webtriggerUrl = await webTrigger.getUrl('sms-reply-web-trigger');
console.log('Webtrigger URL:', webtriggerUrl);
const SMS_API_KEY = process.env.SMS_API_KEY;
const smsPayload = {
body: smsBody,
to_number: formattedPhoneNumber,
issue_key: issueKey,
site_name: siteName,
webtrigger_url: webtriggerUrl,
summary: taskName,
description: taskDescription
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 seconds
try {
console.log('Sending SMS request to API...');
const smsResponse = await api.fetch('https://1740-2607-fea8-3c5f-8020-2451-a975-30a9-118a.ngrok-free.app/send_sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': SMS_API_KEY
},
body: JSON.stringify(smsPayload),
signal: controller.signal
});
clearTimeout(timeout);
if (smsResponse.status === 200) {
console.log(`SMS sent successfully to ${formattedPhoneNumber}`);
return {
body: {
content: `SMS sent successfully to ${formattedPhoneNumber}`
}
};
} else {
const errorText = await smsResponse.text();
console.error(`SMS failed with status ${smsResponse.status}:`, errorText);
return {
body: {
content: `Failed to send SMS. Status: ${smsResponse.status}`
}
};
}
} catch (err) {
clearTimeout(timeout);
console.error("SMS API call failed:", err);
return {
body: {
content: "Error sending SMS. Timeout or API failure."
}
};
}
} catch (error) {
console.error("Unhandled error in SMS action:", error);
return {
body: {
content: "Unexpected error. Please check logs."
}
};
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
import api, { route, webTrigger } from "@forge/api";
export async function run(event, context) {
try {
console.log('SMS Action - Event received:', JSON.stringify(event, null, 2));
console.log('SMS Action - Context received:', JSON.stringify(context, null, 2));
let issue = null;
if (event.context?.extension?.issue) {
issue = event.context.extension.issue;
} else if (event.context?.platformContext?.issueKey) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${event.context.platformContext.issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
issue = { key: event.context.platformContext.issueKey, ...data };
} catch (error) {
console.error('Error fetching issue by key:', error);
return { body: { content: "Error fetching issue by key." } };
}
}
if (!issue) {
console.error('No issue found in context');
return { body: { content: "No issue found in event context." } };
}
const issueKey = issue.key;
console.log(`Manual SMS trigger for issue ${issueKey}...`);
const fieldsResponse = await api.asApp().requestJira(route`/rest/api/3/field`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const fields = await fieldsResponse.json();
const phoneNumberField = fields.find(field => field.name === "Phone Number");
if (!phoneNumberField) {
console.error('Phone Number field not found');
return { body: { content: "Phone Number field not found in Jira configuration." } };
}
console.log(`Phone Number field ID: ${phoneNumberField.id}`);
const issueResponse = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const issueData = await issueResponse.json();
const phoneNumber = issueData.fields[phoneNumberField.id];
if (!phoneNumber) {
console.error('Phone number not present in issue');
return { body: { content: "No phone number found in the issue." } };
}
const formattedPhoneNumber = '+' + phoneNumber.toString().replace(/^\+/, '');
console.log('Formatted phone number:', formattedPhoneNumber);
const taskName = issueData.fields.summary;
const taskDescription = issueData.fields.description && issueData.fields.description.content
? issueData.fields.description.content.map(p => p.content?.map(c => c.text).join(' ')).join('\n')
: (typeof issueData.fields.description === 'string' ? issueData.fields.description : '');
const smsBody = `Please provide some updates on this task: ${taskName}`;
const siteResponse = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const siteInfo = await siteResponse.json();
const siteName = siteInfo.baseUrl;
const webtriggerUrl = await webTrigger.getUrl('sms-reply-web-trigger');
console.log('Webtrigger URL:', webtriggerUrl);
const SMS_API_KEY = process.env.SMS_API_KEY;
const smsPayload = {
body: smsBody,
to_number: formattedPhoneNumber,
issue_key: issueKey,
site_name: siteName,
webtrigger_url: webtriggerUrl,
summary: taskName,
description: taskDescription
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 seconds
try {
console.log('Sending SMS request to API...');
const smsResponse = await api.fetch('URL/send_sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': SMS_API_KEY
},
body: JSON.stringify(smsPayload),
signal: controller.signal
});
clearTimeout(timeout);
if (smsResponse.status === 200) {
console.log(`SMS sent successfully to ${formattedPhoneNumber}`);
return {
body: {
content: `SMS sent successfully to ${formattedPhoneNumber}`
}
};
} else {
const errorText = await smsResponse.text();
console.error(`SMS failed with status ${smsResponse.status}:`, errorText);
return {
body: {
content: `Failed to send SMS. Status: ${smsResponse.status}`
}
};
}
} catch (err) {
clearTimeout(timeout);
console.error("SMS API call failed:", err);
return {
body: {
content: "Error sending SMS. Timeout or API failure."
}
};
}
} catch (error) {
console.error("Unhandled error in SMS action:", error);
return {
body: {
content: "Unexpected error. Please check logs."
}
};
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
import api, { route, webTrigger } from "@forge/api";
export async function run(event, context) {
try {
console.log('SMS Action - Event received:', JSON.stringify(event, null, 2));
console.log('SMS Action - Context received:', JSON.stringify(context, null, 2));
let issue = null;
if (event.context?.extension?.issue) {
issue = event.context.extension.issue;
} else if (event.context?.platformContext?.issueKey) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${event.context.platformContext.issueKey}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
issue = { key: event.context.platformContext.issueKey, ...data };
} catch (error) {
console.error('Error fetching issue by key:', error);
return { body: { content: "Error fetching issue by key." } };
}
}
.
.
.
.
if (smsResponse.status === 200) {
console.log(`SMS sent successfully to ${formattedPhoneNumber}`);
return {
body: {
content: `SMS sent successfully to ${formattedPhoneNumber}`
}
};
} else {
const errorText = await smsResponse.text();
console.error(`SMS failed with status ${smsResponse.status}:`, errorText);
return {
body: {
content: `Failed to send SMS. Status: ${smsResponse.status}`
}
};
}
} catch (err) {
clearTimeout(timeout);
console.error("SMS API call failed:", err);
return {
body: {
content: "Error sending SMS. Timeout or API failure."
}
};
}
} catch (error) {
console.error("Unhandled error in SMS action:", error);
return {
body: {
content: "Unexpected error. Please check logs."
}
};
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks for sharing your code!
I believe the issue is in your `jira:issueAction` module:
jira:issueAction:
- key: send-sms-action
function: sms_action
title: Send SMS Update
icon:
url: https://cdn-icons-png.flaticon.com/512/455/455705.png
action:
response:
type: text
The "action" property isn't valid syntax, and a "function" can't be specified at the top level. Was this generated by an LLM by any chance? It may be confusing the module syntax with a feature from another platform, or hallucinating an action response type.
A typical `jira:issueAction` module looks something like this:
jira:issueAction:
- key: hello-world-issue-action
resource: main
resolver:
function: sms_action
render: native
title: Send SMS Update
The "resource" specifies a UI Kit resource that will be displayed in a dialog when the user clicks the action button, and the "resolver" specifies the function that acts as a backend for the UI.
If you'd like to see a working example of this, I'd recommend creating a new Forge app using the CLI and picking the Jira issue action template. That will show you the general architecture of an issue action, which you can adapt to suit your "send SMS" use-case.
Hope that helps!
Tim
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.