How to Show Warnings That Still Allow Saving in Oracle APEX

Check a soft rule on the server, ask the user with a declarative Confirm dialog, record the answer in a hidden item and keep hard errors as real validations.

Not every rule in a form is a hard rule. Some values are unusual but allowed, like an order that goes above a credit limit or a date far in the future. A validation error blocks the save, so users can't continue even when they have a good reason.

A warning that still allows saving solves this. When the user clicks Save, the page checks the rule on the server. If it fails, a confirm dialog explains the problem and offers Save Anyway or Go Back. Hard errors stay real errors and still block the save.

This guide builds that pattern with a dynamic action, the declarative Confirm action and one server-side validation that also stops a scripted submit. It uses orders and a customer credit limit as the example; the same steps work for any table and any warning rule.

How to Show Success, Error, and Confirm Messages Using apex.message in Oracle APEX explains the JavaScript message API. How to Warn About Possible Duplicate Records While Typing in Oracle APEX shows a region while the user types. Here the warning comes at save time and the server checks that the user confirmed it.

Step 1: Create the Tables

The example needs two tables. TRADE_ACCOUNTS stores each account with a credit limit and a status. TRADE_ORDERS stores the orders. The open balance of an account is the sum of its orders with the status OPEN.

Create the tables, the index and the sample data:

Example:

create table trade_accounts (
  account_id      number generated by default on null as identity,
  account_name    varchar2(100 char) not null,
  credit_limit    number(12,2) not null,
  account_status  varchar2(10 char) default on null 'ACTIVE' not null,
  constraint trade_accounts_pk primary key (account_id),
  constraint trade_accounts_name_uk unique (account_name),
  constraint trade_accounts_limit_ck check (credit_limit >= 0),
  constraint trade_accounts_status_ck check (account_status in ('ACTIVE', 'ON_HOLD'))
);

create table trade_orders (
  order_id      number generated by default on null as identity,
  account_id    number not null,
  order_date    date default on null trunc(sysdate) not null,
  order_total   number(12,2) not null,
  order_status  varchar2(10 char) default on null 'OPEN' not null,
  notes         varchar2(400 char),
  constraint trade_orders_pk primary key (order_id),
  constraint trade_orders_account_fk foreign key (account_id)
    references trade_accounts (account_id),
  constraint trade_orders_total_ck check (order_total > 0),
  constraint trade_orders_status_ck check (order_status in ('OPEN', 'PAID'))
);

create index trade_orders_account_ix on trade_orders (account_id, order_status);

insert into trade_accounts (account_id, account_name, credit_limit, account_status) values (1, 'Northwind Traders', 50000, 'ACTIVE');
insert into trade_accounts (account_id, account_name, credit_limit, account_status) values (2, 'Blue Harbor Supplies', 20000, 'ACTIVE');
insert into trade_accounts (account_id, account_name, credit_limit, account_status) values (3, 'Summit Retail Group', 30000, 'ON_HOLD');

insert into trade_orders (order_id, account_id, order_date, order_total, order_status) values (1, 1, date '2026-10-01', 18000, 'OPEN');
insert into trade_orders (order_id, account_id, order_date, order_total, order_status) values (2, 1, date '2026-10-05', 12500, 'OPEN');
insert into trade_orders (order_id, account_id, order_date, order_total, order_status) values (3, 1, date '2026-09-12', 22000, 'PAID');
insert into trade_orders (order_id, account_id, order_date, order_total, order_status) values (4, 2, date '2026-10-08', 16000, 'OPEN');
insert into trade_orders (order_id, account_id, order_date, order_total, order_status) values (5, 3, date '2026-10-02', 9000, 'OPEN');

commit;

alter table trade_accounts modify account_id generated by default on null as identity (start with limit value);
alter table trade_orders modify order_id generated by default on null as identity (start with limit value);

column account_name format a21
column account_status format a14
select a.account_id,
       a.account_name,
       a.credit_limit,
       a.account_status,
       nvl(sum(case when o.order_status = 'OPEN' then o.order_total end), 0) as open_balance
from   trade_accounts a
left join trade_orders o on o.account_id = a.account_id
group by a.account_id, a.account_name, a.credit_limit, a.account_status
order by a.account_id;

Output:

Table TRADE_ACCOUNTS created.

Table TRADE_ORDERS created.

Index TRADE_ORDERS_ACCOUNT_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

Table TRADE_ACCOUNTS altered.

Table TRADE_ORDERS altered.

   ACCOUNT_ID ACCOUNT_NAME               CREDIT_LIMIT ACCOUNT_STATUS       OPEN_BALANCE
_____________ _______________________ _______________ _________________ _______________
            1 Northwind Traders                 50000 ACTIVE                      30500
            2 Blue Harbor Supplies              20000 ACTIVE                      16000
            3 Summit Retail Group               30000 ON_HOLD                      9000
  • TRADE_ORDERS_ACCOUNT_FK links each order to an account, and TRADE_ORDERS_ACCOUNT_IX indexes it together with the status used by the balance query.
  • TRADE_ORDERS_TOTAL_CK rejects a total of zero or less. That is a hard rule, so the table enforces it.
  • DEFAULT ON NULL fills ORDER_DATE, ORDER_STATUS and ACCOUNT_STATUS when a program inserts a row without them.
  • Summit Retail Group is ON_HOLD. The form will block its orders with a real error.

Northwind Traders has a credit limit of 50,000 and an open balance of 30,500. A new open order of more than 19,500 will take it over the limit.

Step 2: Write the Warning Rule Once

The warning is checked twice: once before the save to show the dialog, and once during the save to make sure the user confirmed it. Put the rule in one function, so both checks always agree.

The function returns the warning text, or NULL when the order is fine. It leaves out the order being edited from the open balance, and it ignores orders with the status PAID:

Example:

create or replace function credit_limit_warning (
  p_account_id  in number,
  p_order_id    in number,
  p_order_total in number,
  p_status      in varchar2 default 'OPEN'
) return varchar2
is
  l_limit   trade_accounts.credit_limit%type;
  l_open    number;
  l_new     number;
begin
  if p_account_id is null or p_order_total is null or p_status = 'PAID' then
    return null;
  end if;

  select credit_limit into l_limit
  from   trade_accounts
  where  account_id = p_account_id;

  select nvl(sum(order_total), 0) into l_open
  from   trade_orders
  where  account_id = p_account_id
  and    order_status = 'OPEN'
  and    order_id <> nvl(p_order_id, -1);

  l_new := l_open + p_order_total;

  if l_new > l_limit then
    return 'This order brings the open balance to '
        || to_char(l_new, 'FM999G999G990D00')
        || ', which is above the credit limit of '
        || to_char(l_limit, 'FM999G999G990D00') || '.';
  end if;
  return null;
exception
  when no_data_found then
    return null;
end credit_limit_warning;
/

column warning format a100
select credit_limit_warning(1, null, 15000) as warning from dual;
select credit_limit_warning(1, null, 25000) as warning from dual;
select credit_limit_warning(1, 2, 30000) as warning from dual;
select credit_limit_warning(1, null, 25000, 'PAID') as warning from dual;

Output:

Function CREDIT_LIMIT_WARNING compiled

WARNING
__________

WARNING
_________________________________________________________________________________________________
This order brings the open balance to 55,500.00, which is above the credit limit of 50,000.00.

WARNING
__________

WARNING
__________

The four test calls show the cases. An extra 15,000 stays under the limit. An extra 25,000 goes over it. Editing order 2 to 30,000 is fine, because its old amount is not counted twice. A PAID order never gives a warning.

Step 3: Create the Report and Form Pages

In App Builder, open the application and click Create Page. On the Component tab, click Interactive Report:

Create a Page dialog with the Interactive Report page type marked with a red box
The Interactive Report page type.

Enter Page Number 560 and Name Trade Orders. Turn on Include Form Page, enter Form Page Number 561 and Form Page Name Trade Order, and keep Form Page Mode Modal Dialog:

Create Interactive Report wizard with page 560 Trade Orders and form page 561 Trade Order marked with red boxes
One wizard creates the report page and the form page.

Under Data Source, keep Local Database and Source Type Table, and enter Table / View Name TRADE_ORDERS:

Data Source section with Table / View Name TRADE_ORDERS marked with a red box
The form is based on the TRADE_ORDERS table.

Keep Use Breadcrumb and Use Navigation on, so the report gets a menu entry, and click Next:

Navigation section of the Create Interactive Report wizard with breadcrumb and navigation menu settings
The Navigation settings of the wizard.

On the Primary Key screen, select ORDER_ID (Number) as Primary Key Column 1 and click Create Page:

Primary Key screen of the wizard with ORDER_ID (Number) selected and marked with a red box
ORDER_ID identifies each row.

APEX creates the Trade Orders report on page 560 and the Trade Order modal form on page 561. It also created a select list for ACCOUNT_ID with a shared list of values that shows the account names.

Step 4: Set Up the Form Items

Open page 561 in Page Designer and select P561_ACCOUNT_ID. Change Label to Account, turn on Display Null Value and enter Null Display Value - Select Account -:

P561_ACCOUNT_ID with Label Account, Display Null Value on and Null Display Value - Select Account - marked with red boxes
The account list starts empty for a new order.

Select P561_ORDER_STATUS. Change Type to Radio Group and Number of Columns to 2. In the List of Values group, select Type SQL Query and enter this query:

Example:

select 'Open' as d, 'OPEN' as r from dual
union all
select 'Paid', 'PAID' from dual
P561_ORDER_STATUS with Type Radio Group, Number of Columns 2 and the SQL Query list of values marked with red boxes
The status becomes two radio buttons.

In the Default group of the same item, select Type Static and enter Static Value OPEN:

Default group of P561_ORDER_STATUS with Type Static and Static Value OPEN marked with a red box
A new order starts as Open.

Select P561_ORDER_TOTAL. In the Settings group, enter Minimum Value 0.01 and Maximum Value 1000000:

P561_ORDER_TOTAL with Minimum Value 0.01 and Maximum Value 1000000 marked with a red box
APEX checks the range on the server.

In a first test, only Minimum Value was set. A total of 0 then showed the message "must be a number less than or equal to ." with no number in it. With both values set, the message was correct.

Step 5: Add Two Hidden Items

The page needs two hidden items. P561_CREDIT_WARNING holds the warning text returned by the server. P561_WARNING_ACCEPTED is set to Y when the user clicks Save Anyway.

Right-click the Trade Order region and choose Create Page Item:

Context menu of the Trade Order region with Create Page Item marked with a red box
Create the items inside the form region.

Enter Name P561_CREDIT_WARNING and select Type Hidden. Turn off Value Protected, and set Storage to Per Request (Memory Only):

Hidden item P561_CREDIT_WARNING with Value Protected off and Storage Per Request (Memory Only) marked with red boxes
The item that receives the warning text.

Create the second item the same way, with Name P561_WARNING_ACCEPTED:

Hidden item P561_WARNING_ACCEPTED with Value Protected off and Storage Per Request (Memory Only) marked with red boxes
The item that records the user's answer.

Both settings matter. JavaScript changes these values in the browser, and a protected hidden item rejects a changed value with a checksum error. Per Request storage means the value lives only for one request, so an old Y can't carry over to the next order.

Step 6: Add the Hard Error

An account on hold must never get an order. That is a hard rule, so it stays a normal validation.

Right-click P561_ACCOUNT_ID and choose Create Validation:

Context menu of P561_ACCOUNT_ID with Create Validation marked with a red box
Start the validation from the account item.

Enter Name Account Not On Hold, select Type No Rows returned and enter this SQL Query:

Example:

select 1
from   trade_accounts
where  account_id = :P561_ACCOUNT_ID
and    account_status = 'ON_HOLD'

Enter Error Message This account is on hold. Orders cannot be saved for it. Associated Item is already P561_ACCOUNT_ID:

Validation Account Not On Hold with Type No Rows returned, the SQL query, the error message and Associated Item marked with red boxes
The hard error blocks the save.

Step 7: Add the Validation That Checks the Answer

The dialog runs in the browser, and a browser can be skipped. This validation runs during the save and fails when the order is over the limit and the user has not confirmed the warning.

Right-click P561_ORDER_TOTAL and choose Create Validation. Enter Name Credit Limit Warning Accepted, keep Type Expression and Language PL/SQL, and enter this PL/SQL Expression:

Example:

credit_limit_warning(:P561_ACCOUNT_ID, :P561_ORDER_ID, :P561_ORDER_TOTAL, :P561_ORDER_STATUS) is null
or nvl(:P561_WARNING_ACCEPTED, 'N') = 'Y'

Enter Error Message The order is above the credit limit. Click the save button again and confirm the warning. Associated Item is already P561_ORDER_TOTAL:

Validation Credit Limit Warning Accepted with Type Expression, the PL/SQL expression, the error message and Associated Item marked with red boxes
The server-side check of the warning.

Don't leave out the NVL. In a first test, the expression ended with or :P561_WARNING_ACCEPTED = 'Y'. A scripted submit over the limit then saved the order.

The reason is PL/SQL logic. FALSE OR NULL gives NULL, not FALSE, and APEX did not raise the error for a NULL result. With NVL, the expression is FALSE, and the same submit was blocked.

Step 8: Let a Dynamic Action Handle the Save Buttons

The CREATE and SAVE buttons must not submit the page directly. A dynamic action will check the rule first.

Select the CREATE button. Enter CSS Classes js-credit-check, change Action to Defined by Dynamic Action and enter HTML DOM ID CREATE:

CREATE button with CSS Classes js-credit-check, Action Defined by Dynamic Action and HTML DOM ID CREATE marked with red boxes
The Create button now waits for the dynamic action.

Select the SAVE button and set the same values, with HTML DOM ID SAVE:

SAVE button with CSS Classes js-credit-check, Action Defined by Dynamic Action and HTML DOM ID SAVE marked with red boxes
The Apply Changes button gets the same settings.

The HTML DOM ID equals the button name. The dynamic action later submits the page with that ID as the request, so the form process still inserts for CREATE and updates for SAVE. Without it, APEX renders an ID like B53383862216938050.

Step 9: Create the Dynamic Action

Right-click the CREATE button and choose Create Dynamic Action:

Context menu of the CREATE button with Create Dynamic Action marked with a red box
Start the dynamic action from a save button.

Enter Name Check Credit Limit. Keep Event Click, change Selection Type to jQuery Selector and enter jQuery Selector .js-credit-check. Now one dynamic action serves both buttons:

Dynamic action Check Credit Limit with Event Click, Selection Type jQuery Selector and jQuery Selector .js-credit-check marked with red boxes
The event fires for both save buttons.

Action 1: Ask the Server

Open the Dynamic Actions tab, expand Check Credit Limit and select the Show action under True. Change Action to Execute Server-side Code and enter this PL/SQL Code:

Example:

:P561_CREDIT_WARNING := credit_limit_warning(
    :P561_ACCOUNT_ID, :P561_ORDER_ID,
    :P561_ORDER_TOTAL, :P561_ORDER_STATUS);

Enter Items to Submit P561_ACCOUNT_ID,P561_ORDER_ID,P561_ORDER_TOTAL,P561_ORDER_STATUS and Items to Return P561_CREDIT_WARNING. Keep Wait For Result on:

Execute Server-side Code action with the PL/SQL code, Items to Submit, Items to Return and Wait For Result marked with red boxes
The server fills the warning item.

Wait For Result makes the next actions wait until the warning text is back in the browser.

Action 2: The Confirm Dialog

Right-click True and choose Create TRUE Action. Set these properties:

  • Identification > Action: Confirm
  • Settings > Title: Credit Limit Warning
  • Settings > Message: &P561_CREDIT_WARNING. Do you want to save the order anyway?
  • Settings > Style: Warning
  • Settings > Confirm Label: Save Anyway
  • Settings > Cancel Label: Go Back
  • Client-side Condition > Type: Item is not null
  • Client-side Condition > Item: P561_CREDIT_WARNING
Confirm action with Title, Message, Style Warning, Confirm Label Save Anyway, Cancel Label Go Back and the client-side condition marked with red boxes
The dialog appears only when there is a warning.

The &P561_CREDIT_WARNING. reference is replaced in the browser with the text the server returned. When the user clicks Go Back, the Confirm action stops the dynamic action, and nothing after it runs.

Action 3: Submit the Page

Right-click True again, choose Create TRUE Action, and select Action Execute JavaScript Code. Enter this Code:

Example:

apex.item("P561_WARNING_ACCEPTED").setValue(apex.item("P561_CREDIT_WARNING").isEmpty() ? "" : "Y");
apex.page.submit({ request: this.triggeringElement.id, showWait: true });
Execute JavaScript Code action with the code that sets P561_WARNING_ACCEPTED and submits the page, marked with red boxes
The last action records the answer and submits.

This action runs when there was no warning, or when the user clicked Save Anyway. It sets P561_WARNING_ACCEPTED to Y only in the second case. this.triggeringElement.id is CREATE or SAVE, the button that was clicked.

Click Save in Page Designer.

Step 10: Test the Form

An Order Under the Limit

Run page 560 and click Create. Select Northwind Traders, enter Order Date 10/17/2026 and Order Total 10000, and click Create:

Trade Order dialog for Northwind Traders with Order Total 10000 and the Create button marked with a red box
This order keeps the account under its limit.

The open balance becomes 40,500, which is under 50,000. The server returned no warning, so no dialog appeared and the order was saved at once.

An Order Over the Limit

Click Create again. Select Northwind Traders, enter Order Total 15000 and click Create. The new balance would be 55,500, so the warning appears:

Credit Limit Warning dialog with the balance and limit text and the Go Back and Save Anyway buttons, marked with a red box
The user decides what happens next.

Click Go Back. The dialog closes, the form keeps the value 15000, and nothing is saved. The user can lower the amount.

Click Create again and choose Save Anyway this time. The order is saved and the report shows it:

Trade Orders report with the new 15000 order of Northwind Traders marked with a red box
The warning did not stop a confirmed save.

A Scripted Submit (Negative Test)

A user could skip the dialog by submitting the form from the browser console. To test the server-side check, a new Northwind Traders order of 5000 was submitted with this script:

Example:

apex.page.submit("CREATE");

No dialog appeared, because the dynamic action did not run. The Credit Limit Warning Accepted validation stopped the save:

Trade Order dialog with the error The order is above the credit limit below Order Total, marked with a red box
The server rejects an unconfirmed order over the limit.

The Hard Error

Create an order of 1000 for Summit Retail Group. The amount is under its limit, so no warning appears. The Account Not On Hold validation blocks the save:

Trade Order dialog for Summit Retail Group with the error This account is on hold, marked with a red box
A hard error has no Save Anyway option.

Then the total was changed to 25000, which would take Summit Retail Group to 34,000 against a limit of 30,000. The warning appeared first. After Save Anyway, the on-hold error still blocked the save. A confirmed warning never overrides a hard error.

A total of 0 was also tested. No warning appeared, and the server returned Order Total must be a number between .01 and 1000000.

Editing an Existing Order

Open the 16000 order of Blue Harbor Supplies and change Order Total to 25000. Click Apply Changes:

Credit Limit Warning for an existing Blue Harbor Supplies order showing a balance of 25,000.00 against a limit of 20,000.00, marked with a red box
The same warning protects Apply Changes.

The balance in the message is 25,000, not 41,000, because the function leaves out the old amount of this order. After Go Back, the total was changed to 19000. Apply Changes then saved it without a dialog.

Paid Orders and the Hidden Flag

A new PAID order of 30000 for Blue Harbor Supplies was saved without a warning. The rule only counts open orders.

Last, a Northwind Traders order of 2000 was submitted from the console with the flag already set:

Example:

apex.item("P561_WARNING_ACCEPTED").setValue("Y");
apex.page.submit("CREATE");

That order was saved. Setting the flag by hand gives the same result as clicking Save Anyway, which a warning allows anyway. A limit that must never be passed belongs in a hard error, like the on-hold check.

The Saved Data

The tables show what the tests saved:

Example:

column account_name format a21
select o.order_id,
       a.account_name,
       o.order_total,
       o.order_status
from   trade_orders o
join   trade_accounts a on a.account_id = o.account_id
order by o.order_id;

select a.account_name,
       a.credit_limit,
       nvl(sum(case when o.order_status = 'OPEN' then o.order_total end), 0) as open_balance
from   trade_accounts a
left join trade_orders o on o.account_id = a.account_id
group by a.account_name, a.credit_limit
order by a.account_name;

Output:

   ORDER_ID ACCOUNT_NAME               ORDER_TOTAL ORDER_STATUS
___________ _______________________ ______________ _______________
          1 Northwind Traders                18000 OPEN
          2 Northwind Traders                12500 OPEN
          3 Northwind Traders                22000 PAID
          4 Blue Harbor Supplies             19000 OPEN
          5 Summit Retail Group               9000 OPEN
         16 Northwind Traders                10000 OPEN
         17 Northwind Traders                15000 OPEN
         18 Blue Harbor Supplies             30000 PAID
         19 Northwind Traders                 2000 OPEN

9 rows selected.

ACCOUNT_NAME               CREDIT_LIMIT    OPEN_BALANCE
_______________________ _______________ _______________
Blue Harbor Supplies              20000           19000
Northwind Traders                 50000           57500
Summit Retail Group               30000            9000

Orders 16, 17 and 19 belong to Northwind Traders, whose open balance is now 57,500, above its limit, by confirmed choice. Identity values can have gaps, so the new IDs start at 16. The blocked orders for Summit Retail Group are not in the table.

Things to Know

  • Keep the rule in one function. The dynamic action and the validation call the same code, so the dialog and the server check can't disagree.
  • Always add the server-side validation. In the scripted submit test, the dynamic action never ran, and only the validation stopped the order.
  • Wrap the flag in NVL. FALSE OR NULL is NULL in PL/SQL, and in this test APEX let a NULL expression result pass.
  • Turn off Value Protected on the hidden items that JavaScript changes, and use Per Request (Memory Only) storage so an old answer is never reused.
  • The Confirm action replaces &ITEM. references in its message in the browser, so the server can build the message text.
  • Warnings run before hard errors in this design. A confirmed warning still ends in an error when a hard validation fails.
  • Set both Minimum Value and Maximum Value on a Number Field. With only Minimum Value, the range message had an empty number in this test.

Related Guides

Conclusion

Warnings that still allow saving in Oracle APEX need three parts. A function holds the rule, a dynamic action asks the server and shows a declarative Confirm dialog with Save Anyway and Go Back, and a hidden item records the user's answer for the submit.

The server-side validation makes the warning reliable. It blocks an unconfirmed save that skips the dialog, while hard errors like an account on hold stay normal validations that no confirmation can override.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted