Whenever JavaScript on an Oracle APEX page needs data from the database, or needs to run PL/SQL, it goes through apex.server. apex.server.process calls an Ajax Callback process and hands back the result as a promise, taking care of the session, page items, and errors for you.
This guide covers the whole of apex.server: sending values and items, handling errors, queuing requests, calling plug-in Ajax functions, building GET URLs, loading scripts, and splitting long text. It also covers the smaller namespaces that go with server calls: apex.da for dynamic action plug-ins, apex.event.trigger, and apex.debug for logging at the page's debug level. Two recipes at the end show a lookup as the user types and several calls running in parallel.
Quick Reference
| Function | What it does |
|---|---|
| apex.server.process | Calls an Ajax Callback process and returns a promise |
| apex.server.plugin | Calls a plug-in's PL/SQL Ajax function |
| apex.server.url, pluginUrl | Build the URL of a GET request to a page process or plug-in |
| apex.server.loadScript | Loads a JavaScript file, RequireJS-aware |
| apex.server.chunk | Splits long text into pieces of 8,000 characters |
| apex.da.resume, handleAjaxErrors, cancel | Continue, fail, or stop a dynamic action from plug-in code |
| apex.event.trigger | Triggers an event and reports whether a handler cancelled it |
| apex.debug.getLevel, setLevel | Read or change the page's debug level |
| apex.debug.message, log, error, warn, info, trace | Write to the console at a debug level |
The Ajax Callback Used in the Examples
apex.server.process calls an Ajax Callback process: an application process, or a process on the current page, whose processing point is Ajax Callback. Every example in this guide calls one application process named ORDER_SUMMARY. It reads an order ID from x01, optionally waits x02 seconds so slow requests can be demonstrated, and writes a JSON object with APEX_JSON:
declare
l_order_id orb_orders.order_id%type := to_number(apex_application.g_x01);
begin
if apex_application.g_x02 is not null then
dbms_session.sleep(to_number(apex_application.g_x02));
end if;
for r in (select o.order_number, o.status, o.order_date, o.order_total, c.customer_name,
(select count(*) from orb_order_items i where i.order_id = o.order_id) item_count
from orb_orders o
join orb_customers c on c.customer_id = o.customer_id
where o.order_id = l_order_id)
loop
apex_json.open_object;
apex_json.write('orderNumber', r.order_number);
apex_json.write('customer', r.customer_name);
apex_json.write('status', r.status);
apex_json.write('orderDate', to_char(r.order_date, 'YYYY-MM-DD'));
apex_json.write('total', r.order_total);
apex_json.write('lines', r.item_count);
apex_json.write('P20_TEXT', v('P20_TEXT'));
apex_json.close_object;
return;
end loop;
apex_json.open_object;
apex_json.write('error', 'Order ' || apex_application.g_x01 || ' does not exist.');
apex_json.close_object;
end;The tables it queries, orb_orders, orb_customers, and orb_order_items, are part of the Orbit Outfitters sample schema, which you can install from the orb_tables repository on GitHub. Notice how the process reports a missing order: it writes an error property instead of raising an exception. That convention is what lets apex.server turn the response into a proper failure, as the error examples show. For the basics of creating a callback, see the guide to Ajax callbacks in Oracle APEX.
How to Run These Examples
The examples ran in Oracle APEX 26.1 on pages of a test application. The output under each example is exactly what the browser console printed. To try them, create an Ajax Callback process like the one above, open a page, press F12, and paste the code into the Console tab. Most examples use await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.
Calling the Server with apex.server
apex.server.process
Calls an Ajax Callback process and returns a promise that resolves with the response, parsed as JSON by default. It wraps jQuery.ajax and adds the APEX session, page items, and error handling.
apex.server.process(pName, [pData], [pOptions]) → Promise
| Parameter | Type | Description |
|---|---|---|
| pName | string | The name of the process. |
| pData | object | The data to send. x01 to x20 arrive as the scalars apex_application.g_x01 to g_x20. f01 to f20, strings or arrays, arrive as the arrays g_f01 to g_f20. pageItems, a selector, jQuery object, or array of item names, puts those items into session state first. Anything else goes into the JSON parameter p_json. |
| pOptions | object | Any jQuery.ajax option, such as dataType (default "json"), success, error, complete, beforeSend, and headers, plus the APEX options below. |
The APEX-specific options, which apex.server.plugin accepts as well:
| Option | Type | Description |
|---|---|---|
| loadingIndicator | selector, jQuery, or function | Where to show a spinner during the call, or a function that places one and returns a function that removes it. |
| loadingIndicatorPosition | string | before, after, prepend, append, centered, or page. |
| queue | object | {name, action}: requests with the same name run one after another. action is wait (the default), replace, or lazyWrite. |
| refreshObject | selector or jQuery | The element on which apexbeforerefresh and apexafterrefresh fire around the call. |
| refreshObjectData | object | Extra data passed to those events. |
| clear | function | Called after apexbeforerefresh and before the request is sent. |
| target | selector or jQuery | The region that column items in pageItems belong to. |
// ORDER_SUMMARY is an Ajax Callback application process of the API Lab (see the text).
apex.item("P20_TEXT").setValue("sent with the request");
const summary = await apex.server.process("ORDER_SUMMARY",
{ x01: "2282", pageItems: "#P20_TEXT" },
{ loadingIndicator: "#text_items", loadingIndicatorPosition: "append" });
console.log(summary);Output:
{
"orderNumber": "ORD-12283",
"customer": "Wildflower Travel Co.",
"status": "SHIPPED",
"orderDate": "2026-09-23",
"total": 463.21,
"lines": 2,
"P20_TEXT": "sent with the request"
}The last property proves that pageItems works: the process read P20_TEXT from session state with v('P20_TEXT') and got the value the browser had just set. Without pageItems, the server would still have the value from the last page load or submit. This is the most common reason an Ajax callback "does not see" a changed item.
Handling Errors
A request fails when the server returns an HTTP error, when the response is not valid JSON, or when the JSON has an error property, the convention a process uses to report a problem. The promise is then rejected with the jqXHR object, and the error callback receives the jqXHR, a text status (which is "APEX" for an error property), and the message:
try {
await apex.server.process("ORDER_SUMMARY", { x01: "999999" });
} catch (jqXHR) { // the promise is rejected with the jqXHR
console.log("rejected:", jqXHR.responseJSON);
}
apex.server.process("ORDER_SUMMARY", { x01: "999999" }, {
success: (data) => console.log("success:", data),
error: (jqXHR, textStatus, errorThrown) => console.log("error callback:", textStatus, "-", errorThrown)
});
await new Promise((resolve) => setTimeout(resolve, 1000));Output:
rejected: {"error":"Order 999999 does not exist."}
error callback: APEX - Order 999999 does not exist.Two things to know in 26.1. A process that raises an unhandled exception, or does not compile, returns an empty response, which fails with the status parsererror, and the real reason appears only in the debug log. So catch exceptions in the process and write an error property yourself. And for some failures, such as an expired session, APEX shows its own error message regardless of your handler. To show such errors to the user properly, see the guide to showing messages with apex.message.
Queuing Requests
The queue option decides what happens when several requests with the same queue name overlap:
// x02 makes the server wait that many seconds before it answers.
const call = (id, action) => apex.server.process("ORDER_SUMMARY", { x01: id, x02: "1" },
{ queue: { name: "summary", action } })
.then((data) => console.log(action, "answered:", data.orderNumber))
.catch((e) => console.log(action, id, "->", e.statusText ?? e));
const start = Date.now();
await Promise.all([call("2282", "wait"), call("2280", "wait")]);
console.log("two waits:", Math.round((Date.now() - start) / 1000), "s (one after the other)");
await Promise.all([call("2282", "replace"), call("2280", "replace"), call("2279", "replace")]);Output:
wait answered: ORD-12283 wait answered: ORD-12280 two waits: 2 s (one after the other) replace 2282 -> abort replace 2280 -> abort replace answered: ORD-12279
With wait, a queue's requests run one at a time in order, so two one-second calls took two seconds. With replace, a new request aborts the one in progress and replaces any still waiting, so only the last answers; that is the right choice for a search as the user types. lazyWrite waits for a pause before sending and then sends only the last request, which suits saving preferences such as a column width.
apex.server.plugin
Calls the PL/SQL Ajax function of a plug-in, identified by the Ajax identifier that APEX_PLUGIN.GET_AJAX_IDENTIFIER returns in the plug-in's render function. The parameters, options, and result are the same as for process. Instead of an identifier, pData can hold a regions array whose entries each have an ajaxIdentifier.
apex.server.plugin([pAjaxIdentifier], [pData], [pOptions]) → Promise
APEX's own regions use it too. This example temporarily wraps the function to watch a faceted search region call its Ajax function when counts are refetched:
// Native regions call their PL/SQL Ajax function with apex.server.plugin; watch the facets region do it.
const original = apex.server.plugin;
apex.server.plugin = function (ajaxIdentifier, data, options) {
console.log("plugin:", typeof ajaxIdentifier === "string" ? ajaxIdentifier.slice(0, 12) + "…" : "(in data.regions)",
"- data:", Object.keys(data || {}).join(", "));
return original.apply(this, arguments);
};
const done = apex.region("product_search").fetchCounts();
await new Promise((resolve) => setTimeout(resolve, 1500));
apex.server.plugin = original;Output:
plugin: UkVHSU9OIFRZ… - data: pageItems, x01, x02, x03
Wrapping a function like this is a handy debugging trick, but restore the original afterwards, as the example does. Writing a plug-in with its own Ajax function is covered in building your first Oracle APEX plug-in, and the faceted search methods in the guide to setting, clearing, and resetting facets with JavaScript.
apex.server.url and pluginUrl
Return the URL of a GET request. url points at the current page or another page, for an Ajax Callback, a file download, or a report. pluginUrl points at a plug-in's Ajax function, for use as an image source or a link. pData takes the same properties as for process, and pageItems are added to the URL.
apex.server.url([pData], [pPage]) → string apex.server.pluginUrl(pAjaxIdentifier, [pData]) → string
const show = (url) => console.log(url.replace(/p_instance=\d+/, "p_instance=…"));
show(apex.server.url({ p_request: "APPLICATION_PROCESS=ORDER_SUMMARY", x01: "2282" }));
show(apex.server.url({ p_request: "APPLICATION_PROCESS=ORDER_SUMMARY", x01: "2282" }, "1"));
const response = await fetch(apex.server.url({ p_request: "APPLICATION_PROCESS=ORDER_SUMMARY", x01: "2282" }));
console.log("GET:", (await response.json()).orderNumber);Output:
wwv_flow.show?p_flow_id=200&p_flow_step_id=4&p_instance=…&p_debug=&p_request=APPLICATION_PROCESS%3DORDER_SUMMARY&x01=2282&p_context=api-lab/products/19197201519135 wwv_flow.show?p_flow_id=200&p_flow_step_id=1&p_instance=…&p_debug=&p_request=APPLICATION_PROCESS%3DORDER_SUMMARY&x01=2282&p_context=api-lab/products/19197201519135 GET: ORD-12283
const ajaxIdentifier = apex.region("categories").call("option", "ajaxIdentifier") ??
Object.values(apex.region("categories").widget().data()).find((w) => w.options?.ajaxIdentifier)?.options.ajaxIdentifier;
console.log("ajaxIdentifier:", ajaxIdentifier ? ajaxIdentifier.slice(0, 16) + "…" : ajaxIdentifier);
const url = apex.server.pluginUrl(ajaxIdentifier, { x01: "LAZY" });
console.log(url.replace(/p_instance=\d+/, "p_instance=…").replace(/p_request=PLUGIN%3D[^&]+/, "p_request=PLUGIN%3D…"));Output:
ajaxIdentifier: UkVHSU9OIFRZUEV-… wwv_flow.show?p_flow_id=200&p_flow_step_id=17&p_instance=…&p_debug=&x01=LAZY&p_request=PLUGIN%3D…&p_context=api-lab/product-categories/10745609329537
A GET URL is what you need when the browser itself makes the request, such as an img src for an image stored in a BLOB, or a download link. Because the values travel in the URL, never put sensitive data in them.
apex.server.loadScript
Loads a JavaScript file, using RequireJS when the page uses it and jQuery.getScript otherwise, and calls the callback once the file has loaded. Prefer it to getScript, which breaks libraries that expect RequireJS.
apex.server.loadScript(pOptions, [callback])
| Option | Type | Description |
|---|---|---|
| path | string | The file to load. |
| requirejs | boolean | Load the file with RequireJS. |
| global | string | The global name the file defines, for RequireJS. |
console.log("before:", typeof apex.jQuery.fn.iconList);
apex.server.loadScript({ path: apex.env.APEX_FILES + "libraries/apex/minified/widget.iconList.min.js" },
() => console.log("after: ", typeof apex.jQuery.fn.iconList));
await new Promise((resolve) => setTimeout(resolve, 1500));Output:
before: undefined after: function
Loading a library on demand keeps pages that rarely need it fast. For files every page needs, add them under the application's or page's JavaScript File URLs instead.
apex.server.chunk
Splits text into pieces of at most 8,000 characters, the size limit of a VARCHAR2 parameter, or returns it unchanged when it is shorter. Send the array as f01 and join apex_application.g_f01 back together on the server.
apex.server.chunk(pText) → string | string[]
const text = "x".repeat(20000);
const chunks = apex.server.chunk(text);
console.log(Array.isArray(chunks), chunks.map((c) => c.length));
console.log(apex.server.chunk("short text"));
// send long text in f01, which the process reads as apex_application.g_f01(1..n)
console.log(Object.keys({ f01: chunks }));Output:
true [8000, 8000, 4000] short text ["f01"]
Note that short text comes back as a plain string, not an array of one. The server code should handle both, which it does naturally if it loops over g_f01, since APEX puts a single f01 value in the first element.
Dynamic Action Plug-ins with apex.da
A dynamic action plug-in runs as a JavaScript function with this set to the action. When an action calls the server and has the Wait for Result attribute, the next action only runs once the plug-in calls resume. These functions are how plug-in code tells the dynamic action what happened.
apex.da.resume
Continues with the dynamic action's next action. Pass this.resumeCallback and whether an error occurred; with an error and Stop Execution on Error switched on, the remaining actions are skipped.
apex.da.resume(pCallback, pErrorOccurred)
// In a dynamic action plug-in, this.resumeCallback continues with the next action.
const resumeCallback = (errorOccurred) => console.log("resume, errorOccurred:", errorOccurred);
apex.server.process("ORDER_SUMMARY", { x01: "2282" }).then((data) => {
console.log("got", data.orderNumber);
apex.da.resume(resumeCallback, false);
});
await new Promise((resolve) => setTimeout(resolve, 1500));Output:
got ORD-12283 resume, errorOccurred: false
Outside a plug-in there is no this.resumeCallback, so the example passes a function of its own that logs what the dynamic action would receive.
apex.da.handleAjaxErrors
Reports an Ajax error the APEX way, with an alert showing the message, and resumes the dynamic action with an error once the user closes the alert. Pass it as the error callback of an apex.server call, along with the resume callback.
apex.da.handleAjaxErrors(pjqXHR, pTextStatus, pErrorThrown, pResumeCallback)
const resumeCallback = (errorOccurred) => console.log("resume, errorOccurred:", errorOccurred);
apex.server.process("ORDER_SUMMARY", { x01: "999999" }, {
error: (jqXHR, textStatus, errorThrown) =>
apex.da.handleAjaxErrors(jqXHR, textStatus, errorThrown, resumeCallback)
});
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log("alert:", apex.jQuery(".ui-dialog:visible .ui-dialog-content").text().replace(/\s+/g, " ").trim());
setTimeout(() => apex.jQuery(".ui-dialog:visible .ui-dialog-buttonpane button").trigger("click"), 100);Output:
alert: Confirmation Order 999999 does not exist. resume, errorOccurred: true
apex.da.cancel
Stops the dynamic action's remaining actions without reporting an error; returning false from an action stops them too, but as an error. Outside a dynamic action, it sets the flag that apex.event.trigger returns, which the next example uses.
apex.da.cancel()
Triggering Events with apex.event.trigger
Triggers an event on an element, like jQuery's trigger, and returns true when a handler cancelled it by calling apex.da.cancel. This is how dynamic actions on a custom event can stop the code that triggered it. Calling preventDefault in a handler does not change the result.
apex.event.trigger(pSelector, pEvent, [pData]) → boolean
const orders = apex.jQuery("#recent_orders");
orders.on("orbitordershipped", (event, data) => console.log("handler got:", data));
console.log("returned:", apex.event.trigger(orders, "orbitordershipped", { orderId: 2282 }));
// preventDefault does not change the result...
orders.on("orbitbeforecancel", (event) => event.preventDefault());
console.log("returned:", apex.event.trigger(orders, "orbitbeforecancel"));
// ...apex.da.cancel, called by a handler, does
orders.on("orbitbeforedelete", () => apex.da.cancel());
console.log("returned:", apex.event.trigger(orders, "orbitbeforedelete"));Output:
handler got: {"orderId":2282}
returned: false
returned: false
returned: trueThis makes custom events a clean extension point. Your code triggers something like orbitbeforedelete, a developer adds a dynamic action on that Custom Event with a condition, and if the action cancels, your code sees true and skips the delete.
Logging with apex.debug
apex.debug writes to the browser console at a level, and only when the page's debug level is at least that high. For a normal request the level is 0, which is off; when the page runs in debug mode, it is the debug mode's level. apex.debug.LOG_LEVEL holds the constants OFF (0), ERROR (1), WARN (2), INFO (4), APP_TRACE (6), and ENGINE_TRACE (9).
apex.debug.getLevel and setLevel
Read and change the page's debug level. Changing it is rarely needed, since running the page in debug mode sets it.
apex.debug.getLevel() → number apex.debug.setLevel(pLevel)
const { LOG_LEVEL } = apex.debug;
console.log("level:", apex.debug.getLevel(), LOG_LEVEL);
apex.debug.info("not written: the level is", apex.debug.getLevel());
apex.debug.setLevel(LOG_LEVEL.INFO);
apex.debug.info("written at level", apex.debug.getLevel());
apex.debug.trace("not written: APP_TRACE is 6");
apex.debug.setLevel(LOG_LEVEL.OFF);
apex.debug.error("error() always writes, even when logging is off");Output:
level: 0 {"OFF":0, "ERROR":1, "WARN":2, "INFO":4, "APP_TRACE":6, "ENGINE_TRACE":9}
written at level 4
Error: error() always writes, even when logging is offapex.debug.message, log, error, warn, info, and trace
message writes at a level you give it. The others are shortcuts: error always writes, with a stack trace; warn writes at WARN, info at INFO, and trace at APP_TRACE; and log writes at any level except off. Arguments are written the way console.log writes them.
apex.debug.message(pLevel, ...args) apex.debug.log(...args) apex.debug.error(...args) apex.debug.warn(...args) apex.debug.info(...args) apex.debug.trace(...args)
apex.debug.setLevel(apex.debug.LOG_LEVEL.APP_TRACE);
apex.debug.message(apex.debug.LOG_LEVEL.INFO, "message at INFO:", { orderId: 2282 });
apex.debug.log("log: the highest level");
apex.debug.warn("warn: order total is negative");
apex.debug.info("info: 6 orders loaded");
apex.debug.trace("trace: entering refreshOrders");
apex.debug.message(apex.debug.LOG_LEVEL.ENGINE_TRACE, "not written: ENGINE_TRACE is 9");Output:
message at INFO: {"orderId":2282}
log: the highest level
Warning: warn: order total is negative
info: 6 orders loaded
trace: entering refreshOrdersLeave debug calls in your code rather than deleting them before release. They cost nothing when debugging is off, and they show what is happening the moment someone runs the page in debug mode. For the server side of debugging, see debugging, source control, and going to production in Oracle APEX.
Recipes
Look Up a Record as the User Types
The user types an order number, and as soon as they pause, the page shows the order, or an inline error when there is no such order. Three APIs work together: apex.util.debounce waits for a 400-millisecond pause, the queue action replace makes sure only the answer to the latest request arrives, and the error property in the callback's JSON becomes an inline error.
Put the code in the page's Execute when Page Loads attribute, and use the ORDER_SUMMARY callback from the start of this guide. The part marked Try it simulates a fast typist.
// === Page › Execute when Page Loads ===
// Looks up the order whose ID the user types into P20_TEXT, 400 ms after the last key.
const lookUpOrder = apex.util.debounce(() => {
const orderId = apex.item("P20_TEXT").getValue().trim();
apex.message.clearErrors();
if (!orderId) return;
apex.server.process("ORDER_SUMMARY", { x01: orderId },
{ queue: { name: "order-lookup", action: "replace" } }) // only the last one
.then((order) => {
const total = apex.locale.formatNumber(order.total, "FML999G999G990D00");
apex.item("P20_DISPLAY_ONLY")
.setValue(`${order.orderNumber} · ${order.customer} · ${total}`);
console.log("found:", apex.item("P20_DISPLAY_ONLY").getValue());
})
.catch((jqXHR) => {
if (jqXHR.statusText === "abort") return; // replaced by a newer request
apex.item("P20_DISPLAY_ONLY").setValue("");
apex.message.showErrors({ type: "error", location: "inline", pageItem: "P20_TEXT",
message: jqXHR.responseJSON?.error || "The lookup failed.", unsafe: false });
console.log("error:", $("#P20_TEXT_error").text());
});
}, 400);
$("#P20_TEXT").on("input", lookUpOrder);
// === Try it: type 2277 one key at a time, then 99999 ===
const type = async (text) => {
for (let i = 1; i <= text.length; i++) {
$("#P20_TEXT").val(text.slice(0, i)).trigger("input");
await new Promise((resolve) => setTimeout(resolve, 120)); // a fast typist
}
await new Promise((resolve) => setTimeout(resolve, 1500));
};
await type("2277");
await type("99999");Output:
found: ORD-12277 · Prairie Travel Co. · $11,495.93 error: Order 99999 does not exist.
Four keystrokes became one request, and the error text came straight from the server's JSON as jqXHR.responseJSON.error. A request aborted by a newer one also rejects its promise, with the status text "abort", which the handler deliberately ignores. For a related technique, see validating without a submit using JavaScript and an Ajax callback.
Run Several Calls at Once and Wait for All
To total the selected orders, the page asks the server about each one. The requests run in parallel, because the browser sends them together, and Promise.allSettled waits for every one of them, including those that fail, so one missing order does not lose the others.
Put the function in the page's Function and Global Variable Declaration attribute, and call it wherever the IDs are known.
// === Page › Function and Global Variable Declaration ===
// Gets the summaries of several orders at the same time and waits for all of them.
async function orderSummaries(orderIds) {
const answers = await Promise.allSettled(
orderIds.map((id) => apex.server.process("ORDER_SUMMARY", { x01: id })));
return answers.map((answer, i) => answer.status === "fulfilled"
? answer.value
: { orderId: orderIds[i], error: answer.reason.responseJSON?.error });
}
// === Try it: the orders selected in Recent Orders, and one that does not exist ===
apex.region("recent_orders").setSelectedValues(["2277", "2276"]);
const ids = [...apex.region("recent_orders").getSelectedValues(), "99999"];
const summaries = await orderSummaries(ids);
summaries.forEach((s) =>
console.log(s.error ? `${s.orderId}: ${s.error}` : `${s.orderNumber}: ${s.total}`));
const total = summaries.filter((s) => !s.error).reduce((sum, s) => sum + s.total, 0);
console.log("total:", apex.locale.formatNumber(total, "FML999G999G990D00"));Output:
ORD-12277: 11495.93 ORD-12276: 9051.16 99999: Order 99999 does not exist. total: $20,547.09
With Promise.all instead, the first failure would reject the whole result. And for many IDs, one call that takes them all as an f01 array beats one call per ID, since every request costs a round trip and a database session. The selection methods used here are covered in the guide to selecting rows and paging through reports and cards.
Conclusion
apex.server.process is the workhorse for calling PL/SQL from the browser. It sends x01 to x20, f01 to f20, page items, and JSON data, returns a promise, rejects it when the JSON carries an error property, and orders or replaces overlapping requests through queues. Remember pageItems whenever the process reads an item the user just changed, and catch exceptions in the process so failures come back as readable errors instead of parsererror. apex.server.plugin does the same for a plug-in's Ajax function, url and pluginUrl build GET URLs, loadScript loads libraries on demand, and chunk splits text longer than 8,000 characters. apex.da lets dynamic action plug-ins resume, report errors, or cancel, apex.event.trigger reports that cancellation to the code that raised an event, and apex.debug logs at the page's debug level at no cost when debugging is off.
