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.
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 type | Condition |
| Packages and procedures | No references to SYNCHRONIZE, GO_BLOCK, GO_ITEM, SET_ITEM_PROPERTY, COMMIT_FORM, CLEAR_BLOCK, EXECUTE_QUERY |
| Database triggers | BEFORE INSERT, AFTER UPDATE, etc. — run at the database level, invisible to the UI layer |
| Functions used in SQL queries | Unchanged if no Forms globals or UI item references |
| Validation logic | Callable from SQL*Plus without APEX context |
| The entire data model | Schema, 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 type | Issue |
| Program units mixing business logic with UI commands | Extract the database logic before discarding the UI logic |
| Code using NAME_IN | Forms global variable access — must be rewritten to use v() in APEX |
| Package-level global variables for state | Must shift to APEX_UTIL.SET_SESSION_STATE |
| Code using COPY or DEFAULT_VALUE built-ins | No direct APEX equivalent |
| Built-in | Reason |
| SYNCHRONIZE | UI event queue flush — the concept does not exist in APEX |
| GO_BLOCK, GO_ITEM | Navigation is URL-based or button-driven in APEX |
| SET_ITEM_PROPERTY / GET_ITEM_PROPERTY | Replaced by Dynamic Actions or apex.item() JavaScript |
| ENTER_QUERY / EXECUTE_QUERY | APEX has no query mode |
| CLEAR_BLOCK, DELETE_RECORD | UI-level data operations with no equivalent |
| MESSAGE() | Replaced by apex_application.g_print_success_message or alerts |
| .pll library code with UI commands | All 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.
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.
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.
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 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.
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.
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.
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.
Forms trigger types map to APEX as follows:
| Forms Trigger | APEX Equivalent | Gotcha |
| WHEN-VALIDATE-ITEM | APEX Validation (PL/SQL) | Often contains complex DB logic — audit each one before categorizing |
| POST-QUERY | No direct equivalent | Derived values must be embedded in the SQL query or pre-computed in a Before Regions process |
| PRE-FORM | Page Load Process (Before Regions) | Runs once at page load — same intent, different execution point |
| ON-ERROR | apex_error.add_error() + Error Handling Function | Centralized; must be wired explicitly |
| KEY-COMMIT | Submit Page + DML Process | APEX handles DML declaratively or via a PL/SQL process |
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:
Audit every POST-QUERY trigger during discovery. Do not assume they are trivial.
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.
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.
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 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.
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 Point | What Goes Here | When It Runs |
| Computations | Assign values to items | On page load or page submit |
| Validations | Edit checks that block submission | Before Processes, on submit |
| Processes | DML operations and package calls | After Validations pass, on submit |
| Dynamic Actions | Client-side event-driven logic | Browser events (click, change, focus) |
| On-Demand Processes (AJAX Callbacks) | PL/SQL called from JavaScript | Asynchronously, 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.
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