Pretius. Built Smarter: Strategic merger as an answer to modern challenges
Pretius. Built Smarter:
Strategic merger as an answer to modern challenges

Managing the Oracle Forms PL/SQL migration: What to Keep and What to Rewrite in APEX

Bartosz Świątek

Content Writer

  • May 5, 2026

Contents

TL;DR

  • Oracle Forms PL/SQL splits into two categories: database code that migrates unchanged, and UI manipulation code that has no APEX equivalent. Knowing which is which is the entire migration problem.
  • The diagnostic test: search your codebase for SYNCHRONIZE, GO_BLOCK, GO_ITEM, SET_ITEM_PROPERTY, and COMMIT_FORM. Every hit marks a redesign decision, not a port.
  • SYNCHRONIZE has no APEX equivalent — replace it with apex.server.process() AJAX callbacks. This is an architectural change, not a syntax swap.
  • The target architecture is SmartDB: thin APEX UI layer, all business logic in database packages. If logic can’t be tested from a SQL prompt, it’s in the wrong place.
  • As of APEX 21.1, Oracle has desupported the Forms Migration Project. Current migrations are manual, ground-up rebuilds. Oracle’s own FAQ states it is “faster to build your apps from scratch.”
  • Roughly 30–40% of Forms PL/SQL code can move to the database unchanged (Pitss). That number is only useful if you know how to identify which 30–40%.

Where APEX Actually Runs Your Code

APEX is a set of PL/SQL packages living inside the Oracle Database. ORDS — the middle tier — handles HTTP routing, session token validation, and static file serving. It does not execute business logic or run page validations. When a request enters the database, the APEX engine takes over: it resolves the page definition, initializes session state, runs computations, executes validations, and performs DML — all in a single database session.

When a page is submitted, the execution order is fixed: Computations Validations Processes Branch. Calling an existing package from an APEX Page Process is a BEGIN … END block. Nothing needs to change in the package itself:

BEGIN
 my_existing_package.process_order(
   p_order_id => :P1_ORDER_ID,
   p_user     => :APP_USER
 );
END;

The one thing that changes the PL/SQL calculus is session state. Oracle Forms maintained a persistent database connection throughout the user session. APEX is stateless HTTP — values are stored in database tables between requests, not in memory. The same APEX session may hit different database sessions across requests. Package-level global variables will not hold their values reliably. This matters, and we’ll come back to it.

The Decision Matrix — Keep, Evaluate, Rewrite

Run a text search across all program units and triggers for the built-in names in the REWRITE list below. Every hit marks a redesign decision, not a port. This is the fastest way to scope an Oracle Forms PL/SQL migration before writing a single line of APEX code.

Keep (zero or minimal changes)

Code typeCondition
Packages and proceduresNo references to SYNCHRONIZE, GO_BLOCK, GO_ITEM, SET_ITEM_PROPERTY, COMMIT_FORM, CLEAR_BLOCK, EXECUTE_QUERY
Database triggersBEFORE INSERT, AFTER UPDATE, etc. — run at the database level, invisible to the UI layer
Functions used in SQL queriesUnchanged if no Forms globals or UI item references
Validation logicCallable from SQL*Plus without APEX context
The entire data modelSchema, views, indexes — untouched

The test is simple: if it runs from a SQL*Plus prompt without APEX context, it survives.

Evaluate (likely needs refactoring)

Code typeIssue
Program units mixing business logic with UI commandsExtract the database logic before discarding the UI logic
Code using NAME_INForms global variable access — must be rewritten to use v() in APEX
Package-level global variables for stateMust shift to APEX_UTIL.SET_SESSION_STATE
Code using COPY or DEFAULT_VALUE built-insNo direct APEX equivalent

Rewrite (cannot migrate directly — Forms built-ins with no APEX equivalent)

Built-inReason
SYNCHRONIZEUI event queue flush — the concept does not exist in APEX
GO_BLOCK, GO_ITEMNavigation is URL-based or button-driven in APEX
SET_ITEM_PROPERTY / GET_ITEM_PROPERTYReplaced by Dynamic Actions or apex.item() JavaScript
ENTER_QUERY / EXECUTE_QUERYAPEX has no query mode
CLEAR_BLOCK, DELETE_RECORDUI-level data operations with no equivalent
MESSAGE()Replaced by apex_application.g_print_success_message or alerts
.pll library code with UI commandsAll Forms built-ins must be removed before migration

The practical implication: a Forms Program Unit that mixes a SYNCHRONIZE call into business logic needs to be split, not ported. The business logic moves to a database package. The UI behavior gets redesigned. These are two separate tasks.

The SYNCHRONIZE Problem (and Its Actual Replacement)

SYNCHRONIZE is the most commonly asked question in Oracle Forms to APEX migration projects, and the one most underserved in existing documentation. Here is the full treatment.

What Does SYNCHRONIZE Do in Oracle Forms?

SYNCHRONIZE is a UI flush. It forces the Forms runtime to immediately process its pending event queue — it tells the screen to repaint now. It does not touch the database. It does not commit. It does not fetch records.

The typical use case is progress feedback during a long-running PL/SQL loop:

-- Oracle Forms: progress feedback pattern
FOR i IN 1..10000 LOOP
 :CTRL.PROGRESS_LABEL := 'Processing record ' || i;
 SYNCHRONIZE;  -- Force screen refresh so user sees progress
 process_record(i);
END LOOP;

This works in Forms because the Forms client is a persistent Java applet with a continuous connection to the database. The UI runtime and PL/SQL execution share a thread. SYNCHRONIZE can interrupt that thread and repaint the screen while PL/SQL is still running.

Why SYNCHRONIZE Has No APEX Equivalent

APEX runs over stateless HTTP. Each page submission is a complete request-response cycle. There is no in-flight UI state to flush, because there is no persistent client runtime holding that state. The Forms execution model and the APEX execution model are structurally different — SYNCHRONIZE does not have a drop-in replacement because the concept it implements does not exist in APEX.

This is an architecture problem, not a syntax problem. Acknowledging that plainly is more useful than minimizing it. If you find SYNCHRONIZE in a Forms Program Unit, you have found a unit that is coupled to the Forms UI runtime. The intent — showing the user that something is happening — still exists in APEX. The implementation must be completely different.

The APEX Replacement Pattern

The replacement for SYNCHRONIZE-based progress feedback is an async AJAX pattern using apex.server.process() — an asynchronous call that triggers a server-side PL/SQL process and updates the UI via a JavaScript callback.

JavaScript Dynamic Action (triggers the server-side process and updates the page):

function updateProgress(step) {
 apex.server.process('UPDATE_PROGRESS', {
   x01: step
 }, {
   success: function(data) {
     // Callback fires when the server responds — update the UI here
     apex.item('P1_PROGRESS').setValue(data.progress);
   }
 });
}

APEX On-Demand Process named UPDATE_PROGRESS (the PL/SQL that runs on the server):

DECLARE
 l_step NUMBER := apex_application.g_x01;
BEGIN
 -- x01 carries the step parameter passed from JavaScript
 process_record(l_step);
 apex_json.open_object;
 apex_json.write('progress', ROUND((l_step/10000)*100) || '%');
 apex_json.close_object;
END;

For simpler cases — where all you need is to run PL/SQL and refresh a page item — a Dynamic Action with “Execute Server-Side PL/SQL Code” and an “Items to Return” list handles it without JavaScript. apex.region(‘my_report’).refresh() replaces the EXECUTE_QUERY + SYNCHRONIZE pattern for report refresh.

The full progress-feedback pattern requires the AJAX approach above. That is a genuine redesign, and treating it as such will save time compared to discovering mid-migration that the shortcut doesn’t work.

Decoupling the Database Layer — The SmartDB Pattern

SmartDB is the recognized architectural target for APEX backend design. Defined by Philipp Salvisberg: the APEX UI layer is a thin client. No business logic lives in Page Processes — only calls to database packages. If the logic cannot be executed from a SQL prompt without opening Page Designer, the architecture is wrong.

Why This Matters for Oracle Forms PL/SQL Migration

Forms developers routinely embed business logic inside triggers and program units that are tightly coupled to the UI layer. When those get migrated to APEX Page Processes — the path of least resistance — the coupling problem migrates with them. Business logic ends up invisible to utPLSQL, untestable from a CI/CD pipeline, and difficult to maintain.

The architectural target is a three-layer service structure: Data Services (TAPI) own individual tables, Business Modules orchestrate across multiple TAPIs, and APEX Page Processes are thin wrappers that do nothing but call into the Business Module layer.

-- Layer 1: TAPI owns the table — all DML goes through here
CREATE OR REPLACE PACKAGE emp_tapi AS
 PROCEDURE update_salary(p_emp_id IN employees.employee_id%TYPE,
                         p_salary IN employees.salary%TYPE);
END emp_tapi;

-- Layer 2: Business module orchestrates across TAPIs
CREATE OR REPLACE PACKAGE emp_business AS
 PROCEDURE hire_employee(p_first   IN VARCHAR2,
                         p_dept_id IN NUMBER,
                         p_salary  IN NUMBER);
END emp_business;

-- Layer 3: APEX Page Process — a single call, no logic
BEGIN
 emp_business.hire_employee(
   p_first   => :P10_FIRST_NAME,
   p_dept_id => :P10_DEPARTMENT_ID,
   p_salary  => :P10_SALARY
 );
END;

Validation logic in database packages can push errors directly to APEX’s inline field display using apex_error.add_error(). Session state is readable from packages via v(‘P1_ITEM’) and writable via APEX_UTIL.SET_SESSION_STATE(). The full APEX API is available from inside PL/SQL packages — the database layer does not need to know it is being called from a web application.

The testability point is direct: business logic in TAPI and Business Modules is testable with utPLSQL. Logic trapped inside APEX Page Designer is not. CI/CD pipelines can test packages; they cannot test Page Processes. This is why decoupling the database layer is an architectural requirement for APEX, not a preference.

Trigger Migration — The Hidden Business Logic Problem

Database triggers — BEFORE INSERT, AFTER UPDATE — survive unchanged. They operate at the database level and are invisible to the UI layer. Forms triggers are a different matter.

Which Forms Triggers Have No APEX Equivalent?

Forms trigger types map to APEX as follows:

Forms TriggerAPEX EquivalentGotcha
WHEN-VALIDATE-ITEMAPEX Validation (PL/SQL)Often contains complex DB logic — audit each one before categorizing
POST-QUERYNo direct equivalentDerived values must be embedded in the SQL query or pre-computed in a Before Regions process
PRE-FORMPage Load Process (Before Regions)Runs once at page load — same intent, different execution point
ON-ERRORapex_error.add_error() + Error Handling FunctionCentralized; must be wired explicitly
KEY-COMMITSubmit Page + DML ProcessAPEX handles DML declaratively or via a PL/SQL process

POST-QUERY: the hardest trigger to migrate

POST-QUERY fires row-by-row after each record fetch in Forms. It is used to compute derived values — fields calculated from the fetched record that are not stored in the database. APEX has no row-by-row post-fetch hook.

This is the most common source of production bugs in migrated Forms applications. The trigger looks like a reporting concern rather than business logic, and missing calculations only surface in production when users notice wrong values.

The fix depends on what the trigger does:

  • If the calculation is deterministic given the query data — embed it directly in the region SQL using CASE, analytic functions, or subqueries.
  • If it requires external data or complex logic — use a “Before Regions” page process to populate an APEX collection with pre-computed values before the region renders.

Audit every POST-QUERY trigger during discovery. Do not assume they are trivial.

WHEN-VALIDATE-ITEM: do not assume field-level checks

The name implies simple input validation. WHEN-VALIDATE-ITEM triggers frequently contain business rule enforcement that queries other tables and coordinates multiple conditions. Treat each one as a black box until you have read the code.

Three Gotchas That Will Produce Silent Bugs

Package global state is not reliable in APEX

APEX uses connection pooling. The same APEX session may be served by different database sessions across requests. Package-level global variables — g_current_user VARCHAR2(100) declared at the package level — are per-database-session, not per-APEX-session. In Forms, a package global persisted across trigger calls within a session. In APEX, that assumption breaks silently.

Fix: use v(‘APP_USER’) and APEX_UTIL.SET_SESSION_STATE(). These read from and write to the session state table, which persists correctly across requests regardless of which database session handles the next request.

Dynamic Actions require explicit “Items to Submit”

When a Dynamic Action fires an “Execute Server-Side PL/SQL Code” action, it uses the session state value — the value last submitted to the server — not what is currently in the browser. If P1_LIST was changed in the UI but not yet submitted to the database, the PL/SQL will operate on the previous value.

Fix: list every page item the PL/SQL needs in the “Items to Submit” field on the dynamic action action step. This syncs the current browser value to the session state before the PL/SQL runs.

APEX has implicit commit points

APEX issues implicit commits at points that are not always obvious: after page rendering completes, before branching to another page, and after APEX_UTIL.SET_SESSION_STATE changes a value. Migrated code that assumes explicit transaction control — closing a transaction only with explicit COMMIT or ROLLBACK calls — needs to account for these. An implicit commit mid-process can leave partial transactions in an unexpected state.

APEX Processing Points — What Forms’ “Logical Areas” Map To

Forms Program Units bundle everything together. APEX forces explicit separation into typed processing points. Coming from Forms, the mapping is not immediately obvious.

APEX Processing PointWhat Goes HereWhen It Runs
ComputationsAssign values to itemsOn page load or page submit
ValidationsEdit checks that block submissionBefore Processes, on submit
ProcessesDML operations and package callsAfter Validations pass, on submit
Dynamic ActionsClient-side event-driven logicBrowser events (click, change, focus)
On-Demand Processes (AJAX Callbacks)PL/SQL called from JavaScriptAsynchronously, via apex.server.process()

PL/SQL package calls belong in Processes and On-Demand Processes. JavaScript belongs in Dynamic Actions. Page Designer provides the wiring between them. The database developer writes the packages, which is where the substantive work is.

What Survives, What Doesn’t, and What Comes Next

Roughly 30–40% of Oracle Forms PL/SQL code can move to the database unchanged (Pitss). The remainder is UI logic that must be redesigned. That split is only useful if you can identify which 30–40%, which is what the decision matrix above gives you. The diagnostic text search is where to start: find every SYNCHRONIZE, every GO_BLOCK, every SET_ITEM_PROPERTY, and you have found the boundary between what ports and what must be rebuilt.

One thing worth stating plainly: Oracle desupported the Forms Migration Project in APEX 21.1. Their own FAQ explains why — “it’s faster to build your apps from scratch rather than convert them using the Migration Project.” The tool was not delivering value because automating a migration across an architectural boundary requires understanding both sides of it. Current Oracle Forms to APEX migrations are manual builds. There is no automated shortcut around the SYNCHRONIZE problem or the POST-QUERY problem.

Pretius’s AI Forms to APEX Assistant — which analyzes.FMB files and generates TAPI/XAPI packages from trigger code — can compress the discovery and scaffolding phases. It achieves 90% estimation accuracy and explicitly flags constructs with no APEX equivalent, which is useful precisely because those flags are the expensive part of scoping a migration.

For developers building out their migrated APEX application, Pretius maintains open-source plugins on GitHub covering common APEX development needs. One worth knowing is Translate APEX — a community translation project supporting 52 languages, including 21 not natively supported by Oracle APEX. Oracle natively supports 31 languages; if your migrated application serves users across regions, you will reach the internationalization question quickly. Translate APEX covers the gap.

Explore the full Pretius plugin library on GitHub: https://github.com/orgs/Pretius/repositories

Looking for a software development company?

Work with a team that already helped dozens of market leaders. Book a discovery call to see:

  • How our products work
  • How you can save time & costs
  • How we’re different from another solutions

footer-contact-steps

We keep your data safe: ISO certified

We operate in accordance with the ISO 27001 standard, ensuring the highest level of security for your data.
certified dekra 27001
© 2026 Pretius. All right reserved.