Script API reference
Every global, type and value an Oktul Code Studio script uses, as the TypeScript declarations the console's editor reads.
Oktul Code Studio’s console checks a script against these TypeScript declarations and completes from them. They are taken from Code Studio’s source, so they list exactly what a run defines, and a script imports none of them. How to use them is in writing scripts.
Globals and the contexts
/** A request's options: query parameters and headers. */interface CcRequestOptions { query?: Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>; headers?: Record<string, string | number>;}
/** A response as the host answered it. In the sandbox a write is answered with a simulated one. */interface CcResponse { status: number; statusText?: string; headers: Record<string, string>; body: string; /** True when the sandbox recorded the request instead of sending it. */ simulated?: boolean;}
/** Thrown by the verbs on a status of 400 or more. */declare class HttpError extends Error { readonly name: 'HttpError'; readonly status: number; readonly method: string; readonly target: string; /** The response's headers, lower case: retry-after on a 429 or 503 says how long to wait. */ readonly headers: Record<string, string>; /** The response body, parsed when it is JSON. */ readonly body: unknown;}
/** Jira, Confluence or Assets, by the product's own REST paths. Requests run as the run's principal. */interface CcProductClient { get<T = any>(path: string, options?: CcRequestOptions): Promise<T>; delete<T = any>(path: string, options?: CcRequestOptions): Promise<T>; post<T = any>(path: string, body?: unknown, options?: CcRequestOptions): Promise<T>; put<T = any>(path: string, body?: unknown, options?: CcRequestOptions): Promise<T>; patch<T = any>(path: string, body?: unknown, options?: CcRequestOptions): Promise<T>; /** The raw response; never throws on a status. */ request(request: { method?: string; path: string; query?: CcRequestOptions['query']; headers?: CcRequestOptions['headers']; body?: unknown }): Promise<CcResponse>;}
/** Jira's REST APIs: /rest/api/3, /rest/agile/1.0, /rest/servicedeskapi. */declare const jira: CcProductClient;/** Confluence's REST APIs: /wiki/api/v2 and /wiki/rest/api. */declare const confluence: CcProductClient;/** Assets, with paths relative to the site's workspace: /object/1, /object/aql. */declare const assets: CcProductClient;
/** Another system, through a connection an administrator set up; the connection adds its credentials. */declare const http: { get<T = any>(connection: string, url: string, options?: CcRequestOptions): Promise<T>; delete<T = any>(connection: string, url: string, options?: CcRequestOptions): Promise<T>; post<T = any>(connection: string, url: string, body?: unknown, options?: CcRequestOptions): Promise<T>; put<T = any>(connection: string, url: string, body?: unknown, options?: CcRequestOptions): Promise<T>; patch<T = any>(connection: string, url: string, body?: unknown, options?: CcRequestOptions): Promise<T>; /** The raw response. With connection null, a host an administrator approved, with no credentials. */ request(request: { connection?: string | null; method?: string; url: string; query?: CcRequestOptions['query']; headers?: CcRequestOptions['headers']; body?: unknown }): Promise<CcResponse>;};
declare const connections: { /** The connection keys this site has. */ list(): Promise<string[]>;};
interface CcStore { get<T = any>(key: string): Promise<T | null>; set(key: string, value: unknown, options?: { ttlSeconds?: number }): Promise<void>; delete(key: string): Promise<void>; list(prefix?: string, cursor?: string | null): Promise<{ keys: string[]; cursor: string | null }>;}
/** JSON values kept between runs, for the scripts of this script's space. */declare const store: CcStore & { /** The site's own values, for every space: a run an administrator starts, or a listener, schedule or other trigger. */ shared: CcStore;};
/** The run's log, read in Code Studio. Secret variable values are blanked out. */declare const log: { debug(...values: unknown[]): void; info(...values: unknown[]): void; warn(...values: unknown[]): void; error(...values: unknown[]): void;};
/** * What every run's context holds, whatever started it: the second argument of a script's default * export. Each way a script is started hands a context of its own that extends this one * (ListenerContext, ScheduleContext, ApiTriggerContext, AutomationContext, FieldContext), so a script * names the one for its trigger and reads what that trigger always gives without checking for it. * A run from the console's Run button gets this one. */interface ScriptContext<State = unknown> { /** Script variables, by name. */ environment: { vars: CcVariables }; runId: string; mode: 'sandbox' | 'production'; /** The state of the last checkpoint when this run continued from one, else null. */ resumedFrom: State | null; /** Marks where a continuation may resume, with the state it needs. */ checkpoint(state: State): void;}
// --- Listeners ---
/** A Jira user, as GET /rest/api/3/user answers. */interface JiraUser { accountId: string; displayName?: string; /** Absent when the person's profile visibility hides it. */ emailAddress?: string; active?: boolean; accountType?: 'atlassian' | 'app' | 'customer' | string; timeZone?: string; [property: string]: any;}
/** A work item's fields. The named ones are always there; every other field, custom fields included, is read by id. */interface JiraWorkItemFields { summary: string; issuetype: { id: string; name: string; subtask?: boolean; [property: string]: any }; project: { id: string; key: string; name: string; [property: string]: any }; status: { id: string; name: string; statusCategory?: { key: 'new' | 'indeterminate' | 'done' | string; [property: string]: any }; [property: string]: any }; assignee?: JiraUser | null; reporter?: JiraUser | null; priority?: { id: string; name: string; [property: string]: any } | null; labels?: string[]; /** Rich text: an Atlassian Document Format document. */ description?: any; /** ISO date-time. */ created?: string; /** ISO date-time. */ updated?: string; [fieldId: string]: any;}
/** A work item as GET /rest/api/3/issue/{key} answers it. */interface JiraWorkItem { id: string; key: string; self?: string; fields: JiraWorkItemFields;}
/** What an update changed, one item per field. */interface JiraChangelog { id?: string; items: Array<{ field: string; fieldId?: string; fieldtype?: string; from: string | null; fromString: string | null; to: string | null; toString: string | null }>;}
interface JiraComment { id: string; author?: JiraUser; /** Rich text: an Atlassian Document Format document. */ body?: any; created?: string; updated?: string; [property: string]: any;}
/** * Any Jira event a listener receives: the webhook payload a ScriptRunner Connect listener reads. * Which fields are present depends on the event; JiraWorkItemEvent is the one for work item events. */interface JiraEvent { /** The webhook's event name, such as jira:issue_updated, comment_created or sprint_started. */ webhookEvent: string; /** Epoch milliseconds. */ timestamp: number; issue?: JiraWorkItem; /** The person who made the change, read in full for work item and comment events. */ user?: JiraUser; changelog?: JiraChangelog; /** issue_created, issue_updated, issue_assigned, issue_commented and so on. */ issue_event_type_name?: string; comment?: JiraComment; [property: string]: any;}
/** A work item created, updated, assigned, commented, or deleted: the work item is always there, read in full. */interface JiraWorkItemEvent extends JiraEvent { issue: JiraWorkItem;}
/** A page, blog post or comment, as a Confluence Connect webhook gave it. */interface ConfluenceContent { id: string; title: string | null; spaceKey: string | null; creatorAccountId: string | null; lastModifierAccountId: string | null; /** Epoch milliseconds. */ creationDate: number | null; /** Epoch milliseconds. */ modificationDate: number | null; version: number | null; contentType: 'page' | 'blogpost' | 'comment' | string; /** The storage format, read for page, blog post and comment events. */ body?: string;}
/** Any Confluence event a listener receives: the Connect webhook payload. */interface ConfluenceEvent { /** The webhook's event name, such as page_created or comment_updated. */ webhookEvent: string; /** Epoch milliseconds. */ timestamp: number; /** The person who made the change; null for a change the platform made. */ userAccountId: string | null; page?: ConfluenceContent; blog?: ConfluenceContent; comment?: ConfluenceContent; [property: string]: any;}
/** A page created, updated, moved, trashed and so on: the page is always there. */interface ConfluencePageEvent extends ConfluenceEvent { page: ConfluenceContent;}
/** What any listener receives. */type ListenerEvent = JiraEvent | ConfluenceEvent;
/** The context of a run a listener started. */interface ListenerContext<State = unknown> extends ScriptContext<State> { /** The event exactly as Forge delivered it; absent when it was too large to keep beside the event. */ forgeEvent?: { eventType: string; [property: string]: any };}
// --- Schedules ---
/** What a schedule hands its script. */interface ScheduleEvent { type: 'schedule'; scheduleId: string; /** ISO date-time of the minute this occurrence was due, so a run that starts late still works on the right day. */ scheduledFor: string;}
/** The context of a run a schedule started. */interface ScheduleContext<State = unknown> extends ScriptContext<State> {}
// --- API triggers ---
/** The HTTP request that started an API trigger's run. A JSON object body's own fields are on the event too. */interface ApiTriggerEvent<Body = unknown> { method: 'GET' | 'POST'; /** What the caller put after the trigger's URL; empty when nothing. */ path: string; /** Header names in lower case. Authorization is never handed to the script. */ headers: Record<string, string>; queryString: string; queryStringParams: Record<string, string>; sourceIp: string; /** A POST's body: parsed JSON, text for text and forms, base64 for anything else; absent when empty and on a GET. */ body?: Body; bodyType?: 'json' | 'text' | 'base64'; [property: string]: any;}
/** * What a script returns to shape the HTTP answer, when its trigger waits for the result. * Anything else a script returns is sent as JSON with status 200, and nothing at all as 204. */interface ApiTriggerResponse { /** 200 to 599. */ statusCode: number; headers?: Record<string, string | number | Array<string | number>>; /** A string is sent as it is; anything else as JSON. */ body?: unknown;}
/** The context of a run an API trigger started. */interface ApiTriggerContext<State = unknown> extends ScriptContext<State> {}
// --- Automation ---
/** What Jira automation's Run a Code Studio script action hands its script. */interface AutomationEvent<Params = any> { type: 'automation'; /** The rule's parameters with smart values filled in: parsed when they are JSON, the text itself otherwise; null when there are none. */ params: Params | null; /** The work item the rule ran on, or null for a rule without one. */ issue: { id: string | null; key: string | null } | null; /** The rule's name. */ rule: string | null; /** The account the rule ran for. */ initiator: string | null;}
/** The context of a run an automation rule started. */interface AutomationContext<State = unknown> extends ScriptContext<State> {}
// --- Scripted fields ---
/** * The work item a scripted field's default export computes a value for: GET /rest/api/3/issue/{key} * with every field. On a form, where the value is a preview, it is built from the form: id is null * on a create screen, key is null, and fields holds what the form holds. */interface FieldWorkItem { id: string | null; key: string | null; fields: Record<string, any>;}
/** Where a preview of a scripted field runs: the form beside it, in the browser of whoever fills it in. */interface FieldFormContext { view: string; projectId: string | null; issueTypeId: string | null; /** The work item's id on a transition; null on a create screen. */ issueId: string | null; accountId: string | null; requestTypeId: string | null;}
/** * The context of a scripted field's run: its default export and its options export get this one. * On a form the value is a preview the browser computes, and form says where: there the script * reads Jira as the person, writes nothing, has no variables, and its log goes to the browser's console. */interface FieldContext<State = unknown> extends ScriptContext<State> { field: { id: string; name: string; type: string; contextId: string | null }; /** Null when Code Studio computes the stored value. */ form: FieldFormContext | null;}
// --- Dynamic forms ---
/** One field of a form, with the changes a dynamic form's script may ask for. Each setter returns the field. */interface FormField { id: string; name: string; type: string; value: any; visible: boolean; required: boolean; readOnly: boolean; description: string; /** False for a field this form does not hold: its setters then do nothing, so one script fits several forms. */ present: boolean; setVisible(visible?: boolean): FormField; setHidden(hidden?: boolean): FormField; setRequired(required?: boolean): FormField; setReadOnly(readOnly?: boolean): FormField; setName(name: string): FormField; setDescription(description: string): FormField; setValue(value: unknown): FormField; clear(): FormField; /** Shows only these option ids, or hides them, up to 100. */ setOptionsVisibility(optionIds: string[], visible?: boolean): FormField;}
/** The Jira form a dynamic form's script changes, as it opens or as a field changes. */interface Form { /** init as the form opens, change when a field changed. */ trigger: 'init' | 'change'; view: string; fields: FormField[]; /** The field whose change ran the script; null as the form opens. */ changed: FormField | null; /** A field by id or by name; a field the form does not hold has present false. */ field(idOrName: string): FormField; has(idOrName: string): boolean;}
/** * The context of a dynamic form's script. It runs in the browser of whoever fills in the form and * reads Jira as them: jira.get only, no variables, no storage, its log in the browser's console. */interface FormContext { trigger: 'init' | 'change'; view: string; projectId: string | null; issueTypeId: string | null; /** The work item's id on the work item view and a transition; null on a create screen. */ issueId: string | null; accountId: string | null; /** On a portal's request form only. */ portalId?: string | null; requestTypeId?: string | null;}
/** Script variables. The console adds this site's own names. */interface CcVariables { [name: string]: string | undefined;}The editor adds this site’s own variable keys to CcVariables, so
context.environment.vars. completes them.
Besides these, a script has the Web APIs fetch, Response, Headers, URL,
URLSearchParams, TextEncoder, atob, btoa, setTimeout, clearTimeout and
crypto.randomUUID, and console.log, info, debug, warn and error, which write the run’s
log. There is no TextDecoder, no Node module and no npm package.
What each trigger hands a script
A script’s default export takes (event, context). Each trigger hands an event and a context of
its own, so a script names the pair for its trigger and reads what that trigger always gives
without checking whether it is there. Every context extends ScriptContext, and ScriptContext
alone has no catch-all key, so a misspelt name is a type error rather than undefined.
| Started by | event |
context |
What the script returns |
|---|---|---|---|
| The console’s Run button | The test event you give, or null |
ScriptContext |
Shown as the run’s result |
| A Jira listener on a work item event | JiraWorkItemEvent, the work item read in full |
ListenerContext |
Recorded on the run |
| Any other Jira listener | JiraEvent |
ListenerContext |
Recorded on the run |
| A Confluence listener on a page event | ConfluencePageEvent |
ListenerContext |
Recorded on the run |
| Any other Confluence listener | ConfluenceEvent |
ListenerContext |
Recorded on the run |
| A schedule | ScheduleEvent |
ScheduleContext |
Recorded on the run |
| An API trigger | ApiTriggerEvent<Body> |
ApiTriggerContext |
When the trigger waits for the result: the HTTP answer, as ApiTriggerResponse or any JSON value |
| A Jira automation rule | AutomationEvent<Params> |
AutomationContext |
{{triggeredCodestudioRun.result}}, when the step waits |
| A scripted field | FieldWorkItem |
FieldContext |
The field’s value, below |
| A dynamic form | Form |
FormContext |
Nothing: the script changes the form |
A schedule’s and an API trigger’s contexts add nothing to ScriptContext today. They are named
anyway, so a script written against one keeps compiling when they gain a member.
In TypeScript:
export default async function (event: JiraWorkItemEvent, context: ListenerContext) { log.info(event.issue.key, event.issue.fields.status.name);}In JavaScript the same types are written as JSDoc. The console completes from them; it checks types in .ts scripts only.
/** @param {ApiTriggerEvent<{ key: string }>} event @param {ApiTriggerContext} context @returns {Promise<ApiTriggerResponse>} */export default async function (event, context) { return { statusCode: 200, body: { received: event.body?.key ?? null } };}The console’s New script starts from a template for each trigger with these signatures already written: see writing scripts.
How requests behave
| Call | Answers | On a status of 400 or more |
|---|---|---|
jira.get(path, options) and the other verbs |
The body, parsed when it is JSON; null when it is empty |
Throws HttpError |
jira.request({ method, path, query, headers, body }) |
CcResponse: status, headers (lower case), body as text, simulated |
Answers it |
http.get(connection, url, options) and the other verbs |
As jira’s, from the connection’s base URL with its credential |
Throws HttpError |
http.request({ connection, method, url, … }) |
CcResponse. With connection: null, a host an administrator approved, with no credential |
Answers it |
fetch(url, init) |
A Response, from a host an administrator approved |
Answers it |
- A path starts with
/, as Atlassian’s REST documentation writes it. assetspaths are relative to the site’s workspace:/object/aql,/objectschema/list.- A string body is sent as text, anything else as JSON.
- In the sandbox a write answers
simulated: truewith a simulated body. /rest/api/3/uiModificationsis refused to every script, because Code Studio keeps its dynamic forms there.
What a scripted field’s script returns
A scripted field’s script exports default (workItem, context), which returns one work item’s
value, and for a picked field options(context), which returns the choices. null empties the
field.
| Field type | The default export returns |
|---|---|
| Text | A string of up to 255 characters, on one line |
| Paragraph | A string; each line becomes a paragraph |
| Number | A number, or a string that reads as one |
| Date | A date as 2026-10-15 |
| Date and time | An ISO 8601 string such as 2026-10-15T09:30:00Z, or milliseconds |
| URL | An http or https address |
| Labels | A list of words with no spaces |
| Select, radio buttons | The text of one choice |
| Multi select, checkboxes | A list of choices |
| Cascading select | { parent, child }, two strings |
| User picker, one or several | An account id, or a list of them |
| Group picker, one or several | A group id, or a list of them |
/** A picked field's choices: texts, or { value, label } for people and groups. */export function options(context: FieldContext): Array<string | { value: string; label: string }> | Promise<Array<string | { value: string; label: string }>>;
/** One work item's value, or its default on a picked field. */export default async function (workItem: FieldWorkItem, context: FieldContext): Promise<unknown>;A value the type refuses leaves the work items as they were and becomes a line in the run’s log.
A dynamic form rule
A dynamic form’s script runs in the person’s browser, with jira.get for reading Jira as that
person and nothing else.
export default async function (form: Form, context: FormContext): Promise<void>;Form, FormField and FormContext are declared above.
Which changes run a rule. Code Studio reads the field ids a rule writes as literal strings in
quotes, 'priority' or 'customfield_10050'. A rule that names any runs again only when one of
those fields changes, so a field it reads by display name or by an id built at run time starts no
run. A rule that names no id that way runs on every change.
When each setter applies, and where it does not, is in fields and forms.
Related
A problem or a question? Write to support@oktul.com or open a request in the Help Center (opens in a new tab).