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
awaitis 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, norequestJira/requestConfluenceshorthand (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)β
- Forge fires the scheduler trigger periodically (approximately every hour).
- The trigger fetches all enabled scheduled jobs.
- For each job, it parses the cron expression and checks if the previous scheduled execution time is within the past 60 minutes.
- If yes, the job ID is pushed to the
scheduled-jobs-queueForge Queue. - The queue listener dequeues the job, fetches its code from storage, and calls
runScript(). runScript()POSTs the script and args to the configured remote Companion App.- The Companion App executes the script and returns
{ result, error, logs }. - 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.
| Global | Type | Description |
|---|---|---|
api | ForgeAPI | Forge API object β use api.asApp().requestJira(...) or api.asApp().requestConfluence(...) |
route | Template-literal tag | Construct safe API route strings: route`/rest/api/3/issue/${id}` |
fetch | (url, options?) => Promise<Response> | Standard fetch for external HTTP calls (non-Atlassian APIs) |
authorize | ForgeAuthorize | Forge authorization utilities |
console | Console | log, 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, andcontextare 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);
Example 2 β Perform a JQL Searchβ
/*
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β
-
Always use
api.asApp()β there is no current user in a scheduled job. Calls withoutapi.asApp()will fail. Never useapi.asUser()in this context. -
Never use
requestJira,view,context, orshowFlagβ these are frontend globals from Script Console and Fragments. They do not exist in scheduled jobs and will throwReferenceError. -
Use
console.logfor observability β the return value is logged as a plain string (return: [object Object]for objects). Useconsole.log('label', JSON.stringify(obj))for readable structured output in the Logs panel. -
Check
response.statusbefore.json()β a failed API call returns a non-JSON error body on 4xx/5xx. Callresponse.text()on error to read the raw message. -
Paginate all list queries β Jira search and Confluence list APIs return at most 50 results by default. Always implement a
startAt/whileloop for operations that need all results. -
Catch errors per item in bulk loops β a single failed update should not abort the entire job. Wrap per-item operations in
try/catchand collect errors separately. -
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. -
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.
-
Do not use forbidden globals β
eval,require,import(),process,new Function,new Proxy,structuredClone,.prototype,.__proto__,.constructorwill either be blocked at parse time or throw at runtime with"Unsafe code detected". -
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.
Useful REST API Linksβ
Jira REST API v3β
| Endpoint | Description | Docs |
|---|---|---|
GET /rest/api/3/serverInfo | Instance info | link |
GET /rest/api/3/search?jql=... | Search issues via JQL | link |
GET /rest/api/3/issue/{issueIdOrKey} | Get issue | link |
POST /rest/api/3/issue | Create issue | link |
PUT /rest/api/3/issue/{issueIdOrKey} | Update issue fields | link |
POST /rest/api/3/issue/{issueIdOrKey}/transitions | Transition issue | link |
GET /rest/api/3/issue/{issueIdOrKey}/transitions | List available transitions | link |
POST /rest/api/3/issue/{issueIdOrKey}/comment | Add comment | link |
GET /rest/api/3/project | List projects | link |
GET /rest/api/3/users/search | Search users | link |
GET /rest/api/3/filter | Get filters | link |
GET /rest/api/3/field | List all fields | link |
Confluence REST API v1 / v2β
| Endpoint | Description | Docs |
|---|---|---|
GET /wiki/rest/api/user/current | Current app user info | link |
GET /wiki/rest/api/space | List spaces | link |
GET /wiki/rest/api/content | List pages/blogposts | link |
GET /wiki/rest/api/content/{id} | Get page | link |
POST /wiki/rest/api/content | Create page | link |
PUT /wiki/rest/api/content/{id} | Update page | link |
DELETE /wiki/rest/api/content/{id} | Trash page | link |
GET /wiki/api/v2/pages | List pages (v2) | link |
GET /wiki/api/v2/spaces | List spaces (v2) | link |