Skip to main content

Scheduled Jobs API Reference for AI

This document is a complete API reference and code guide for writing scripts that run in Script Master's Scheduled Jobs module β€” an Atlassian Forge app that lets users define backend JavaScript functions executed on a cron schedule, automating recurring tasks like syncing data, enforcing governance rules, generating reports, or triggering external integrations in Jira and Confluence.

Use this document as context when asking an AI assistant (Claude, Gemini, ChatGPT, etc.) to generate Scheduled Job scripts:

Here is the API reference: [link to this file]. Build a scheduled job script for Script Master that [your task].


Execution Model​

Scripts run as async JavaScript functions executed on the Forge backend (Node.js, ARM64). This is a server-side FaaS environment β€” not a browser and not the user's machine. Your script code is wrapped like this:

const result = await (async function(api, route, fetch, authorize, console, /* ...JS globals */) {
// YOUR SCRIPT CODE HERE
})(api, route, fetch, authorize, wrappedConsole, /* ...JS globals */);

Key implications:

  • Top-level await is supported β€” use it freely.
  • The return value of the script is captured and appears in the execution log as return: <value>. There is no HTTP response to construct β€” just return data for debugging.
  • console.log() / console.info() / console.warn() / console.error() are captured and stored in the job's invocation log. Maximum 100 log entries per run; each entry capped at 2 048 characters.
  • Errors thrown by the script are caught and recorded in the log.
  • Scripts run as the app (not as the current user) β€” always use api.asApp() for all Atlassian API calls.
  • No browser APIs, no window, no DOM, no requestJira/requestConfluence shorthand (those are Script Console-only frontend globals).
  • Execution requires a configured Companion App for remote execution. Without it, scripts will not run and will log an error.

Scheduling and Timing​

Forge triggers fire the scheduler once per hour. The scheduler checks all enabled jobs and puts a job into its execution queue if the most recent scheduled time (per its cron expression) falls within the past 60 minutes. This means:

  • Minimum real-world interval is approximately 1 hour, regardless of the cron expression.
  • Cron expressions with sub-hourly intervals (e.g. */15 * * * *) are valid but the job will still only execute once per Forge trigger cycle β€” approximately every 60 minutes.
  • Actual execution may be delayed further by Forge queue processing latency.
  • A job can also be triggered manually from the dashboard using the Run button, which bypasses scheduling and queues the job immediately.

Execution Flow (for production runs)​

  1. Forge fires the scheduler trigger periodically (approximately every hour).
  2. The trigger fetches all enabled scheduled jobs.
  3. For each job, it parses the cron expression and checks if the previous scheduled execution time is within the past 60 minutes.
  4. If yes, the job ID is pushed to the scheduled-jobs-queue Forge Queue.
  5. The queue listener dequeues the job, fetches its code from storage, and calls runScript().
  6. runScript() POSTs the script and args to the configured remote Companion App.
  7. The Companion App executes the script and returns { result, error, logs }.
  8. The result and logs are persisted to the job's invocation history (visible in the Logs panel).

Execution Flow (for test runs from the UI)​

When you click Test in the editor, the ScheduledJobs.Test resolver is invoked directly with a 24-second timeout. This runs synchronously against the companion app and returns output immediately to the test panel.


Available Globals​

Scheduled job scripts are backend functions. The available globals are the same on both Jira and Confluence β€” there are no platform-specific globals.

GlobalTypeDescription
apiForgeAPIForge API object β€” use api.asApp().requestJira(...) or api.asApp().requestConfluence(...)
routeTemplate-literal tagConstruct safe API route strings: route`/rest/api/3/issue/${id}`
fetch(url, options?) => Promise<Response>Standard fetch for external HTTP calls (non-Atlassian APIs)
authorizeForgeAuthorizeForge authorization utilities
consoleConsolelog, info, warn, error β€” output captured in the invocation log

Standard JavaScript globals available: Array, Promise, String, Set, Object, Boolean, Symbol, BigInt, Number, Map, WeakMap, WeakSet, JSON, Intl, Math, RegExp, Error, Date, Buffer, ArrayBuffer, DataView, typed arrays, AbortController, AbortSignal, URL, URLSearchParams, TextEncoder, TextDecoder, crypto, atob, btoa, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, setTimeout, setInterval, clearTimeout, clearInterval, setImmediate, parseFloat, parseInt, isFinite, isNaN.

Forbidden (will throw): eval, require, import(, process, new Function, Function, new Proxy, structuredClone, queueMicrotask, escape, unescape, debug, print, module. Also forbidden at the source-code level (checked before execution): .prototype, .__proto__, .constructor( / .constructor.constructor, arguments., .callee, .caller.

Note: requestJira, requestConfluence, requestBitbucket, view, showFlag, and context are not available in scheduled jobs. Those globals exist only in Script Console (frontend) and Fragments (frontend).


API Reference​

api​

The Forge API object from @forge/api. The primary usage pattern is api.asApp() which returns a client that acts as the installed app (with its declared OAuth scopes), not as any individual user.

api.asApp().requestJira(route: RouteString, options?: RequestInit): Promise<Response>
api.asApp().requestConfluence(route: RouteString, options?: RequestInit): Promise<Response>

Both methods return a standard Response object. Always call .json() or .text() on it to read the body.

Example β€” Jira:

const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
console.log('status', response.status);
console.log('data', data);

Example β€” Confluence:

const response = await api.asApp().requestConfluence(route`/wiki/rest/api/space`, {
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
console.log('spaces', data.results?.length);

route​

A tagged template literal that constructs safe URL route strings. It encodes any interpolated values to prevent URL injection. Always use route when building paths with dynamic values.

// Safe β€” encodes the variable
const issueKey = 'TEST-123';
route`/rest/api/3/issue/${issueKey}`

// Also valid for static paths
route`/rest/api/3/serverInfo`

fetch​

Standard Fetch API for calling external (non-Atlassian) HTTP endpoints. Not needed for Jira or Confluence β€” use api.asApp().requestJira() / api.asApp().requestConfluence() for those.

const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
const data = await response.json();
console.log('external response', data);

console​

All four standard methods are captured in the invocation log:

console.log('message', someObject);
console.info('info message');
console.warn('warning');
console.error('error details', error);

Log entries are limited to 100 per run. Each entry is truncated at 2 048 characters. Log entries from production runs are persisted and visible in the Logs panel in the UI. Log entries from test runs appear in the test output panel.


Code Patterns​

Pattern 1 β€” Paginate through all Jira issues​

Jira search returns at most 50 issues per request by default. Use startAt to paginate.

const jql = 'project = "MY-PROJECT" AND status = "To Do"';
let startAt = 0;
const maxResults = 50;
const allIssues = [];

while (true) {
const response = await api.asApp().requestJira(
route`/rest/api/3/search?jql=${jql}&startAt=${startAt}&maxResults=${maxResults}`,
{ headers: { 'Accept': 'application/json' } }
);
const data = await response.json();

if (!data.issues || data.issues.length === 0) break;
allIssues.push(...data.issues);

if (startAt + data.issues.length >= data.total) break;
startAt += data.issues.length;
}

console.log('total found', allIssues.length);
return allIssues.map(i => i.key);

Pattern 2 β€” Error handling per item in bulk operations​

When processing multiple items, catch per-item errors so one failure does not abort the entire run.

const issues = ['TEST-1', 'TEST-2', 'TEST-3'];
const results = { success: [], failed: [] };

for (const key of issues) {
try {
const response = await api.asApp().requestJira(route`/rest/api/3/issue/${key}`, {
method: 'PUT',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ fields: { priority: { name: 'High' } } })
});
if (response.status >= 400) {
const err = await response.text();
results.failed.push({ key, error: err });
} else {
results.success.push(key);
}
} catch (err) {
results.failed.push({ key, error: String(err) });
}
}

console.log('success', results.success.join(', '));
console.log('failed', results.failed.map(f => `${f.key}: ${f.error}`).join('\n'));
return results;

Pattern 3 β€” Check response status before parsing JSON​

Always check response.status before calling .json(). A 4xx/5xx response body may not be valid JSON.

const response = await api.asApp().requestJira(route`/rest/api/3/issue/TEST-999`, {
headers: { 'Accept': 'application/json' }
});

if (!response.ok) {
const errorText = await response.text();
console.error('API error', response.status, errorText);
return { error: response.status, details: errorText };
}

const issue = await response.json();
console.log('summary', issue.fields.summary);
return issue;

Pattern 4 β€” Return structured output for easy log reading​

Return an object with named fields rather than a primitive. The return value is logged as return: ${String(result)} β€” objects will be [object Object]. Use console.log with JSON for human-readable output instead.

const summary = {
processed: 42,
updated: 38,
skipped: 4,
errors: [],
};

// Log the full object for the invocation log
console.log('run summary', JSON.stringify(summary, null, 2));

// Return a simple status
return `processed=${summary.processed}, updated=${summary.updated}, errors=${summary.errors.length}`;

Complete Examples​

Example 1 β€” Jira REST API (check server info)​

/* no import needed β€” 'api', 'route', 'fetch', 'authorize' already available as global variables */

const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: {
'Accept': 'application/json'
}
});

const responseData = await response.json();

console.log('responseData', responseData);

/*
This is an example of getting issues using JQL.
Read more about Jira Query Language (JQL) here:
https://support.atlassian.com/jira-service-management-cloud/docs/use-advanced-search-with-jira-query-language-jql/
Learn more about the REST API in this example here:
https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-get
*/
const jql = 'created > -7d'; // JQL: get all the issues created last 7 days

const response = await api.asApp().requestJira(route`/rest/api/3/search?jql=${jql}`, {
headers: {
'Accept': 'application/json'
}
});
const issuesResponse = await response.json();

// Return all issue keys for the specified JQL
const foundIssueKeys = issuesResponse.issues.map((issue) => issue.key).join(', ');

console.log('foundIssueKeys', foundIssueKeys);

Example 3 β€” Fetch External Resources​

/*
Fetch External Resources
In this example, we demonstrate how to use the fetch function to access any external REST API.
Specifically, we will request the complete list of available apps from the Atlassian Marketplace.
*/
const response = await fetch('https://marketplace.atlassian.com/rest/2/addons/vendor/467796300');
const data = await response.json();

console.log('Best apps:', data);

Example 4 β€” Forge Bridge API globals (Confluence)​

/* no import needed β€” 'api', 'route', 'fetch', 'authorize' already available as global variables */

console.log(typeof api !== 'undefined' && api ? 'api is defined' : 'api is undefined');
console.log(typeof route !== 'undefined' && route ? 'route is defined' : 'route is undefined');
console.log(typeof fetch !== 'undefined' && fetch ? 'fetch is defined' : 'fetch is undefined');
console.log(typeof authorize !== 'undefined' && authorize ? 'authorize is defined' : 'authorize is undefined');

Example 5 β€” Confluence REST API​

/* no import needed β€” 'api', 'route', 'fetch', 'authorize' already available as global variables */

const response = await api.asApp().requestConfluence(route`/wiki/rest/api/user/current`, {
headers: {
'Accept': 'application/json'
}
});

const responseData = await response.json();

console.log('responseData', responseData);

return responseData;

Example 6 β€” Bulk transition stale Jira issues​

/*
Close all issues in project KEY that have been open and unupdated for more than 90 days.
Customize: jql, transitionId (get from /rest/api/3/issue/{key}/transitions).
*/
const jql = 'project = "KEY" AND status != Done AND updated < -90d';
const transitionId = '31'; // Get this from the transitions API for your workflow

let startAt = 0;
const processed = [];
const errors = [];

while (true) {
const searchResp = await api.asApp().requestJira(
route`/rest/api/3/search?jql=${jql}&startAt=${startAt}&maxResults=50&fields=summary,status`,
{ headers: { 'Accept': 'application/json' } }
);
const searchData = await searchResp.json();
if (!searchData.issues?.length) break;

for (const issue of searchData.issues) {
const transResp = await api.asApp().requestJira(
route`/rest/api/3/issue/${issue.key}/transitions`,
{
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ transition: { id: transitionId } })
}
);
if (transResp.status === 204) {
processed.push(issue.key);
} else {
const errText = await transResp.text();
errors.push(`${issue.key}: ${errText}`);
}
}

if (startAt + searchData.issues.length >= searchData.total) break;
startAt += searchData.issues.length;
}

console.log('transitioned', processed.join(', '));
if (errors.length) console.error('errors', errors.join('\n'));
return `transitioned=${processed.length}, errors=${errors.length}`;

Example 7 β€” Post a daily summary to an external webhook (Slack, Teams, etc.)​

/*
Every day, count open high-priority issues and post a summary to an external webhook.
Set WEBHOOK_URL to your Slack/Teams/other incoming webhook URL.
*/
const WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'; // ← configure this
const jql = 'priority in (Highest, High) AND status != Done';

const searchResp = await api.asApp().requestJira(
route`/rest/api/3/search?jql=${jql}&maxResults=0`,
{ headers: { 'Accept': 'application/json' } }
);
const searchData = await searchResp.json();
const count = searchData.total;

const message = { text: `*Open high-priority issues:* ${count}` };

const webhookResp = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});

console.log('webhook status', webhookResp.status);
return `posted count=${count}, webhook=${webhookResp.status}`;

Writing Good Scripts β€” Rules​

  1. Always use api.asApp() β€” there is no current user in a scheduled job. Calls without api.asApp() will fail. Never use api.asUser() in this context.

  2. Never use requestJira, view, context, or showFlag β€” these are frontend globals from Script Console and Fragments. They do not exist in scheduled jobs and will throw ReferenceError.

  3. Use console.log for observability β€” the return value is logged as a plain string (return: [object Object] for objects). Use console.log('label', JSON.stringify(obj)) for readable structured output in the Logs panel.

  4. Check response.status before .json() β€” a failed API call returns a non-JSON error body on 4xx/5xx. Call response.text() on error to read the raw message.

  5. Paginate all list queries β€” Jira search and Confluence list APIs return at most 50 results by default. Always implement a startAt / while loop for operations that need all results.

  6. Catch errors per item in bulk loops β€” a single failed update should not abort the entire job. Wrap per-item operations in try/catch and collect errors separately.

  7. Remember the 60-minute timing floor β€” even a * * * * * cron expression will only execute approximately once per hour due to how Forge triggers work. Do not design logic that depends on sub-hourly precision.

  8. Respect the 5-job limit β€” each instance of Script Master supports a maximum of 5 scheduled jobs. Design jobs to be composable (one job can handle multiple tasks internally) rather than creating many single-purpose jobs.

  9. Do not use forbidden globals β€” eval, require, import(), process, new Function, new Proxy, structuredClone, .prototype, .__proto__, .constructor will either be blocked at parse time or throw at runtime with "Unsafe code detected".

  10. Always configure a Companion App β€” local execution is not supported. If a Companion App is not configured, every run will fail with "Running scripts locally is not supported in this version of Script Master". The test panel will show this error before running any code.


Jira REST API v3​

EndpointDescriptionDocs
GET /rest/api/3/serverInfoInstance infolink
GET /rest/api/3/search?jql=...Search issues via JQLlink
GET /rest/api/3/issue/{issueIdOrKey}Get issuelink
POST /rest/api/3/issueCreate issuelink
PUT /rest/api/3/issue/{issueIdOrKey}Update issue fieldslink
POST /rest/api/3/issue/{issueIdOrKey}/transitionsTransition issuelink
GET /rest/api/3/issue/{issueIdOrKey}/transitionsList available transitionslink
POST /rest/api/3/issue/{issueIdOrKey}/commentAdd commentlink
GET /rest/api/3/projectList projectslink
GET /rest/api/3/users/searchSearch userslink
GET /rest/api/3/filterGet filterslink
GET /rest/api/3/fieldList all fieldslink

Confluence REST API v1 / v2​

EndpointDescriptionDocs
GET /wiki/rest/api/user/currentCurrent app user infolink
GET /wiki/rest/api/spaceList spaceslink
GET /wiki/rest/api/contentList pages/blogpostslink
GET /wiki/rest/api/content/{id}Get pagelink
POST /wiki/rest/api/contentCreate pagelink
PUT /wiki/rest/api/content/{id}Update pagelink
DELETE /wiki/rest/api/content/{id}Trash pagelink
GET /wiki/api/v2/pagesList pages (v2)link
GET /wiki/api/v2/spacesList spaces (v2)link

Forge References​