Ajax in Oracle APEX: Callbacks and apex.server.process

Learn how Ajax works in Oracle APEX, from callback processes and apex.server.process to returning JSON and following background jobs.

Several dynamic actions already talk to the server without reloading the page. Set Value runs a query, Execute Server-side Code calls a procedure, Refresh re-renders a region. Each one sends an Ajax request and updates part of the page with the answer.

Those declarative actions cover most of what an application needs. When they do not, Oracle APEX lets you write the server half as an Ajax Callback process and call it from JavaScript. This guide shows what an APEX Ajax request actually looks like, how to write a callback, how to get values in and JSON out, and how to follow long-running background work.

What an APEX Ajax Request Looks Like

Every Ajax request from an APEX page goes to the same endpoint and carries the application, page, and session, plus a request value saying what to run. Open the Network tab in your browser's developer tools and change an item that has a Set Value action, and you will see it.

POST /ords/wwv_flow.ajax?p_context=orbit-sales/order/6714593808632
p_flow_id=100&p_flow_step_id=10&p_instance=6714593808632
&p_request=PLUGIN=REEgVFlQRX5-Mzc2MDgxMzIy
&p_json={"pageItems":{"itemsToSubmit":[{"n":"P10_CUSTOMER_ID","v":"16"}]}}

The request names the dynamic action's plug-in, and pageItems carries the Items to Submit, which APEX writes into session state before running anything. The response is small JSON that the action feeds into the target item.

Your own callbacks look the same, with the request naming your process instead. Two consequences follow from all of this running inside the user's session: Ajax requests read and write session state exactly like a page submit, and a request without a valid session is rejected outright.

Writing an Ajax Callback

Suppose an order form should show a live summary of the chosen customer: credit limit, lifetime value, open orders. No item on the page holds that, and the browser cannot compute it.

The Display Item

Switching off Send On Page Submit for a display-only item in Oracle APEX
An item written by JavaScript should not be submitted back.

Create a display-only item with a null source, then switch off Send On Page Submit. This is the detail that saves an hour of confusion later.

APEX protects display-only and hidden items by remembering the value it rendered and rejecting a submit where that value changed. An item filled in by JavaScript will have changed, so leaving Send On Page Submit on means every save after a customer change fails with a session state protection violation. Switched off, the item is simply never sent, which is fine because it exists only to be read.

The related case is different: for a hidden item that JavaScript changes and the server genuinely needs, switch off Value Protected instead, and validate the value in your code.

The Callback Process

An Ajax Callback process in Oracle APEX
An Ajax Callback process runs only when JavaScript asks.
declare
    l_customer_id orb_customers.customer_id%type := apex_application.g_x01;
begin
    apex_json.open_object;
    for c in (
        select c.credit_limit,
               orb_sales.customer_lifetime_value(c.customer_id) as lifetime_value,
               (select count(*)
                  from orb_orders o
                 where o.customer_id = c.customer_id
                   and o.status in ('NEW', 'PENDING_APPROVAL', 'APPROVED')) as open_orders
          from orb_customers c
         where c.customer_id = l_customer_id
    ) loop
        apex_json.write('creditLimit',   c.credit_limit);
        apex_json.write('lifetimeValue', c.lifetime_value);
        apex_json.write('openOrders',    c.open_orders);
    end loop;
    apex_json.close_object;
end;
Ajax callbacks listed in the Processing tree in Oracle APEX
Callbacks sit in their own node, never running on render or submit.

The process name is what JavaScript will call, so name it like an API rather than a page component. Whatever the process writes to the HTTP output becomes the response, and APEX_JSON writes there by default.

The JavaScript

A dynamic action running JavaScript that calls an Ajax callback
A dynamic action, firing on change and on page load.
const money = new Intl.NumberFormat('en-US',
    { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });

apex.server.process('GET_CUSTOMER_INFO', {
    x01: apex.item('P10_CUSTOMER_ID').getValue()
}).then((data) => {
    apex.item('P10_CUSTOMER_INFO').setValue(
        'Credit limit ' + money.format(data.creditLimit) +
        ' · lifetime value ' + money.format(data.lifetimeValue) +
        ' · ' + data.openOrders +
        (data.openOrders === 1 ? ' open order' : ' open orders'));
}).catch((error) => {
    apex.message.showErrors([{
        type: 'error', location: 'page',
        message: 'Customer details could not be loaded.'
    }]);
});

apex.server.process returns a promise. The parsed JSON arrives in then, and catch handles both network failures and exceptions raised inside the callback. Do write that catch: a silent failure here looks like an item that mysteriously stays empty.

A customer summary loaded by Ajax on an Oracle APEX form
The summary, fetched without a page submit.
An Ajax-loaded summary updating after a customer change
Change the customer and the summary follows.

Leaving Fire on Initialization on here is deliberate, because the summary should also appear when an existing order opens. That is the opposite of the rule for actions that change data, and the distinction is worth internalizing: read-only actions usually want initialization, data-changing ones usually do not.

Sending Values to the Server

PropertySendsRead in PL/SQL as
x01 to x20Up to twenty single valuesapex_application.g_x01 and friends
f01 to f20Up to twenty arraysapex_application.g_f01 collections
pageItemsPage items, by selector or name arrayBind variables, because they reach session state first
p_clob_01A large text valueapex_application.g_clob_01

The choice between x01 and pageItems is a real design decision rather than a style preference. Sending a page item puts the value into session state, which is what you want when a report on the same page also reads it. Sending x01 leaves session state untouched, which is what you want when the value is a passing argument and nothing else should see it.

The third argument holds options, including a data type for responses that are not JSON, and a loading indicator that puts a spinner beside an element while the request runs. For anything that might take a moment, showing a spinner is the difference between "slow" and "broken".

Returning JSON

APEX_JSON writes JSON piece by piece, and it can also serialize an entire cursor in one call, which is the shortest path from a query to an array of objects.

declare
    l_cursor sys_refcursor;
begin
    open l_cursor for
        select order_number, order_date, order_total
          from orb_orders
         where customer_id = apex_application.g_x01
         order by order_date desc
         fetch first 5 rows only;
    apex_json.write(l_cursor);
end;

The database's own SQL JSON functions are an equally good alternative, building the document in the query and printing it with htp.p.

Whichever you choose, the callback must write nothing but the JSON. One stray debug line printed to the output makes the response unparseable and the promise rejects, which produces an error message that points nowhere near the actual mistake. If you need to trace something, write to a logging table.

Following Long-Running Work

A background execution chain hands the page straight back to the user, which is the point, but leaves them with no idea when the work finishes. A callback that reads the execution's status, polled from the browser, closes that gap.

A dynamic action polling for background job progress in Oracle APEX
A Page Load action, conditional on there being a job to follow.
declare
    l_done boolean := true;
begin
    apex_json.open_object;
    for s in (
        select status, status_code, sofar, totalwork
          from apex_appl_page_bg_proc_status
         where execution_id = :P8_RECALC_ID
    ) loop
        l_done := s.status_code in ('SUCCESS', 'FAILED', 'ABORTED');
        apex_json.write('status',    s.status);
        apex_json.write('sofar',     s.sofar);
        apex_json.write('totalwork', s.totalwork);
    end loop;
    apex_json.write('done', l_done);
    apex_json.close_object;

    if l_done then
        apex_util.set_session_state('P8_RECALC_ID', null);
    end if;
end;

The chain's Return ID into Item setting puts the execution's ID into a hidden item when the job is queued, which is how the page knows what to watch. Clearing that item once the job finishes matters just as much, otherwise the next visit to the page starts polling a job that ended yesterday.

function checkProgress() {
    apex.server.process('GET_RECALC_PROGRESS').then((data) => {
        if (data.done) {
            apex.message.showPageSuccess('Recalculation ' + data.status.toLowerCase() +
                ': ' + data.sofar.toLocaleString() + ' orders.');
        } else {
            apex.message.showPageSuccess(data.totalwork
                ? 'Recalculating order totals: ' + data.sofar.toLocaleString() +
                  ' of ' + data.totalwork.toLocaleString() + ' orders…'
                : 'Recalculation: ' + data.status + '…');
            setTimeout(checkProgress, 1000);
        }
    });
}
checkProgress();
A page showing that background work has been queued in Oracle APEX
The job is queued and the page is already usable.
A page showing completed background work in Oracle APEX
The same message, updated when the work finishes.

Poll politely. Every poll is a request, so match the interval to the job: a second for work measured in seconds, ten seconds for work measured in minutes, and always stop when done. And for anything that finishes in a few seconds, skip polling entirely and use Execute Server-side Code with Show Processing, which waits with a spinner and needs no callback at all.

Partial Page Refresh

Refreshing a region is the same machinery wearing different clothes. The declarative Refresh action and its JavaScript equivalent both ask the server to render the region again with current session state, plus the region's Page Items to Submit, and swap it into the page while everything else, including the scroll position, stays put.

apex.region('top_customers').refresh();

That name is the region's HTML DOM ID, not its title. Regions fire Before Refresh and After Refresh events that other dynamic actions can hook, and many offer Lazy Loading, which renders the page first and fetches the region's data afterwards, so one slow report no longer delays everything else.

Keeping Callbacks Secure

A callback is a public entry point to your server, and it deserves the same suspicion as any other.

  • Treat the input as untrusted. Values in x01 or f01 come from the browser and can be anything at all. A callback returning or changing restricted data must check the user is entitled to it, through an authorization scheme on the process or a check in the code.
  • Use bind variables. Never concatenate a value from the browser into dynamic SQL.
  • Page items keep their protection. Items sent through pageItems are subject to session state protection exactly as on a submit, so a value-protected item cannot be changed by Ajax.
  • Mind the scope. A page-level callback can only be called from its page, while an application process with the Ajax Callback point is reachable from every page, which is convenient and correspondingly wider exposure.

Conclusion

Ajax in APEX is less mysterious than it looks: every request goes to one endpoint inside the user's session, carrying a request value that names what to run, and reads and writes session state exactly as a page submit does. Write the server half as an Ajax Callback process, read what the browser sent from x01 through x20 or from session state, and return JSON built with APEX_JSON or the database's own JSON functions, taking care that the response contains nothing else. Call it from JavaScript with apex.server.process, which hands back a promise, and always attach a catch so a failure surfaces instead of silently doing nothing. Choose pageItems when the value belongs in session state and x01 when it is just an argument, switch off Send On Page Submit for display items that JavaScript writes, and poll background work no faster than the work actually changes. Most of the time a declarative dynamic action is still the right answer, but when it is not, this is the seam APEX leaves open for you.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE and software veteran with 25+ years of experience, passionate about AI and IT innovation.

guest

0 Comments
Oldest
Newest Most Voted
00