Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Jira Forge Issue Action Spins Indefinitely Despite Successful Function Execution

faridmirahadi July 1, 2025

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!


screen_shot.png

1 answer

0 votes
Tim Pettersen
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 1, 2025

Hi @faridmirahadi

 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

faridmirahadi July 2, 2025

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."
}
};
}
}
faridmirahadi July 2, 2025

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
faridmirahadi July 2, 2025

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."

      }

    };

  }

}
faridmirahadi July 2, 2025
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...');
        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."
      }
    };
  }
}
faridmirahadi July 2, 2025

Function Part1:

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
    };
faridmirahadi July 2, 2025
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."
}
};
}
}
faridmirahadi July 2, 2025
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."
}
};
}
}
faridmirahadi July 2, 2025
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."
}
};
}
}

 

Tim Pettersen
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 2, 2025

Hi @faridmirahadi

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

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
FREE
PERMISSIONS LEVEL
Product Admin
TAGS
AUG Leaders

Atlassian Community Events