How to Build Your First Oracle APEX Plugin from Scratch (With Real Code Examples)

Learn to build your first Oracle APEX plugin from scratch.

You just spent three days trying to make a native APEX item do something it clearly was not designed for. You are Googling, checking forums, and every answer ends with “you could use a plugin for that.” But where do you even start?

Most plugin tutorials skip the fundamentals. They throw you into code you do not understand. They assume you already know what a callback function is or how APEX passes parameters to your PL/SQL procedure.

This guide walks you through building your first Oracle APEX plugin from absolute scratch. No assumptions. No skipped steps. You will understand the plugin architecture, learn what each file does, and build a small item plug-in whose rendering, configuration and client-side behavior you can inspect and test.

By the end of this article, you will know exactly how to extend APEX when built-in features are not enough.

What Exactly Is an Oracle APEX Plugin?

An Oracle APEX plugin is a packaged extension that adds functionality not available in standard APEX components. Think of it as a self-contained module you build once and reuse everywhere.

Plugins live in Shared Components. You configure them declaratively. You can export them as SQL scripts and share them across applications, workspaces, or even with the entire Oracle community.

Why Plugins Exist (When Built-In Features Are Not Enough)

APEX gives you a solid foundation. Interactive grids, dynamic actions, charts, forms. But sometimes you hit a wall.

Maybe you need a signature capture field. Maybe you need coordinate-aware image zooming. Maybe you need a custom date picker that handles Islamic calendar dates. Native APEX does not support these out of the box.

You have two options. Write page-level JavaScript and PL/SQL that you copy-paste into every application. Or build a plugin once and make it reusable.

According to Jeff Kemp’s plugin guide, plugins offer a write-once approach that makes them atomic, loosely coupled, and easier to document. They integrate directly into the APEX Builder interface with help text, configuration attributes, and version control support.

When I built the BioSig Pro signature plugin, I needed digital signatures in an enterprise app. Native APEX had nothing. I could have written custom JavaScript on every page. Instead, I built a plugin. Now any developer in my organization can drag it onto a page and configure it in 30 seconds.

The 8 Types of APEX Plugins You Can Build

APEX supports eight distinct plugin types. Each one extends a different part of the platform.

Item plugins create custom form fields. These replace or enhance standard items like text fields, select lists, or date pickers. Think sliders, color pickers, or the signature capture fields in BioSig Pro.

Region plugins render entire page sections with custom logic. I built an image zoom plugin with coordinate-aware panning as a region plugin because users needed to interact with an entire image container, not just a single form field.

Dynamic Action plugins extend APEX’s declarative event handling. Instead of writing jQuery on every page, you package the behavior into a reusable plugin that developers configure through the Builder.

Process plugins create reusable code blocks that run during page processing. Think API calls, batch operations, or file uploads that you need across multiple applications.

Authentication Scheme plugins implement custom login mechanisms. LDAP integration, SAML single sign-on, or proprietary authentication systems.

Authorization Scheme plugins control page and component access. You might need role-based access that checks an external API instead of database tables.

REST Data Source plugins let you implement custom REST connectors when APEX’s standard REST integration does not handle your specific API authentication or response format.

Template Component plug-ins, introduced in APEX 23.1, package reusable UI templates for regions and report components. Web Source Modules is the older name for REST Data Sources, not an additional plug-in type introduced in 23.1.

For a comprehensive breakdown of plugin types, the Rittman Mead team covers each one with use cases and architecture considerations.

How Plugins Differ from Page-Level JavaScript

Page-level JavaScript runs every time APEX renders that specific page. You write it in the page attributes. It stays tied to that one page in that one application.

Plugins package your code into a Shared Component. Other developers configure them declaratively. They do not need to understand your JavaScript or PL/SQL. They just set attributes in the Builder.

Centralizing the code in a plug-in makes a release easier to manage. Components in the same application use that application’s shared plug-in definition. Copies in other applications do not update themselves merely because you changed the source: deploy the new export or use the supported subscription refresh workflow, and keep an explicit version number. Page-level code can also be versioned in source control.

Understanding Plugin Architecture Before You Build

Every Oracle APEX plugin has three core pieces. PL/SQL callback functions. JavaScript files. CSS files. APEX orchestrates how these pieces work together.

The Three Core Components (PL/SQL Callback, JavaScript Files, CSS Files)

PL/SQL callback functions execute on the server during page rendering. APEX calls your render function and expects you to return HTML, set session state, or configure JavaScript callbacks. For item plugins, you might also implement a validation function or a meta_data function.

JavaScript files handle client-side interactions and, when needed, AJAX calls. Upload files to the plug-in, then list each required file under File URLs to Load. Uploading a file alone does not cause the browser to load it.

CSS files style your plug-in’s output. Upload and reference them in the CSS File URLs to Load setting when the plug-in needs a stylesheet.

Not every plugin needs all three. A simple process plugin might only have PL/SQL. A UI-heavy region plugin needs all three working in sync.

How APEX Calls Your Plugin (The Rendering Flow)

When APEX renders a page containing your plugin, it follows a specific sequence.

First, APEX calls your render function during page generation. APEX passes a p_item (for item plugins) or p_region (for region plugins) record containing all the configuration attributes the developer set in the Builder.

Your render callback generates HTML using htp.p() or sys.htp.prn(). For the item plug-in below, the loaded JavaScript file registers apex.item.addAttachHandler to initialize the input. The javascript_function result field belongs to dynamic-action plug-ins, not to the item render result.

Second, APEX includes your attached JavaScript and CSS files in the page head. The browser downloads them and executes your client-side logic.

Third, if the user interacts with your plugin and triggers an AJAX callback, APEX calls your ajax function (if you defined one). This lets you fetch data, validate input, or update session state without a full page refresh.

According to Oracle’s official plugin documentation, understanding this render-ajax cycle is critical. Your PL/SQL and JavaScript need to communicate through the apex_plugin API properly or the plugin breaks in production.

Plugin Attributes vs. Application Items

Plugin attributes are configuration options developers set when they use your plugin. Think of them as parameters.

Standard attributes are defined by APEX. For item plugins, this includes Label, Help Text, Width, and Read Only settings. You get these automatically.

Custom attributes are configuration options you define. Attribute limits and access syntax depend on the plug-in type and API interface. New APEX 24.2 item plug-ins using the Procedure interface support named attributes without the older fixed component-attribute limit. This example reads attributes by their Static ID, such as p_item.attributes.get_number('max_length', 500). Do not mix this with a legacy Function-interface example that reads attribute_01.

For example, BioSig Pro has custom attributes for pen color, line width, and whether to show a clear button. Developers set these values declaratively without touching code.

Application items are session-level variables that persist across pages. Plugins do not replace them. You might use plugin attributes to configure behavior and application items to store the plugin’s output values.

Building Your First Item Plugin Step by Step

Let’s build a simple item plugin. A Text Length Counter that shows remaining characters as users type. It will display “45 characters remaining” below a text field and update in real-time.

Version and scope: the corrected example below targets the APEX 24.2 Item plug-in Procedure interface and ordinary VARCHAR2 page items. It is not an Interactive Grid column item, a CLOB item, or an AJAX plug-in. Its APIs have been checked against Oracle's documentation, and the JavaScript has a small mocked behavior test; the PL/SQL has not been compiled or imported into an APEX workspace as part of this correction. Run the verification checklist below before deploying it.

Setting Up the Plugin Definition in Shared Components

Navigate to Shared Components in your APEX application. Under Other Components, click Plug-ins, then Create and From Scratch.

Give your plugin a name: Text Length Counter. Set the internal name to com.yourcompany.text_length_counter. Oracle recommends namespacing internal names with a reverse domain to avoid conflicts when sharing plugins.

Set the Type to Item. This tells APEX you are building a form field plugin.

Under Source, you will define your PL/SQL callback functions. Leave this blank for now. We will write the render function next.

Under Standard Attributes, enable Is Visible Widget, Standard Form Element and Session State Changeable. Also enable Has Read Only Attribute, Has Source Attributes and Has Placeholder Attribute, which the renderer below implements. Do not enable width, icon or arbitrary element-attribute options unless you extend the renderer to honor them. Leave Session State Supports CLOB disabled: this example is a VARCHAR2 page item, even though it uses a CLOB-safe output API for escaped HTML.

Set API Interface to Procedure. Configure the plug-in for visible page items with an HTML element; do not enable Interactive Grid column use for this example.

Create these three component-level custom attributes before adding the render code:
– Max Length: Static ID max_length, type Integer, default 500. Use integers from 1 through 32767; 0 is not a disable switch.
– Show Counter: Static ID show_counter, type Yes/No, default Yes.
– Counter Color: Static ID counter_color, type Color (the picker), default #666666. This example accepts six-digit hex colors only.

Keep the exact Static IDs: the PL/SQL reads them by name.

Writing the PL/SQL Render Function

In the plug-in’s callback settings, set the render callback name to render_text_length_counter. Paste the complete procedure below into the plug-in PL/SQL source. Use the Procedure API interface selected above; do not add a RETURN type or a SQL*Plus slash to this inline source.

Now write the actual PL/SQL. You can write it inline in the plugin definition or create a package and reference it. For this example, inline is fine.

procedure render_text_length_counter (
    p_item   in            apex_plugin.t_item,
    p_plugin in            apex_plugin.t_plugin,
    p_param  in            apex_plugin.t_item_render_param,
    p_result in out nocopy apex_plugin.t_item_render_result
)
is
    l_max_length   number := p_item.attributes.get_number('max_length', 500);
    l_show_counter boolean := p_item.attributes.get_boolean('show_counter', true);
    l_color        varchar2(32767) := p_item.attributes.get_varchar2('counter_color', '#666666');
    l_input_name   varchar2(32767);
    l_counter_id   varchar2(4000) := p_item.name || '_counter';
    l_described_by varchar2(32767) := p_item.inline_help_id;

    function attr(p_value in varchar2) return varchar2 is
    begin
        return apex_escape.html_attribute(p_value);
    end;
begin
    if l_max_length is null or l_max_length <> trunc(l_max_length)
       or l_max_length < 1 or l_max_length > 32767 then
        raise_application_error(-20001, 'Max Length must be an integer from 1 to 32767.');
    end if;

    -- Accept only a six-digit hex color, not arbitrary CSS or HTML.
    if l_color is null or not regexp_like(l_color, '^#[0-9A-Fa-f]{6}$') then
        l_color := '#666666';
    end if;

    if p_param.is_readonly or p_param.is_printer_friendly then
        apex_plugin_util.print_read_only(
            p_item          => p_item,
            p_param         => p_param,
            p_display_value => p_param.value,
            p_escape        => true
        );
        p_result.is_navigable := false;
        return;
    end if;

    -- Supported in the APEX 24.2 target of this example, but deprecated.
    -- Re-check the replacement API before porting to a later APEX release.
    l_input_name := apex_plugin.get_input_name_for_page_item(false);

    if l_show_counter then
        l_described_by := trim(l_described_by || ' ' || l_counter_id);
    end if;

    sys.htp.prn('<input type="text" class="text_field apex-item-text owh-text-counter-input"');
    sys.htp.prn(' id="' || attr(p_item.name) || '"');
    sys.htp.prn(' name="' || attr(l_input_name) || '"');
    -- Escaping may expand a VARCHAR2 value beyond 32767 bytes.
    -- Stream the escaped attribute as a CLOB instead of concatenating it.
    sys.htp.prn(' value="');
    if p_param.value is not null then
        apex_util.prn(
            p_clob   => apex_escape.html_attribute_clob(to_clob(p_param.value)),
            p_escape => false
        );
    end if;
    sys.htp.prn('"');
    sys.htp.prn(' maxlength="' || to_char(l_max_length, 'FM99999990') || '"');

    if l_described_by is not null then
        sys.htp.prn(' aria-describedby="' || attr(l_described_by) || '"');
    end if;
    if p_item.is_required then
        sys.htp.prn(' required aria-required="true"');
    end if;
    if p_item.placeholder is not null then
        sys.htp.prn(' placeholder="' || attr(p_item.placeholder) || '"');
    end if;
    sys.htp.prn('>');

    if l_show_counter then
        sys.htp.prn('<div class="owh-text-counter" id="' || attr(l_counter_id) || '"');
        sys.htp.prn(' style="color:' || attr(l_color) || '"></div>');
    end if;

    p_result.is_navigable := true;
    -- The JS file registers an apex.item.addAttachHandler; no result JS field exists.
end render_text_length_counter;

The input id is the APEX item name, for example P1_NOTE. The HTML name is the submission mapping returned by APEX; these are different responsibilities. The renderer escapes attribute values, validates its settings, and uses Oracle’s read-only renderer for read-only and printer-friendly requests.

The mapping API used here, GET_INPUT_NAME_FOR_PAGE_ITEM, is documented but deprecated in APEX 24.2. Treat 24.2 as this example’s compatibility target and check the replacement API before porting it to a later release. No javascript_function or ajax_identifier is assigned to t_item_render_result: neither field exists on that record.

Provide defaults for optional settings and validate the values that remain. The named getters supply defaults in this example, while the positive-integer check prevents an invalid maxlength setting.

For more on avoiding common PL/SQL mistakes junior developers make, I wrote a detailed breakdown of the errors that cause production bugs.

Adding Client-Side JavaScript for Interactivity

Your PL/SQL generated the HTML. Now you need JavaScript to make the counter update as users type.

Create a file named text-counter.js containing the JavaScript below and upload it in the plug-in’s Files section. Then set File URLs to Load → JavaScript to:

#PLUGIN_FILES#text-counter.js

This URL setting is required. Uploading the file without referencing it will leave the counter uninitialized.

(function (apex, $) {
    "use strict";

    function attachTextCounters(context$) {
        $("input.owh-text-counter-input", context$).each(function () {
            var input = this;
            var counter = document.getElementById(input.id + "_counter");

            function updateCounter() {
                if (counter) {
                    // Matches HTML maxlength: UTF-16 code units, not grapheme clusters.
                    counter.textContent = (input.maxLength - input.value.length) +
                        " characters remaining";
                }
            }

            $(input).off(".owhTextCounter")
                .on("input.owhTextCounter change.owhTextCounter", updateCounter);

            apex.item.create(input.id, {
                item_type: "OWH_TEXT_LENGTH_COUNTER",
                setValue: function (value) {
                    input.value = value == null ? "" : String(value);
                    updateCounter();
                    // APEX triggers change unless the caller suppresses it.
                }
            });
            updateCounter();
        });
    }

    apex.item.addAttachHandler(attachTextCounters);
})(apex, apex.jQuery);

The file registers an APEX item attach handler. Each input gets its own counter and namespaced event handlers, so initializing two items does not attach the same handler repeatedly. apex.item.create integrates the field with APEX; its setter refreshes the counter even when apex.item('P1_NOTE').setValue(…) suppresses the change event.

The count matches HTML maxlength and JavaScript string length: UTF-16 code units, not user-perceived characters. Some emoji count as two units. This browser limit is a usability aid, not a security boundary. Enforce your actual length/byte policy in server-side validation and database constraints too.

When I built BioSig Pro, the signature capture plugin, I initially tried to handle touch events the same way as mouse events. That broke completely on mobile devices. The fix required detecting touch vs mouse input and normalizing the coordinates differently for each. The lesson: always test your JavaScript on mobile Safari and Chrome, not just desktop browsers.

Defining Custom Attributes Users Can Configure

All three settings are defined in the setup above and handled in the complete renderer. To test them, create a page item named P1_NOTE using Text Length Counter. Use a normal label template, set Max Length to 500 and Show Counter to Yes, then run the page. Add a second item to confirm that each counter belongs to its own input.

Switch Show Counter to No to hide only the counter. The input and its maxlength remain. Changing Counter Color affects only this plug-in’s counter. Keep configuration in the component attributes instead of editing the source for each item.

What Are the Most Common Plugin Development Mistakes?

I have built multiple production plugins. BioSig Pro. Image Zoom. Custom validation frameworks. Every time, I hit the same mistakes early in development.

Forgetting to Return the Correct Result Type

Callback signatures depend on both plug-in type and API interface. The APEX 24.2 item example above is a procedure that fills an IN OUT t_item_render_result; it does not return that record as a function result. Legacy item functions and dynamic-action callbacks have different contracts. Match the documentation and the API Interface setting exactly.

A mismatched callback or nonexistent result field can fail compilation or rendering. Use the actual compiler/debug error and the documented signature to diagnose it; there is no single guaranteed error message for every mismatch.

Check the dynamic action plugin callbacks documentation from Rittman Mead. They break down the exact return structure for each plugin type.

Not Handling NULL Values in Attributes

An unset attribute may be NULL. Use a default and validate it against the intended range. For this Procedure-interface example:

l_max_length := p_item.attributes.get_number('max_length', 500);

The complete renderer then rejects zero, negative, fractional or overlarge values.

Skipping the Ajax Identifier

This counter has no AJAX callback and needs no AJAX identifier. For an item plug-in that does need server calls, obtain APEX_PLUGIN.GET_AJAX_IDENTIFIER on the server and pass it safely to that item’s JavaScript, then use apex.server.plugin. Do not assign ajax_identifier to t_item_render_result: that record has no such field. Authenticate and authorize any server operation independently of the client-side identifier.

Hardcoding Values Instead of Using Substitution Strings

Never hardcode application IDs, page numbers, or item names in your plugin code. Use APEX substitution strings like &APP_ID. and &APP_PAGE_ID. or reference items dynamically through the plugin API.

Hardcoded values break when you export the plugin to another application. Substitution strings adapt automatically.

How Do You Test and Debug APEX Plugins?

Testing plugins is harder than testing regular APEX pages because you are working with multiple layers. PL/SQL callbacks. JavaScript execution. CSS rendering. AJAX communication.

Using Browser DevTools to Inspect Plugin Output

Open Chrome DevTools (F12) and inspect the HTML your render function generated. Does it match what you expected? Are the CSS classes applied correctly? Is the JavaScript firing on the right events?

In the Console tab, check for JavaScript errors. A typo in your function name will throw Uncaught ReferenceError: initTextCounter is not defined. Fix it in your uploaded JS file and refresh.

In the Network tab, watch AJAX requests when your plugin calls back to the server. Is APEX receiving the correct parameters? Is your ajax function returning valid JSON?

APEX Debug Mode for PL/SQL Callbacks

Enable Debug mode in the APEX Developer Toolbar. Run your page. Check the debug output for your plugin’s render function.

Look for these lines:

...execute plug-in: item="P1_TEXT_COUNTER" type="com.yourcompany.text_length_counter"

If you see errors about invalid return types or missing attributes, your PL/SQL callback has a bug. The debug log shows the exact line that failed.

For production applications, I also use a centralized error logging framework for application-specific context and retention. APEX_DEBUG messages do not disappear when the user logs out, and APEX_DEBUG.ERROR can log even when debug mode is off. A custom logger is an additional operational choice, not a requirement caused by logout deleting debug data.

Testing Across Different APEX Versions

Plugins built for APEX 21.2 might break in APEX 23.1 if Oracle changed the plugin API. Always test your plugin in the earliest APEX version you plan to support.

If you are sharing the plugin publicly, document the minimum APEX version in the help text. Developers will waste hours debugging compatibility issues if you do not warn them.

Making Your Plugin Production-Ready

Before calling this plug-in ready for your application, verify it in your APEX 24.2 development workspace:
1. Save/compile the plug-in source without errors and confirm text-counter.js loads successfully in the browser Network panel.
2. Test an empty value, a prefilled value, two instances, paste, deletion and an emoji. Confirm each counter changes independently.
3. Set the value through apex.item('P1_NOTE').setValue('abc', null, true); the counter must still update.
4. Submit the page and verify P1_NOTE in session state. Test required and server-side length validation; do not rely only on maxlength.
5. Check the label, keyboard focus, read-only rendering, printer-friendly rendering and the Show Counter option.
6. Export and import into a disposable application on the same version, then repeat the checks. Record the result and version before adopting it elsewhere.

Adding Help Text and Documentation

In the plugin definition, every custom attribute has a Help Text field. Fill it in. Explain what the attribute does, what values are valid, and what happens if the developer leaves it blank.

For example:

Max Length – Browser input limit in UTF-16 code units. Defaults to 500; valid values are integers from 1 through 32767. Zero is not supported. Server-side and database validation must enforce the application’s storage and business rules.

Under the main plugin settings, add Help Text that explains how to use the plugin, what dependencies it has, and any known limitations.

Good documentation means other developers can use your plugin without asking you questions. It also means you will remember how it works when you revisit the code six months later.

Versioning Your Plugin Properly

Set a Version number in the plugin definition. Use semantic versioning: 1.0.0 for initial release, 1.1.0 for new features, 2.0.0 for breaking changes.

Update the version every time you modify the plugin. This helps you track which applications are running old versions and need updates.

Exporting and Sharing Your Plugin

In Shared Components, click Export next to your plugin. APEX generates a SQL script containing all the plugin code, attributes, and file attachments.

You can import this script into any other APEX application. You can share it on apex.world, the community plugin repository. You can publish it on GitHub.

Before sharing publicly, review your code for hardcoded credentials, database schema names, or internal business logic. Clean it up. Make variable names generic.

When I open-sourced BioSig Pro, I stripped out all references to our internal schema names and replaced them with configurable attributes. That made the plugin usable by anyone without modifications.

For quality assurance, run the APEX Advisor against your application. According to APEX performance best practices, Advisor catches programming errors, security vulnerabilities, and usability issues automatically. Fix any warnings before you export.

Start Building Your Own Plugins

You now know how plugin architecture works. You understand how APEX calls your PL/SQL render function and passes control to your JavaScript. You know the difference between custom attributes and application items. You have a complete, version-scoped example to compile and test in a development workspace.

The skills you learned here apply to every plugin type. Region plugins use the same render-ajax pattern. Dynamic action plugins follow the same attribute system. Process plugins still need proper error handling and NULL checks.

The best way to learn is to build. Start simple. Build a plugin that solves one specific problem in your application. Add features incrementally. Test in different browsers. Document your code.

Want to see a real production plugin in action? Check out BioSig Pro, the signature capture plugin I built and open-sourced. Study the code. See how it handles mobile touch events and saves signature data to the database. Use it as a template for your own plugins.

Need a development environment to practice? A free evaluation workspace on apex.oracle.com and an Oracle Cloud Free Tier account are different signup paths. Use the Free Tier setup guide for the OCI route and check Oracle’s current account-verification requirements before signing up.

Got questions about plugin development? Drop them in the comments. I read every one.

Hassan Raza
An Oracle ACE Associate and Senior Oracle Application Developer at S&H Software Solution. I am specialized in Oracle APEX, SQL, and PL/SQL and writes about Oracle development at oraclewithhassan.com

Official references for the corrected examples

YOU MAY ALSO LIKE

1 Comment

Comments are closed.