Web Triggers API Reference for AI
This document is a complete API reference and code guide for writing scripts that run in Script Master's Web Triggers module β an Atlassian Forge app that lets users define backend JavaScript functions invoked by external HTTP requests, enabling integration between Jira/Confluence instances and third-party systems (webhooks, automation tools, CI/CD pipelines, etc.).
Use this document as context when asking an AI assistant (Claude, Gemini, ChatGPT, etc.) to generate Web Trigger scripts:
Here is the API reference: [link to this file]. Build a web trigger script for Script Master that [your task].
Execution Modelβ
Scripts run as async JavaScript functions executed on the Forge backend (Node.js 22.x, ARM64, 256 MB memory). This is a server-side FaaS environment β not a browser and not the user's machine. The Web Triggers module wraps your code like this:
const result = await (async function(api, route, fetch, authorize, request, console, /* ...JS globals */) {
// YOUR SCRIPT CODE HERE
})(api, route, fetch, authorize, request, wrappedConsole, /* ...JS globals */);
Key implications:
- Top-level
awaitis supported β use it freely. - The script must
returna response object with at least astatusCodefield. If the return value has nostatusCode, Forge sends a500error to the caller. console.log()/console.error()/ etc. are captured and shown in the test panel. Maximum 100 log entries per run; each entry capped at 4 000 characters.- Errors thrown by the script are caught; the caller receives a
500response. - Scripts run as the app (not as the current user) β use
api.asApp()for all Atlassian API calls. - No browser APIs, no
window, no DOM. - Web Trigger URLs are publicly accessible by default β there is no built-in authentication. You must implement authorization inside the script itself.
Available Globalsβ
Jira and Confluence (identical surface)β
| Global | Type | Description |
|---|---|---|
api | ForgeAPI | Forge API β call api.asApp().requestJira(...) / 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 |
authorize | ForgeAuthorize | Forge authorization utilities |
request | WebTriggerRequest | The incoming HTTP request β method, headers, body, query params, context |
console | Console | log, info, warn, error β output captured in the test panel |
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, btob, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, setTimeout, setInterval, clearTimeout, clearInterval, parseFloat, parseInt, isFinite, isNaN.
Forbidden (will throw): eval, require, import(, process, new Function, new Proxy, structuredClone, queueMicrotask, escape, unescape, any access to .prototype, .__proto__, .constructor via user code, or arguments.callee.caller.
API Referenceβ
api.asApp().requestJira(route, options?)β
Makes an authenticated request to the Jira Cloud REST API as the installed app (service account level).
routeβ use theroutetemplate tag:route`/rest/api/3/...`optionsβ standardfetchRequestInit(method, headers, body)- Returns β a standard
Responseobject
const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
return {
body: JSON.stringify(data),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
api.asApp().requestConfluence(route, options?)β
Makes an authenticated request to the Confluence Cloud REST API as the installed app.
routeβ use theroutetemplate tag:route`/wiki/rest/api/...`optionsβ standardfetchoptions- Returns β a standard
Responseobject
const response = await api.asApp().requestConfluence(route`/wiki/rest/api/user/current`, {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
return {
body: JSON.stringify(data),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
route (template literal tag)β
Constructs a safe API route string. Interpolated values are URL-encoded automatically.
const issueKey = 'PROJ-123';
const path = route`/rest/api/3/issue/${issueKey}`;
// β "/rest/api/3/issue/PROJ-123"
Always use route instead of template literals when building Atlassian API paths.
fetch(url, options?)β
Standard fetch for calling external HTTP endpoints. No built-in authentication β pass your own credentials as needed.
const response = await fetch('https://api.example.com/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'issue_created' }),
});
return {
body: JSON.stringify({ forwarded: response.status }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
request (WebTriggerRequest)β
The incoming HTTP request object passed to every Web Trigger invocation.
interface WebTriggerRequest {
method: string; // "GET", "POST", "PUT", etc.
headers: Record<string, string[]>; // header names are lowercase
body: string; // raw request body as string
path: string; // path after the trigger base URL
queryParameters: Record<string, string[]>; // query params, values are arrays
context: {
cloudId: string; // Atlassian site cloud ID
moduleKey: string; // Forge module key of this trigger
userAccess?: {
enabled?: boolean;
};
};
}
Accessing request data:
// Parse JSON body
const payload = JSON.parse(request.body);
// Read a header (values are always arrays)
const authHeader = request.headers['authorization']?.[0];
// Read a query parameter
const projectKey = request.queryParameters['project']?.[0];
Response object (return value)β
Every script must return a WebTriggerResponse-shaped object. The statusCode field is required β omitting it causes Forge to respond with 500.
interface WebTriggerResponse {
statusCode: number; // REQUIRED
statusText?: string;
body?: string; // must be a string; JSON.stringify objects
headers?: Record<string, string[]>; // values must be arrays
}
// Minimal valid response
return { statusCode: 200 };
// Full response with JSON body
return {
body: JSON.stringify({ ok: true }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
// Error response
return { statusCode: 400, statusText: 'Bad Request' };
Code Patternsβ
Pattern: Authorization via header checkβ
Web Trigger URLs are public by default. Always validate callers inside the script:
const SECRET = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
if (request.headers['authorization']?.[0] !== SECRET) {
return { statusCode: 403, statusText: 'Forbidden' };
}
// ... rest of handler
Pattern: Parse and validate JSON bodyβ
let payload;
try {
payload = JSON.parse(request.body);
} catch {
return { statusCode: 400, statusText: 'Invalid JSON body' };
}
if (!payload.issueKey) {
return { statusCode: 422, statusText: 'Missing issueKey' };
}
// ... process payload
Pattern: Route by HTTP methodβ
if (request.method === 'GET') {
// handle GET
return { statusCode: 200, body: '...' };
}
if (request.method === 'POST') {
// handle POST
return { statusCode: 201, body: '...' };
}
return { statusCode: 405, statusText: 'Method Not Allowed' };
Pattern: Route by query parameterβ
Consolidate multiple logical operations into one trigger using a query parameter:
const action = request.queryParameters['action']?.[0];
if (action === 'sync-issues') {
// ...
return { statusCode: 200, body: JSON.stringify({ synced: true }) };
}
if (action === 'status') {
return { statusCode: 200, body: JSON.stringify({ status: 'ok' }) };
}
return { statusCode: 400, statusText: `Unknown action: ${action}` };
Complete Examplesβ
Example 1: Echo the request (inspect all fields)β
return {
body: JSON.stringify({ request }, null, 2),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
Example 2: Authorization header checkβ
const YOUR_SECRET_KEY = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
if (request.headers['authorization']?.[0] !== YOUR_SECRET_KEY) {
return { statusCode: 403, statusText: 'Not Permitted' };
}
return { statusCode: 200, statusText: 'OK' };
Example 3: Jira REST API call (server info)β
const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: { 'Accept': 'application/json' },
});
const responseData = await response.json();
return {
body: JSON.stringify(responseData),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
Example 4: Jira response with custom headers and error branchβ
if (typeof somethingWrong !== typeof undefined) {
return { statusCode: 400, statusText: 'Bad request' };
}
return {
body: JSON.stringify({ someKey: 'someValue' }),
headers: {
'Content-Type': ['application/json'],
'X-Request-Id': [`rnd-${Math.random()}`],
},
statusCode: 200,
statusText: 'OK',
};
Example 5: Jira license and user countβ
const licenseResponse = await api.asApp().requestJira(route`/rest/api/3/instance/license`, {
headers: { 'Accept': 'application/json' },
});
const licenseData = await licenseResponse.json();
const userCountResponse = await api.asApp().requestJira(route`/rest/api/3/license/approximateLicenseCount`, {
headers: { 'Accept': 'application/json' },
});
const userCountData = await userCountResponse.json();
return {
body: JSON.stringify({ licenses: licenseData, users: userCountData }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
Example 6: Confluence REST API callβ
const response = await api.asApp().requestConfluence(route`/wiki/rest/api/user/current`, {
headers: { 'Accept': 'application/json' },
});
const responseData = await response.json();
return {
body: JSON.stringify({ bodyContent: responseData }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};
Example 7: Webhook receiver β create a Jira issue from an external eventβ
// Validate caller
const SECRET = 'your-shared-secret-here';
if (request.headers['x-webhook-secret']?.[0] !== SECRET) {
return { statusCode: 401, statusText: 'Unauthorized' };
}
// Parse payload
let event;
try {
event = JSON.parse(request.body);
} catch {
return { statusCode: 400, statusText: 'Invalid JSON' };
}
// Create Jira issue
const response = await api.asApp().requestJira(route`/rest/api/3/issue`, {
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: {
project: { key: 'OPS' },
issuetype: { name: 'Task' },
summary: `[External] ${event.title ?? 'Untitled event'}`,
description: {
type: 'doc',
version: 1,
content: [{ type: 'paragraph', content: [{ type: 'text', text: event.description ?? '' }] }],
},
},
}),
});
const issue = await response.json();
return {
body: JSON.stringify({ created: issue.key }),
headers: { 'Content-Type': ['application/json'] },
statusCode: response.status === 201 ? 201 : response.status,
statusText: response.statusText,
};
Writing Good Scripts β Rulesβ
- Always return an object with
statusCodeβ any other return value (or no return) causes Forge to send a500to the caller. ThestatusCodefield is not optional. - Authenticate every request β Web Trigger URLs are publicly reachable. Always validate an
Authorizationheader or a shared-secret query parameter before doing any work. - JSON-stringify the body β the
bodyfield must be astring. Pass objects throughJSON.stringify(...), not raw objects. - Headers values must be arrays β
headersisRecord<string, string[]>. Write'Content-Type': ['application/json'], not'Content-Type': 'application/json'. - Use
api.asApp()β Web Triggers run as the Forge app, not as any Atlassian user. Always chain.asApp()before.requestJira()/.requestConfluence(). - Use the
routetag for Atlassian paths β it auto-encodes interpolated values and is required for Forge's internal request routing. - Parse
bodyexplicitly βrequest.bodyis always a plain string. CallJSON.parse(request.body)for JSON payloads; wrap in try/catch and return400on failure. - Header and query param values are arrays β access them with
?.[0]:request.headers['content-type']?.[0],request.queryParameters['id']?.[0]. - Max 20 triggers per instance β for multiple logical operations, use one trigger and branch on a query parameter or HTTP method.
- No Node.js APIs β this is a Forge FaaS environment.
fs,path,os,child_process,require,import(are all unavailable and will throw.
Useful REST API Linksβ
Jira Cloud REST API v3β
- Server info:
GET /rest/api/3/serverInfoβ docs - Get current user:
GET /rest/api/3/myselfβ docs - Search issues (JQL):
GET /rest/api/3/search?jql=...β docs - Get issue:
GET /rest/api/3/issue/{issueIdOrKey}β docs - Create issue:
POST /rest/api/3/issueβ docs - Update issue:
PUT /rest/api/3/issue/{issueIdOrKey}β docs - Transition issue:
POST /rest/api/3/issue/{issueIdOrKey}/transitionsβ docs - Add comment:
POST /rest/api/3/issue/{issueIdOrKey}/commentβ docs - Get projects:
GET /rest/api/3/project/searchβ docs - Instance license:
GET /rest/api/3/instance/licenseβ docs - Approximate license count:
GET /rest/api/3/license/approximateLicenseCount
Confluence REST APIβ
- Get current user:
GET /wiki/rest/api/user/current - Get spaces:
GET /wiki/api/v2/spacesβ docs - Get pages:
GET /wiki/api/v2/pagesβ docs - Create page:
POST /wiki/api/v2/pagesβ docs - Update page:
PUT /wiki/api/v2/pages/{id}β docs - Delete/purge page:
DELETE /wiki/api/v2/pages/{id}?purge=trueβ docs