How to Make a Whole Form Read-Only Based on the Record's Status in Oracle APEX

Use the page Read Only condition with a status query, hide Save and Delete, show a notice, and add a validation that blocks forced SAVE and DELETE requests.

Many records reach a point where they must not change any more: an order is shipped, an invoice is posted, a request is closed. The form that edits the record should then show the data but stop every change, including Save and Delete.

Oracle APEX has a Read Only condition on the page itself. This guide sets it from the stored status, hides the Save and Delete buttons, shows a notice, and adds a server-side validation that blocks changes even when someone submits the page by hand.

The guide uses store orders as the example; the same steps work for any table with a status column.

The older post How to Make an Item Read Only in Oracle APEX Without Losing Its Value switches one item with JavaScript in the browser. Here the whole page is made read-only on the server, so no item and no button can be used to change a locked record.

Step 1: Create the Tables

The example uses two tables. RETAIL_STORES holds the stores. STORE_ORDERS holds the orders, and each order belongs to one store.

STATUS has a check constraint that allows NEW, PACKED, SHIPPED and CANCELLED. Orders in SHIPPED or CANCELLED are the ones that must become read-only. Create the tables, the index and the sample data:

Example:

create table retail_stores (
  store_id     number generated by default on null as identity,
  store_name   varchar2(100 char) not null,
  city         varchar2(60 char),
  constraint retail_stores_pk primary key (store_id),
  constraint retail_stores_name_uk unique (store_name)
);

create table store_orders (
  order_id     number generated by default on null as identity,
  store_id     number             not null,
  order_date   date               default on null trunc(sysdate) not null,
  status       varchar2(20 char)  default on null 'NEW' not null,
  order_total  number(10,2)       not null,
  ship_note    varchar2(400 char),
  constraint store_orders_pk primary key (order_id),
  constraint store_orders_store_fk foreign key (store_id)
    references retail_stores (store_id),
  constraint store_orders_status_ck
    check (status in ('NEW', 'PACKED', 'SHIPPED', 'CANCELLED')),
  constraint store_orders_total_ck check (order_total >= 0)
);

create index store_orders_store_ix on store_orders (store_id);

insert into retail_stores (store_name, city) values ('Downtown Market', 'Pune');
insert into retail_stores (store_name, city) values ('Lakeside Corner', 'Bhopal');
insert into retail_stores (store_name, city) values ('Hilltop Supplies', 'Shimla');

insert into store_orders (store_id, order_date, status, order_total, ship_note)
values (1, date '2026-10-12', 'NEW', 1250, 'Deliver before noon');
insert into store_orders (store_id, order_date, status, order_total, ship_note)
values (2, date '2026-10-10', 'PACKED', 860.5, null);
insert into store_orders (store_id, order_date, status, order_total, ship_note)
values (3, date '2026-10-05', 'SHIPPED', 2310, 'Sent by road, 3 boxes');
insert into store_orders (store_id, order_date, status, order_total, ship_note)
values (1, date '2026-10-02', 'CANCELLED', 420, 'Store closed for renovation');
commit;

select o.order_id, s.store_name, o.order_date, o.status, o.order_total
from   store_orders o
join   retail_stores s on s.store_id = o.store_id
order  by o.order_id;

Output:

Table RETAIL_STORES created.

Table STORE_ORDERS created.

Index STORE_ORDERS_STORE_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

   ORDER_ID STORE_NAME          ORDER_DATE    STATUS          ORDER_TOTAL
___________ ___________________ _____________ ____________ ______________
          1 Downtown Market     12-10-26      NEW                    1250
          2 Lakeside Corner     10-10-26      PACKED                860.5
          3 Hilltop Supplies    05-10-26      SHIPPED                2310
          4 Downtown Market     02-10-26      CANCELLED               420
  • ORDER_DATE and STATUS use DEFAULT ON NULL, so a new order gets today's date and the status NEW without any code.
  • STORE_ORDERS_STATUS_CK keeps the status to the four allowed values.
  • The sample data has one order in each status, so every case can be tested.

Step 2: 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 470 and Name Store Orders. Turn on Include Form Page, enter Form Page Number 471 and Form Page Name Store Order, and select Form Page Mode Modal Dialog:

Create Interactive Report wizard with page 470 Store Orders and modal form page 471 Store Order marked with red boxes
The report page and a modal form page.

In the Data Source section, keep Local Database and Table, and enter STORE_ORDERS as the Table / View Name:

Data Source section of the wizard with the table STORE_ORDERS marked with a red box
The table for the report and the form.

Keep the Navigation settings, so the wizard adds a breadcrumb and a navigation menu entry. Click Next:

Navigation section of the wizard with Use Breadcrumb and Use Navigation turned on
The default navigation settings.

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 selected and marked with a red box
ORDER_ID is the primary key.

The wizard creates page 470 with the interactive report and page 471 with the form. Because STORE_ID is a foreign key, the wizard also creates a select list for it with a shared list of values on RETAIL_STORES.

Step 3: Turn the Status into a Select List

The wizard creates a text field for STATUS. Open page 471 in Page Designer, select P471_STATUS and set these properties:

  • Identification > Type: Select List
  • List of Values > Type: Static Values
  • Static Values: New (NEW), Packed (PACKED), Shipped (SHIPPED), Cancelled (CANCELLED)
  • Display Null Value: off
Page Designer with P471_STATUS set to Select List with Static Values marked with red boxes
The status as a select list with static values.

Also select P471_STORE_ID and change its Label from Store Id to Store. Click Save.

Step 4: Set the Read Only Condition of the Page

Select the page node Page 471: Store Order at the top of the tree. In the Read Only group, set Type to Rows returned and enter this SQL Query:

Example:

select 1
  from store_orders
 where order_id = :P471_ORDER_ID
   and status in ('SHIPPED', 'CANCELLED')
Page 471 properties with the Read Only Type Rows returned and the SQL Query marked with a red box
The page is read-only when the stored order is shipped or cancelled.

When the query returns a row, Oracle APEX renders every item of the page as read-only. The query reads the status from the table, not from the P471_STATUS item, so it always uses the saved status. Click Save.

Run page 470 and open the shipped order of Hilltop Supplies. All fields are read-only now, including the select lists and the date picker. But the Delete and Apply Changes buttons are still there:

Store Order dialog with read-only fields and the Cancel, Delete and Apply Changes buttons marked with a red box
The page Read Only condition does not hide the buttons.

In a test at this point, Apply Changes saved the order again without changes. Delete removed the shipped order from the table. The order was inserted again for the rest of this guide.

Example:

insert into store_orders (order_id, store_id, order_date, status, order_total, ship_note)
values (3, 3, date '2026-10-05', 'SHIPPED', 2310, 'Sent by road, 3 boxes');
commit;
select order_id, status, order_total, ship_note from store_orders order by order_id;

Output:

1 row inserted.

Commit complete.

   ORDER_ID STATUS          ORDER_TOTAL SHIP_NOTE
___________ ____________ ______________ ______________________________
          1 NEW                    1250 Deliver before noon
          2 PACKED                860.5
          3 SHIPPED                2310 Sent by road, 3 boxes
          4 CANCELLED               420 Store closed for renovation

A read-only page still needs its buttons handled. The next steps hide them and add a check on the server.

Step 5: Hide the Save and Delete Buttons

The wizard shows Apply Changes (SAVE) and Delete (DELETE) when P471_ORDER_ID is not null. Replace this condition with one that also checks the status.

Select the SAVE button. Under Server-side Condition, set Type to Rows returned and enter this SQL Query:

Example:

select 1
  from store_orders
 where order_id = :P471_ORDER_ID
   and status not in ('SHIPPED', 'CANCELLED')
SAVE button with the Server-side Condition Rows returned and the SQL Query marked with a red box
Apply Changes shows only for an existing order that is not locked.

Select the DELETE button and set the same Server-side Condition:

DELETE button with the same Server-side Condition marked with a red box
Delete uses the same condition.

For a new order, P471_ORDER_ID is null and the query returns no row, so both buttons stay hidden and only Create shows. This is the same behavior the wizard's original condition had. Click Save.

Step 6: Show a Notice on Locked Orders

Users should see why they cannot edit the order. In the Rendering tree, right-click Content Body and click Create Region. Set these properties:

  • Identification > Name: Read-Only Order
  • Type: Static Content
  • Source > HTML Code: This order is shipped or cancelled, so it can no longer be changed.
  • Layout > Sequence: 5, so the notice shows above the form region (sequence 10)
Region Read-Only Order with Name, Type, HTML Code and Sequence marked with red boxes
A static content region for the notice.

Under Appearance, set Template to Alert. In Template Options, select Horizontal, Show Default Icons and Warning. Then set Server-side Condition > Type to Rows returned with the same query as the page Read Only condition:

Region appearance with the Alert template and Warning options, and the Server-side Condition Rows returned marked with red boxes
The notice shows only when the order is locked.

Click Save.

Step 7: Block Changes on the Server

Hidden buttons stop normal users. They do not stop a page submit sent from the browser console or a script. A validation checks the status again when the page is submitted.

Click the Processing tab. Right-click Validations and click Create Validation. Set these properties:

  • Identification > Name: Order Can Be Changed
  • Validation > Type: No Rows returned
  • SQL Query: the same query as the page Read Only condition
  • Always Execute: on
Validation Order Can Be Changed with Type No Rows returned, the SQL Query and Always Execute marked with red boxes
The validation fails when the stored order is locked.

Then set the error and the condition:

  • Error > Error Message: Shipped and cancelled orders cannot be changed or deleted.
  • Display Location: Inline in Notification
  • Server-side Condition > Type: Request is contained in Value, Value: SAVE,DELETE
Validation error message, display location and the Server-side Condition Request is contained in Value SAVE,DELETE marked with red boxes
The validation runs for the SAVE and DELETE requests.

Always Execute matters here. The wizard creates the DELETE button with Execute Validations off. In a first test with Always Execute off, a forced DELETE request skipped the validation and deleted the shipped order. With Always Execute on, the validation runs for DELETE too. Click Save.

Step 8: Test the Form

Run page 470 as DEMO_DEV. The report lists the four orders with their status:

Store Orders interactive report with orders in NEW, CANCELLED, PACKED and SHIPPED status
One order in each status.

Open the NEW order of Downtown Market. All fields can be edited, and Delete and Apply Changes are shown:

Store Order dialog for a NEW order with editable fields marked with a red box and the Delete and Apply Changes buttons
An order in NEW status is editable.

Open the shipped order of Hilltop Supplies. The notice shows at the top, every field is read-only, and only Cancel is left:

Store Order dialog for a shipped order with the Read-Only Order notice marked with a red box and only the Cancel button
A shipped order is read-only.

Now ship an order. Open the PACKED order of Lakeside Corner, select Status Shipped, enter Ship Note Courier, 2 boxes, and click Apply Changes:

Store Order dialog for the packed order with Status Shipped and Ship Note Courier, 2 boxes marked with a red box
The last change before the order is locked.

The save works, because the validation checks the stored status, which was still PACKED. The report shows the order as SHIPPED:

Store Orders report with the Row updated message and the Lakeside Corner order now SHIPPED
The order is saved as SHIPPED.

Open the same order again. It is read-only now:

Store Order dialog for the Lakeside Corner order with the Read-Only Order notice marked with a red box
The order is locked after it was shipped.

Click Create on page 470. The new order form is editable and shows only Cancel and Create, so the status conditions do not affect new rows.

Step 9: Test a Forced Submit

Open a shipped order and run this code in the browser console of the dialog. It sends the DELETE request without the button:

Example:

apex.page.submit({ request: 'DELETE', validate: false });

The request reached the server, and the validation stopped it. The dialog stays open with the error message:

Store Order dialog after a forced DELETE with the error Shipped and cancelled orders cannot be changed or deleted marked with a red box
The server rejects the forced DELETE.

The same code with request SAVE on the cancelled order returned the same error. A forced SAVE on the NEW order was saved normally.

The table confirms that no locked order was changed or deleted:

Example:

select o.order_id, s.store_name, o.status, o.order_total, o.ship_note
from   store_orders o
join   retail_stores s on s.store_id = o.store_id
order  by o.order_id;

Output:

   ORDER_ID STORE_NAME          STATUS          ORDER_TOTAL SHIP_NOTE
___________ ___________________ ____________ ______________ ______________________________
          1 Downtown Market     NEW                    1250 Deliver before noon
          2 Lakeside Corner     SHIPPED               860.5 Courier, 2 boxes
          3 Hilltop Supplies    SHIPPED                2310 Sent by road, 3 boxes
          4 Downtown Market     CANCELLED               420 Store closed for renovation

Things to Know

  • The page Read Only condition makes items read-only but does not hide or disable buttons. Always hide Save and Delete with their own condition.
  • Read-only items are protected by a checksum. In a test, changing the value of a read-only field in the browser and submitting ended with "Session state protection violation" and nothing was saved.
  • Use the stored status in all conditions, not the status item. A user who changes the status to Shipped can still save that change once; the form locks on the next load.
  • Buttons with Execute Validations off skip normal validations. Turn on Always Execute for a validation that must also protect DELETE.
  • The validation protects this form only. Other pages, scripts or REST services that update STORE_ORDERS need their own check.
  • For read-only access by role instead of by status, use the Read Only condition of a region or page with an authorization check.

Related Guides

Conclusion

To make a whole form read-only by status in Oracle APEX, set the page Read Only condition to a query on the stored status. Every item on the page becomes read-only, and a static Alert region tells users why.

The Read Only condition does not touch buttons, so hide Save and Delete with the opposite query. A No Rows returned validation with Always Execute on checks the status again on the server and stops forced SAVE and DELETE requests.

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