← Back to list

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…

M Khaleeq Ahmed · 2026-05-15 15:16 · 0 claps · 11.6 min read
#oracle-apex #low-code #oracle-database #workflow-automation #ord
Open on Medium ↗

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