By default, async event consumers time out after 55 seconds. Use cases that required longer computation have previously had to rely on breaking the task into multiple steps or batches, and queuing multiple events to do the work.
You can now configure a timeout of up to 900 seconds (15 minutes) which will allow many such use cases to be performed in a single invocation.
Imagine you are developing an application that generates detailed reports from Jira issues. This process can be time-consuming, especially if you are aggregating data from multiple projects and applying complex calculations. The Long-Running Compute feature allows you to handle these tasks efficiently without running into timeout issues.
This tutorial assumes you're already familiar with developing on Forge and the Async Events API.
Before you start, ensure you have the following:
Create a new Forge app:
1 2forge create
Follow the prompts to set up your application. Name your app and choose the template appropriate to the type of app you are creating. For the purposes of this tutorial, we will use the blank
template. Select the following options:
context: Show All
category: Show All
blank
If you intend to extend this example to use UI to trigger a long-running function, or display the results of your long-running function to the front end, you can select a template from the UI Kit or Custom UI category.
Navigate to your app directory:
1 2cd your-app-name
Add necessary permissions:
Open manifest.yml
and add the required permissions to access Jira data:
1 2permissions: scopes: - read:jira-work
If you chose the blank
template, the existing function
module in the manifest can be removed, along with the sample src/index.js
file.
All long-running functions must be invoked by an async event consumer. Update the manifest.yml
file to include the required event consumer module and corresponding function module:
1 2modules: consumer: - key: queue-consumer-key queue: queue-consumer-name resolver: function: generate-report method: generate-report-event-listener function: - key: generate-report handler: generateReport.handler timeoutSeconds: 900
generateReport.handler
will be invoked by the queue each time an event is pushed to it.generate-report
and method value generate-report-event-listener
. The function value must match the key under the function
module. The method value must be defined on the resolver object in generateReport.js
for the consumer to invoke it. This will be visible in the following section.Create a new long-running function:
In the src
directory, create a new file called generateReport.js
. This is where the resolver and the long-running function is defined. The below long-running function will take 5 seconds to execute, however it can take up to 900 seconds until it gets timed out:
1 2import Resolver from '@forge/resolver'; import api, { route } from '@forge/api'; const resolver = new Resolver(); resolver.define('generate-report-event-listener', async ({ payload, context }) => { try { // This resolver function can take up to 900 seconds to complete console.log("The resolver has been invoked"); const ret = await processGenerate(payload); console.log(`The resolver returned with: ${JSON.stringify(ret)}`); return ret; } catch (error) { console.error('Error in generate-report-event-listener:', error); throw error; } }); export const processGenerate = async (event) => { const { projectKey } = event; if (!projectKey) { throw new Error('Project key is required but not provided in event payload'); } // Simulate a long-running task const reportData = await generateReport(projectKey); return { statusCode: 200, body: reportData, }; }; const generateReport = async (projectKey) => { const issues = await fetchIssuesFromJira(projectKey); // Perform complex calculations and aggregations return performCalculations(issues); }; const createMockIssuesFromJira = (projectKey) => ([ { id: 1, key: `${projectKey}-1`, fields: { status: { name: 'Done' }, customfield_10016: 5 } }, { id: 2, key: `${projectKey}-2`, fields: { status: { name: 'In Progress' }, customfield_10016: 3 } }, ]); const fetchIssuesFromJira = async (projectKey) => { try { // Simulate a delay for fetching data from Jira API await new Promise(resolve => setTimeout(resolve, 2000)); // Fetch real data from Jira API const response = await api.asApp().requestJira( route`/rest/api/3/search?jql=project=${projectKey}&maxResults=100` ); if (!response.ok) { console.log(`API request failed, using mock data. Status: ${response.status}`); // Fallback to mock data if API call fails return createMockIssuesFromJira(projectKey); } const data = await response.json(); return data.issues || []; } catch (error) { console.error('Error fetching issues from Jira:', error); // Return mock data as fallback return createMockIssuesFromJira(projectKey); } }; const performCalculations = (issues) => { // Aggregate data from real Jira issues const statusCounts = {}; let totalStoryPoints = 0; let issuesWithPoints = 0; issues.forEach(issue => { const status = issue.fields?.status?.name || 'Unknown'; statusCounts[status] = (statusCounts[status] || 0) + 1; // Story points are often in customfield_10016, but this varies by instance const storyPoints = issue.fields?.customfield_10016 || 0; if (storyPoints > 0) { totalStoryPoints += storyPoints; issuesWithPoints++; } }); return { totalIssues: issues.length, statusBreakdown: statusCounts, totalStoryPoints, averageStoryPoints: issuesWithPoints > 0 ? Math.round((totalStoryPoints / issuesWithPoints) * 100) / 100 : 0, reportGeneratedAt: new Date().toISOString() }; }; // This variable is referenced to in the manifest export const handler = resolver.getDefinitions();
There are many ways to invoke a function which will push events to the consumer queue, however a simple one we will use is a trigger.
Create a new file src/pushToQueue.js
with the following code:
1 2import { Queue } from "@forge/events"; export const handler = async (event, context) => { try { const queue = new Queue({key: "queue-consumer-name"}); // Extract project key from the Jira issue update event const projectKey = event.issue?.fields?.project?.key; if (!projectKey) { console.error('No project key found in event payload'); return { statusCode: 400, statusText: "Bad Request - No project key found" }; } console.log(`Pushing an event to the queue for project: ${projectKey}`); const { jobId } = await queue.push({ body: { projectKey: projectKey, issueKey: event.issue?.key, triggeredAt: new Date().toISOString() } }); console.log(`Queued job ${queue.key}#${jobId}`); return { statusCode: 200, statusText: "Success" }; } catch (error) { console.error('Error in pushToQueue handler:', error); return { statusCode: 500, statusText: "Internal Server Error" }; } }
Update manifest.yml
to include a new trigger module, and extend the current function module. The event which will invoke the trigger is avi:jira:updated:issue
. This means every time an issue is updated, the trigger is invoked and pushToQueue.handler
is called. Your manifest.yml
file should now look like this:
1 2modules: consumer: - key: queue-consumer-key queue: queue-consumer-name resolver: function: generate-report method: generate-report-event-listener trigger: - key: invoke-lrf-when-jira-issue-updated function: push-to-queue events: - avi:jira:updated:issue function: - key: generate-report handler: generateReport.handler timeoutSeconds: 900 - key: push-to-queue handler: pushToQueue.handler
blank
template was chosen, there are no pre-installed dependencies.
1 2npm install @forge/resolver @forge/events @forge/api
1 2forge deploy
1 2forge install
Update any Jira issue to invoke the long-running function! The console.log
statements executed can be seen in the terminal if running forge tunnel
or in the developer console if not. You can also see them by running forge logs
. To see the output of the long-running function on your frontend, build a UI Kit or Custom UI app.
If you have an active forge tunnel
running, the expected output is as follows:
1 2invocation: ... pushToQueue.handler INFO 14:30:01.146 ... Pushing an event to the queue for project: DEMO INFO 14:30:01.589 ... Queued job queue-consumer-name#... invocation: ... generateReport.handler INFO 14:30:02.506 ... The resolver has been invoked INFO 14:30:04.508 ... The resolver returned with: {"statusCode":200,"body":{"totalIssues":25,"statusBreakdown":{"Done":10,"In Progress":8,"To Do":7},"totalStoryPoints":42,"averageStoryPoints":3.5,"reportGeneratedAt":"2025-01-11T14:30:04.508Z"}}
The ...
replaces anywhere an id is used in the log output. Your output will have real values and may vary depending on your Jira project data.
Long-running functions allow Forge developers to handle complex and time-consuming tasks efficiently. By following this tutorial, you have learned how to set up a realistic use case for generating reports from Jira data.
Rate this page: