Building a Dynamic Multi-Level Approval Workflow Engine in Oracle APEX 24.2 — Part 1
Enterprise ERP systems rarely operate with simple approval requirements. Different business processes require different approval…
Building a Dynamic Multi-Level Approval Workflow Engine in Oracle APEX 24.2 — Part 1
Enterprise ERP systems rarely operate with simple approval requirements. Different business processes require different approval hierarchies, routing mechanisms, and authorization rules. A purchase order may require department-level approval, while a budget request may move through Finance, Accounts, and CEO approvals. Similarly, inventory transfers, leave applications, loan requests, and hospital transactions all follow different business workflows.
In many Oracle APEX applications, approval logic is initially implemented directly inside pages or package procedures. This approach works during early development stages, but over time it becomes difficult to maintain. Every organizational change requires code modification, and every new document type introduces another hardcoded approval flow.
During one of our ERP implementations, we reached a stage where maintaining workflows became increasingly difficult. Different branches required different approval chains, reporting hierarchies were changing frequently, and users often held multiple roles simultaneously. The existing hardcoded approach was becoming a maintenance challenge.
To solve this problem, I designed and implemented a fully metadata-driven workflow engine in Oracle APEX using Oracle Database and PL/SQL. The objective was simple:
- No hardcoded approval logic
- Dynamic approver determination
- Reusable workflow architecture
- Branch-wise approval control
- Complete audit trail
- Notification integration
- Scalable design for future ERP modules
The Real Business Problem
Initially, approval logic was directly embedded inside transaction screens. For example:
- Purchase Orders had separate approval procedures
- Leave requests had separate approval tables
- Budget approvals used different routing logic
- Loan requests followed another structure
Although the functionality worked, the design became difficult to maintain.
Some of the major problems we faced were:
- Approval hierarchies changing frequently
- Different branches requiring different workflows
- Users performing multiple organizational roles
- CEO-level users requiring direct approvals
- Duplicate approval routing
- Difficulty tracking approval history
- Hardcoded approver assignments
- Repeated workflow code across modules
At one point, even small organizational changes required modifications in multiple procedures.
That was the stage where we decided to redesign the workflow system completely.
Workflow Architecture
The final workflow architecture was designed around a metadata-driven approach.
Instead of creating separate approval systems for every module, all ERP transactions use a centralized workflow engine.
The overall flow works like this:
Transaction Creation | Workflow Detection | Role Resolution | Approver Determination | Approval Queue Generation | Notifications | Approval / Rejection / Return | Workflow History Tracking
This architecture allowed the same workflow engine to support:
- Purchase Orders
- Budget Approvals
- Leave Requests
- Loan Requests
- Inventory Transfers
without changing core workflow logic.
Database Design
The workflow engine was designed using reusable tables instead of module-specific structures.
WF_DOCUMENTS
This table stores all workflow-enabled ERP documents.

Examples include:
- Purchase Order
- Leave Request
- Budget Approval
- Inventory Transfer
Each document is linked with:
- Oracle APEX page
- ERP module
- workflow configuration
This became the central registration point for workflow-enabled transactions.
WF_ROLES
This table defines workflow roles such as:
- Department Manager
- HR Manager
- Finance Manager
- Store Manager
- CEO

Instead of assigning approvals directly to users, the workflow system uses role-based routing.
WF_APP_USER_ROLES
This table maps application users with workflow roles.

WF_WORKFLOW
This table stores workflow master definitions.
Each record represents a complete approval workflow.

Examples:
- Purchase Order Workflow
- Budget Approval Workflow
- Leave Approval Workflow
The design also supports branch-wise workflows, allowing different branches to use different approval chains.
For example, here is the sample data:

WF_WORKFLOW_DETAIL
This table stores workflow steps.

WF_APPROVALS
This table stores live workflow transactions.

Whenever a transaction enters workflow, approval records are generated here.
This table tracks:
- current approver
- current workflow stage
- step status
- approval remarks
It acts as the operational approval queue of the ERP system.
WF_APPROVAL_HISTORY
This table stores the complete workflow audit trail.

Every action is recorded:
- Approved
- Rejected
- Returned
- Re-submitted
This became extremely useful during audits and operational reviews.
Dynamic Approval Determination
The core of the system is the procedure:
WF_DETERMINE_APPROVAL
This procedure dynamically generates approval routing for any ERP transaction.
Instead of hardcoding approval chains, the procedure determines everything dynamically using workflow metadata.
The procedure accepts:
P_PAGE_NO P_BRANCH_FK P_USER_FK P_TRANSACTION_FK
This design made the procedure reusable across all ERP modules.
Here is the full PL/SQL procedure:
create or replace procedure wf_determine_approval(p_page_no IN number, p_branch_fk IN number, p_user_fk IN number, p_transaction_fk IN number) IS
v_document_fk number;
v_document_name varchar2(100);
v_user_name varchar2(100);
v_reports_to_fk number;
v_workflow_fk number;
begin
— First check for configured workflow
begin
select workflow_pk, document_fk, document_name
into v_workflow_fk, v_document_fk, v_document_name
from wf_workflow, wf_documents
where document_pk = document_fk
and page_no = p_page_no
and branch_fk = p_branch_fk
and is_active = ‘Y’
AND ROWNUM = 1;
exception
when no_data_found then null;
when others then
raise_application_error(-20001, ‘wf_determine_approval: ‘ || sqlerrm);
end;
if v_document_fk is not null then — Workflow Found
— CLEANUP OLD WORKFLOW (IMPORTANT)
DELETE FROM wf_approvals
WHERE transaction_fk = p_transaction_fk AND branch_fk = p_branch_fk AND page_no = p_page_no;
— ==========================================
— CEO / Top-level Direct approval (skip workflow)
— ==========================================
begin
select reports_to_fk into v_reports_to_fk FROM app_users WHERE app_user_pk = p_user_fk;
exception
when no_data_found then
v_reports_to_fk := null;
when others then
raise_application_error(-20002, ‘Error fetching reports_to_fk: ‘ || sqlerrm);
end;
IF v_reports_to_fk IS NULL THEN
INSERT INTO wf_approvals(document_fk, branch_fk,created_by_fk, transaction_fk, workflow_fk, role_fk, step_no, next_approver_fk, page_no, step_status)
VALUES(v_document_fk, p_branch_fk, p_user_fk, p_transaction_fk, v_workflow_fk, NULL, 1, p_user_fk, p_page_no, ‘Y’);
RETURN; — VERY IMPORTANT
END IF;
— ==========================================
— NORMAL WORKFLOW STARTS HERE
— ==========================================
for i in(
select x.document_pk, x.document_name, x.workflow_pk, x.workflow_name, x.role_pk, x.role_name, x.step_no,
CASE
WHEN x.next_approver_fk IS NOT NULL THEN x.next_approver_fk
ELSE (
SELECT MIN(app_user_fk) FROM wf_app_user_roles r WHERE r.role_fk = x.role_pk AND r.document_fk = x.document_pk AND r.is_active = ‘Y’)
END AS final_approver_fk from( select document_pk, document_name, workflow_pk, workflow_name, role_pk, role_name, step_no, (SELECT APP_USER_PK FROM ( SELECT DISTINCT APP_USER_PK FROM APP_USERS au, WF_APP_USER_ROLES r WHERE au.APP_USER_PK = r.APP_USER_FK AND r.DOCUMENT_FK = wf_documents.DOCUMENT_PK AND r.ROLE_FK = wf_workflow_detail.ROLE_FK AND r.IS_ACTIVE = ‘Y’ AND au.APP_USER_PK <> p_user_fk AND au.APP_USER_PK IN ( SELECT au2.app_user_pk FROM app_users au2 START WITH au2.app_user_pk = p_user_fk CONNECT BY PRIOR au2.reports_to_fk = au2.app_user_pk))
WHERE ROWNUM = 1) AS NEXT_APPROVER_FK
from wf_workflow_detail, wf_workflow, wf_documents, wf_roles
where workflow_pk = wf_workflow_detail.workflow_fk
and document_pk = wf_workflow.document_fk and document_pk = v_document_fk and wf_workflow.branch_fk = p_branch_fk and role_pk = wf_workflow_detail.role_fk and wf_roles.is_active = ‘Y’ and wf_documents.is_active = ‘Y’ order by step_no) x) loop
DECLARE
v_approver NUMBER;
v_status VARCHAR2(20);
BEGIN
— Determine final approver
v_approver := i.final_approver_fk;
— ==========================================
— CASE: No approver found (SELF APPROVAL)
— ==========================================
IF v_approver IS NULL THEN
v_approver := p_user_fk;
v_status := ‘Y’; — auto approve
ELSE
v_status := NULL; — pending approval
END IF;
INSERT INTO wf_approvals(document_fk, branch_fk, created_by_fk, transaction_fk, workflow_fk, role_fk, step_no, next_approver_fk, page_no, step_status)
VALUES(i.document_pk, p_branch_fk, p_user_fk, p_transaction_fk, i.workflow_pk, i.role_pk, i.step_no, v_approver, p_page_no, v_status);
END;
end loop;
— Delete previos records before creator
DECLARE
v_min_step_no NUMBER;
BEGIN
SELECT MIN(step_no) INTO v_min_step_no FROM wf_approvals WHERE transaction_fk = p_transaction_fk AND branch_fk = p_branch_fk AND page_no = p_page_no AND next_approver_fk = p_user_fk;
DELETE FROM wf_approvals WHERE transaction_fk = p_transaction_fk AND branch_fk = p_branch_fk AND page_no = p_page_no AND step_no < v_min_step_no;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL; — User is not in the workflow, no need to delete
END;
— Delete previos records before reporting manager
DECLARE
v_min_step_no NUMBER;
BEGIN
SELECT MIN(step_no) INTO v_min_step_no FROM wf_approvals WHERE transaction_fk = p_transaction_fk AND branch_fk = p_branch_fk AND page_no = p_page_no AND next_approver_fk = NVL((select REPORTS_TO_FK from app_users where app_user_pk = p_user_fk), -100);
DELETE FROM wf_approvals WHERE transaction_fk = p_transaction_fk AND branch_fk = p_branch_fk AND page_no = p_page_no AND step_no < v_min_step_no AND next_approver_fk <> p_user_fk;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL; — User is not in the workflow, no need to delete
WHEN OTHERS THEN
raise_application_error(-20001, ‘wf_determine_approval: ‘ || sqlerrm);
END;
— GET USER NAME
BEGIN
SELECT USER_NAME INTO V_USER_NAME FROM APP_USERS WHERE APP_USER_PK = P_USER_FK;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN
raise_application_error(-20001, ‘wf_determine_approval: ‘ || sqlerrm);
END;
— ============= NEXT APPROVER ==============================
DECLARE
V_INFO WF_STATUS_INFO;
V_NEXT_APPROVER NUMBER;
BEGIN
V_INFO := PKG_WF_APPROVAL_STATUS.GET_CURRENT_STATUS(P_PAGE_NO => P_PAGE_NO, P_TRANSACTION_ID => p_transaction_fk);
V_NEXT_APPROVER:= V_INFO.APPROVER_FK;
— NOTIFY NEXT APPROVER
IF V_NEXT_APPROVER IS NOT NULL THEN
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK,NOTIFICATION_TITLE, DUE_DATE, DESCR, IS_READ, NOTIFICATION_DATE, PAGE_NO, TRANSACTION_FK, NOTIFICATION_TYPE, NOTIFICATION_CATEGORY)
VALUES(V_NEXT_APPROVER, ‘Workflow Approval’, SYSDATE, ‘A new document is created and required your approval.’ || ‘ Document Name: ‘ || V_DOCUMENT_NAME || ‘ User Name:’ || V_USER_NAME, ’N’, SYSDATE, P_PAGE_NO, P_TRANSACTION_FK, ‘Info’, ‘W’);
END IF;
END;
end if;
end;
/
PL/SQL Procedure WF_DETERMINE_APPROVAL Explanation:
Workflow Detection
The first step is identifying whether a workflow exists for the current transaction.
select workflow_pk, document_fk, document_name into v_workflow_fk, v_document_fk, v_document_name from wf_workflow, wf_documents where document_pk = document_fk and page_no = p_page_no and branch_fk = p_branch_fk and is_active = ‘Y’;
CEO Auto-Approval Logic
One interesting business requirement involved top-level management users.
If a user had no reporting manager:
IF v_reports_to_fk IS NULL THEN
the system treated the transaction as automatically approved.
This removed unnecessary approvals for executive users and simplified workflow routing.
Dynamic Approver Resolution
The biggest challenge was determining approvers dynamically.
The system needed to:
- respect organizational hierarchy
- support role-based routing
- avoid self-approvals
- prevent duplicate approvers
The solution used role mappings combined with reporting hierarchy.
SELECT MIN(app_user_fk) FROM wf_app_user_roles r WHERE r.role_fk = x.role_pk AND r.document_fk = x.document_pk AND r.is_active = ‘Y’
To validate hierarchy relationships, hierarchical queries were used:
CONNECT BY PRIOR au2.reports_to_fk = au2.app_user_pk
This allowed workflows to follow actual organizational reporting structures.
Handling Missing Approvers
Another practical challenge was incomplete workflow configurations.
Sometimes:
- role mappings were missing
- branch setup was incomplete
- no approver existed for a role
Instead of stopping the workflow completely, the system automatically performed self-approval.
IF v_approver IS NULL THEN v_approver := p_user_fk; v_status := ‘Y’; END IF;
This prevented workflow deadlocks in production environments.
Workflow Status Management
To manage runtime workflow execution, I implemented the package:
PKG_WF_APPROVAL_STATUS
This package handles:
- workflow status detection
- approval processing
- rejection handling
- return mechanism
- notification generation
The package contains two major components:
- GET_CURRENT_STATUS
- UPDATE_APPROVAL_STATUS
Here is the package specifications:

The complete package body is as below:
create or replace PACKAGE BODY PKG_WF_APPROVAL_STATUS AS
FUNCTION GET_CURRENT_STATUS (
P_PAGE_NO IN NUMBER,
P_TRANSACTION_ID IN NUMBER
) RETURN WF_STATUS_INFO
IS
TYPE t_step IS RECORD (
step_no NUMBER,
step_status VARCHAR2(10),
next_approver_fk NUMBER
);
TYPE t_step_table IS TABLE OF t_step INDEX BY PLS_INTEGER;
l_approvals t_step_table;
V_STATUS VARCHAR2(50) := ‘Pending’;
V_APPROVER_FK NUMBER := NULL;
BEGIN
— Load necessary step data only
SELECT STEP_NO, STEP_STATUS, NEXT_APPROVER_FK BULK COLLECT INTO l_approvals FROM WF_APPROVALS WHERE PAGE_NO = P_PAGE_NO AND TRANSACTION_FK = P_TRANSACTION_ID
ORDER BY STEP_NO;
IF l_approvals.COUNT = 0 THEN
RETURN WF_STATUS_INFO(‘No Workflow Defined’, NULL);
END IF;
FOR i IN 1 .. l_approvals.COUNT LOOP
IF l_approvals(i).step_status = ’N’ THEN
RETURN WF_STATUS_INFO(‘Rejected’, l_approvals(i).next_approver_fk);
END IF;
IF l_approvals(i).step_status IS NULL THEN
IF i = 1 THEN
RETURN WF_STATUS_INFO(‘Pending’, l_approvals(i).next_approver_fk);
ELSIF l_approvals(i — 1).step_status = ‘Y’ THEN
RETURN WF_STATUS_INFO(‘Pending’, l_approvals(i).next_approver_fk);
ELSE
RETURN WF_STATUS_INFO(‘Pending’, l_approvals(i).next_approver_fk);
END IF;
END IF;
END LOOP;
— If all steps are approved
RETURN WF_STATUS_INFO(‘Approved’, NULL);
END;
PROCEDURE UPDATE_APPROVAL_STATUS (
P_BRANCH_FK IN NUMBER,
P_PAGE_NO IN NUMBER,
P_TRANSACTION_FK IN NUMBER,
P_USER_FK IN NUMBER,
P_NEW_STATUS IN CHAR, — Expected values: ‘Y’, ’N’, ‘R’
P_REMARKS IN VARCHAR2) IS
V_STEP_NO WF_APPROVALS.STEP_NO%TYPE;
V_DOCUMENT_FK WF_DOCUMENTS.DOCUMENT_PK%TYPE;
V_DOCUMENT_OWNER_FK NUMBER;
V_DOCUMENT_OWNER_NAME VARCHAR2(100);
V_DOCUMENT_NAME VARCHAR2(200);
BEGIN
— GET DOCUMENT_FK
BEGIN
SELECT DOCUMENT_PK, DOCUMENT_NAME INTO V_DOCUMENT_FK, V_DOCUMENT_NAME FROM WF_DOCUMENTS, WF_WORKFLOW WHERE DOCUMENT_PK = DOCUMENT_FK AND BRANCH_FK = P_BRANCH_FK AND PAGE_NO = P_PAGE_NO;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN NULL;
END;
— GET ORIGINAL OWNER
BEGIN
SELECT DISTINCT CREATED_BY_FK INTO V_DOCUMENT_OWNER_FK FROM WF_APPROVALS WHERE DOCUMENT_FK = V_DOCUMENT_FK AND BRANCH_FK = P_BRANCH_FK AND PAGE_NO = P_PAGE_NO AND TRANSACTION_FK = P_TRANSACTION_FK;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN NULL;
END;
— DOCUMENT OWNER NAME
BEGIN
SELECT USER_NAME INTO V_DOCUMENT_OWNER_NAME FROM APP_USERS WHERE APP_USER_PK = V_DOCUMENT_OWNER_FK;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN NULL;
END;
IF P_NEW_STATUS = ‘R’ THEN
— If status is Returned, NULL all statuses for this document
UPDATE WF_APPROVALS
SET STEP_STATUS = NULL
WHERE PAGE_NO = P_PAGE_NO AND TRANSACTION_FK = P_TRANSACTION_FK;
— Record into History
INSERT INTO WF_APPROVAL_HISTORY(DOCUMENT_FK, BRANCH_FK, TRANSACTION_FK, PAGE_NO, APP_USER_FK, ACTION_TYPE, ACTION_DATE, REMARKS)
VALUES(V_DOCUMENT_FK, P_BRANCH_FK, P_TRANSACTION_FK,P_PAGE_NO, P_USER_FK, ‘Returned’, SYSDATE, P_REMARKS);
— Inform the document owner
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK,NOTIFICATION_TITLE,DUE_DATE,DESCR,IS_READ,NOTIFICATION_DATE,PAGE_NO,TRANSACTION_FK,NOTIFICATION_TYPE,NOTIFICATION_CATEGORY)
VALUES(V_DOCUMENT_OWNER_FK,‘Workflow Approval’,SYSDATE,‘Your document is returned back for clarifications / modifications.’ || ‘Document Name: ‘ || V_DOCUMENT_NAME || ‘ User Name: ‘ ||V_DOCUMENT_OWNER_NAME,‘N’,SYSDATE,P_PAGE_NO,P_TRANSACTION_FK,‘Warning’,‘W’);
— Also inform the first approver
— ============= NEXT APPROVER ==============================
DECLARE
V_INFO WF_STATUS_INFO;
V_NEXT_APPROVER NUMBER;
BEGIN
V_INFO := PKG_WF_APPROVAL_STATUS.GET_CURRENT_STATUS(P_PAGE_NO => P_PAGE_NO, P_TRANSACTION_ID => p_transaction_fk);
V_NEXT_APPROVER:= V_INFO.APPROVER_FK;
if V_NEXT_APPROVER is not null then
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK,NOTIFICATION_TITLE,DUE_DATE,DESCR,IS_READ,NOTIFICATION_DATE,PAGE_NO,TRANSACTION_FK,NOTIFICATION_TYPE,NOTIFICATION_CATEGORY)
VALUES(V_NEXT_APPROVER,‘Workflow Approval’,SYSDATE,‘An old document is reversed and required your re-approval.’ || ‘ DocumentName: ‘ || V_DOCUMENT_NAME || ‘ User Name: ‘ ||V_DOCUMENT_OWNER_NAME,‘N’,SYSDATE,P_PAGE_NO,P_TRANSACTION_FK,‘Info’,‘W’);
end if;
END;
ELSIF P_NEW_STATUS IN (‘Y’) THEN
— Only update the record for the approver
UPDATE WF_APPROVALS
SET STEP_STATUS = P_NEW_STATUS, REMARKS = P_REMARKS
WHERE PAGE_NO = P_PAGE_NO AND TRANSACTION_FK = P_TRANSACTION_FK AND NEXT_APPROVER_FK = P_USER_FK;
— Check and notify next approver (if exists)
DECLARE
V_INFO WF_STATUS_INFO;
V_NEXT_APPROVER NUMBER;
BEGIN
V_INFO := PKG_WF_APPROVAL_STATUS.GET_CURRENT_STATUS(P_PAGE_NO => P_PAGE_NO, P_TRANSACTION_ID => p_transaction_fk);
V_NEXT_APPROVER:= V_INFO.APPROVER_FK;
if V_NEXT_APPROVER is not null then
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK,NOTIFICATION_TITLE,DUE_DATE,DESCR,IS_READ,NOTIFICATION_DATE,PAGE_NO,TRANSACTION_FK,NOTIFICATION_TYPE,NOTIFICATION_CATEGORY)VALUES(V_NEXT_APPROVER,‘Workflow Approval’,SYSDATE,‘A document is forwarded to you and needs your approval.’ || ‘ DocumentName: ‘ || V_DOCUMENT_NAME || ‘ User Name: ‘ ||V_DOCUMENT_OWNER_NAME,‘N’,SYSDATE,P_PAGE_NO,P_TRANSACTION_FK,‘Info’,‘W’);
elsif V_NEXT_APPROVER is null then
— Inform the document owner that the document is fully approved
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK,NOTIFICATION_TITLE,DUE_DATE,DESCR,IS_READ,NOTIFICATION_DATE,PAGE_NO,TRANSACTION_FK,NOTIFICATION_TYPE,NOTIFICATION_CATEGORY)VALUES(V_DOCUMENT_OWNER_FK,‘Workflow Approval (Completed)’,SYSDATE,
‘Congradulations! Your document is approved.’ || ‘ Document Name: ‘ ||V_DOCUMENT_NAME || ‘ User Name: ‘ || V_DOCUMENT_OWNER_NAME,‘N’,SYSDATE,P_PAGE_NO,P_TRANSACTION_FK,‘Success’,‘W’);
end if;
END;
ELSIF P_NEW_STATUS IN (’N’) THEN
— Only update the record for the approver
UPDATE WF_APPROVALS
SET STEP_STATUS = P_NEW_STATUS, REMARKS = P_REMARKS
WHERE PAGE_NO = P_PAGE_NO AND TRANSACTION_FK = P_TRANSACTION_FK AND NEXT_APPROVER_FK = P_USER_FK;
REJECT_DOCUMENT(L_PAGE_NO => P_PAGE_NO, L_TRANSACTION_FK => P_TRANSACTION_FK, L_DOCUMENT_FK => V_DOCUMENT_FK);
— Notify document owner that the document is rejected
INSERT INTO FND_NOTIFICATIONS(APP_USER_FK, NOTIFICATION_TITLE, DUE_DATE, DESCR, IS_READ, NOTIFICATION_DATE, PAGE_NO, TRANSACTION_FK, NOTIFICATION_TYPE, NOTIFICATION_CATEGORY)
VALUES(V_DOCUMENT_OWNER_FK, ‘Workflow Approval (Rejected)’, SYSDATE, ‘Sorry! Your document is rejected.’ || ‘ Document Name: ‘ || V_DOCUMENT_NAME || ‘ User Name: ‘ || V_DOCUMENT_OWNER_NAME, ’N’, SYSDATE, P_PAGE_NO, P_TRANSACTION_FK, ‘Danger’, ‘W’);
ELSE
RAISE_APPLICATION_ERROR(-20001, ‘Invalid status value. Must be Y, N, or R.’);
END IF;
END UPDATE_APPROVAL_STATUS;
END PKG_WF_APPROVAL_STATUS;
/
Integrated Notification Engine
A workflow system is incomplete without proper user notifications.
To solve this, I implemented a centralized notification table:
FND_NOTIFICATIONS

Here is the sample data generated through various documents for approval:

Whenever workflow status changes, notifications are automatically generated.
Examples include:
- New approval requests
- Workflow returns
- Rejections
- Final approvals
Typical notification message:
A document is forwarded to you and needs your approval.
Oracle APEX Integration
The workflow engine was tightly integrated with Oracle APEX.
The application includes:
- Pending approval dashboards
- Approval history regions
- Notification badges
- Dynamic approval buttons
- Workflow timelines
- Approval summary reports
Because the workflow logic is centralized, all ERP modules share the same workflow infrastructure.
Challenges Faced During Implementation
The technical implementation was not the difficult part.
The real challenge was handling real organizational scenarios.
Some examples included:
- users holding multiple roles
- circular reporting hierarchies
- duplicate approvers
- branch-specific workflows
- missing role assignments
- workflow resets after returns
- executive auto-approvals
Initially, workflows were directly mapped to users, but maintaining organizational changes quickly became difficult.
Moving to a role-based workflow architecture solved most of these problems and made the system significantly more scalable.
Performance Considerations
As workflow volume increased, performance optimization became important.
Several optimizations were implemented:
- BULK COLLECT usage
- centralized status evaluation
- reusable PL/SQL packages
- workflow cleanup logic
- reduced repeated queries
- metadata-driven configuration
These improvements allowed the workflow engine to scale across multiple ERP modules without performance degradation.
Conclusion
Building a dynamic workflow engine in Oracle APEX requires much more than adding approval buttons and status columns. The real complexity lies in creating a flexible routing system capable of adapting to organizational changes, reporting hierarchies, and evolving business requirements.
The metadata-driven approach significantly reduced workflow maintenance effort across the ERP system and provided a centralized approval framework reusable across multiple modules.
The biggest lesson learned during this implementation was that workflow systems should never be tightly coupled with individual screens or modules. Once workflows are designed as reusable infrastructure, extending ERP functionality becomes much easier and significantly more maintainable.
For organizations building enterprise applications in Oracle APEX, investing time in designing a scalable workflow architecture early can save a tremendous amount of future maintenance effort.
This workflow engine is currently being used across multiple ERP modules including procurement, HR, finance, inventory, and hospital management systems.
In upcoming parts of this series, I will explain the Oracle APEX implementation screens, approval dashboards, notification center, and practical UI techniques used to make the workflow fully manageable from the application front-end.
메타데이터
- post_id
- fef3f5cb2de7
- slug
- building-a-dynamic-multi-level-approval-workflow-engine-in-oracle-apex-24-2-part-1-fef3f5cb2de7
- url
- https://medium.com/@mkhaleeq2025/building-a-dynamic-multi-level-approval-workflow-engine-in-oracle-apex-24-2-part-1-fef3f5cb2de7
- canonical_url
- https://medium.com/@mkhaleeq2025/building-a-dynamic-multi-level-approval-workflow-engine-in-oracle-apex-24-2-part-1-fef3f5cb2de7
- author_url
- https://medium.com/@mkhaleeq2025
- status
- ok
- fetched_at
- 2026-06-24 11:06:28