Learn how to use all features of the Oracle APEX platform
My major “feature” for APEX 26.1 was writing a book to help devs understand and use the full breadth of functionality Oracle APEX provides. This post on the official APEX blog gives a short overview of all 25 chapters. Download the PDF version from the APEX books page for offline use.
Hope the resource is useful to you, to your teammates, or to anyone you know who may be trying to be successful with Oracle APEX. It is packed with screenshots and figures to try and help you focus on following the ideas of each topic rather than on trying to reproduce the result click by click.
P. S. The title of the book is a nod to my late father who in 1993 suggested the title of my first book Oracle: Forms Developer’s Companion. Dad thought “Companion” sounded like a friend who was there to help you be successful, so it felt appropriate since that was my goal with the new book, too.
Use apex validate to find and fix inconsistencies in mature apps
One Small Step with APEXlang introduced apex validate, the new way to check the integrity of your app metadata. Apps created in recent APEX releases will likely pass without issues. Mature apps, however, may contain inconsistencies that are now easy to find and fix. This article walks through four examples, their likely causes, and how to address them. Along the way, you’ll learn an approach for triaging other kinds of metadata issues, too.
NOTE: To see any image at full size, right click it and open it in a new tab.
Discrepancies Dispatched Automatically
On import and instance upgrade, APEX fixes a number of common issues that would fail the strict metadata verification apex validate performs. We studied a large internal APEX instance with over 50,000 apps. Some apps there have been in service for over 15 years. This helped us identify and classify typical real world cases. We proactively repair the ones we can reliably identify and fix unambiguously.
Among these automatic repairs, we remove illegal…
list items for non-static lists
LOV entries for non-static LOVs
dynamic action steps configured as False actions when event handler has no client-side condition
A handful of wizard-generated incorrect template options
tabform DML page processes on pages with no tabform region
grid DML page processes pages with no grid region
We clear a validation’s associated item when it illegally references a page item on a different page that’s not the global page.
We also fix bad button ID references when the incorrect button on another, non-global page has same button name as the intended button on the current page. We correct the following situations:
When Button Pressed conditions on page processes, branches, validations, and
Triggering button for dynamic action events
When possible, the fix updates the wrong reference to be the ID of the intended button on the same page. When the current page has no button with the same button name, we convert the When Button Pressed condition into the equivalent “RESULT = BUTTON_NAME” condition, but only if the element is not already using its Server-side Condition for another purpose.
You can diagnose and address any inconsistencies that may remain using the techniques explained in the rest of this article.
Validating Your App Using SQLcl
Start by connecting SQLcl to the parsing schema of your workspace. Here I’m using a named connection to connect, but you can connect any way you like.
% sql -name diveintoapex
SQLcl: Release 26.1 Production on Sun May 17 15:32:25 2026
Copyright (c) 1982, 2026, Oracle. All rights reserved.
Finally, use apex validate to verify the app. Use the ‑input argument and specify the relative path to the directory just created by the export. It’s named hr-app in my example after the app alias. If you downloaded the APEXlang from APEX Builder, you can also specify a path to a zip file. The spool command just before and after apex validate lets you save any errors in a log file to review later.
This gives you an hr-app.log text file containing all the errors to investigate.
Validating Your App Using APEX Builder
Alternatively, you can use APEX Builder to export your app in APEXlang format, and then try to import it again. As shown below, if there are any validation errors, you can browse and click the Download link to get an apexlang‑errors.csv file containing all the errors.
Reviewing & Downloading APEXlang Validation Errors During an App Import Attempt
Fix Either in APEX Builder, or in APEXlang
As we explore four metadata mismatch examples, keep in mind that you can apply the fixes either:
Using the APEX Builder, then re-exporting in APEXlang to re-validate, or
Directly in the APEXlang files.
To use the APEX Builder approach, either your existing app was already in your workspace prior to the APEX 26.1 upgrade, or you can import it using your latest copy of its SQL format export.
Wrong Refs After Page Copy in Old Releases
Older APEX releases had a page-copy bug. Builder cloned the page, but button references in the copied page could still point to the original page. The bug has been fixed for several years now, but apps in service for longer may still carry some incorrect references. The figure below illustrates the problem.
Illustration of Incorrect Button Reference After Page Copy in Older APEX Builder Versions
This problem can involve page processes, branches, and validations with When Button Pressed conditions, as well as dynamic actions that were triggered by a button click. While button-triggered events did not fire at runtime in this situation, the other features did work correctly. Why?
When evaluating the When Button Pressed condition, APEX runtime checks the RESULT system variable for the name of the button, rather than relying on the button ID. This explains why this inconsistency could go unnoticed.
In the app we’re studying, the issue surfaces as the following APEXlang compiler error:
File: pages/p00005-manager.apx
Line: 448
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Reference not found: @/begin-self-eval
If we open p00005‑manager.apx in VS Code, we can press (Ctrl)-G and type in 448 followed by [Enter] to jump to that line. It’s an Initiate Self-Evaluation page process triggered by a button with identifier begin‑self‑eval. Since the incorrectly referenced button was on a different page, its reference uses the @/ prefix: @/begin‑self‑eval. However, we don’t find any button named begin‑self‑eval in this page.
Investigating the “Reference not found” Error in VS Code
Searching the file for a button with -eval at the end of the name, we notice the button got renamed after the original page copy to INITIATE_SELF_EVAL with the initiate-self-eval identifier.
Searching the Page for the Intended Button Name to Trigger the Page Process
To fix the problem in APEXlang directly, we just need to modify @/begin‑self‑eval to be @initiate‑self‑eval, changing the referenced button name and removing the slash.
Once we know the issue relates to the Initiate Self-Evaluation page process in page 5, we can alternatively study and fix the problem in the APEX Builder using Page Designer. The Property Editor shows the value of the When Button Pressed property as 41870290932227811 (Invalid). That long number is the internal ID of the incorrect button reference due to the page copy bug from a long time ago.
Invalid Button Reference Displays ID and “(Invalid)” in Property Editor
Fixing the problem in Page Designer is just as easy. As shown below, choose the INITIATE_SELF_EVAL button name from the list and save the page.
Fixing an Invalid When Button Pressed Reference in Page Designer
Orphaned Authorization Scheme Refs
Another common metadata mismatch involves components referencing an authorization scheme that no longer exists. This is the case for all of the REFERENCE_NOT_FOUND errors below.
File: shared-components/lists.apx
Line: 83
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Value not found: 44263289337994936
File: shared-components/lists.apx
Line: 180
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Value not found: 44263289337994936
File: pages/p01000-administration.apx
Line: 13
Column: 8
Type: REFERENCE_NOT_FOUND
Error: Value not found: 44263289337994936
File: pages/p01000-administration.apx
Line: 95
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Value not found: 44263289337994936
File: pages/p01000-administration.apx
Line: 116
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Value not found: 44263289337994936
Studying line 83 of shared-components/lists.apx, we find a security.authorizationScheme property in an Administrationentry in a Navigation Menulist. This property has the raw ID value 44263289337994936 from the imported application since APEX couldn’t find an authorization scheme with that ID.
Missing Authorization Scheme Referenced Using Its Raw Component ID
Studying the sample list entry in APEX Builder, we see the raw ID displays for a missing Authorization Scheme.
Missing Authorization Scheme Displays Using Its Raw Component ID
When a referenced authorization scheme ID is missing, is_authorized returns true so the app behaves as if no authorization scheme were applied. So, presumably the references to the missing scheme went unnoticed if every user was intended to access them. However, if the actual goal was to enforce an authorization check, then a different fix is needed.
If you decide to remove the authorization check, in APEXlang files you can remove the security.authorizationScheme property, along with the group if there are no security properties left.
The equivalent change in APEX Builder is to set the orphaned authorization scheme reference to – No Authorization Required – in the list.
If instead you decide to enforce an authorization check, you can add a new authorization component to the shared-components/authorizations.apx file, and then bulk update the .apx files referencing 44263289337994936 to replace that by the @-sign-prefixed identifier of the new authorization you create.
The equivalent fix in APEX Builder would be to create a new Authorization Scheme, and then visit the different edit pages for the referencing components and choose the newly created authorization scheme. You would fix references to the new scheme within pages using the Property Editor in Page Designer.
Conditions Not Met for Reference
Sometimes a component reference has additional validity constraints. For example, both a Faceted Search and a Smart Filters region have a filteredRegion property. It must reference a region in the current page whose region type supports being facet-filtered: calendar, cards, classicReport, or map.
When a validation error contains “Conditions not met for reference”, it means some aspect of the required constraint was not satisfied.
File: pages/p00006-employees-search.apx
Line: 283
Column: 12
Type: REFERENCE_NOT_FOUND
Error: Conditions not met for reference: @departments
Checking the p00006-employees-search.apx page, at line 283 we find the culprit:
departments Region Does Not Fully Satisfy the Requirements to be Valid Filtered Region
By searching, or using the Outline pane as shown below, we can inspect the properties of the referenced departments region. There, we see it’s an interactiveReport region, which is not one of the kinds of region that can be facet-filtered.
Navigating to Inspect the Properties of the departments Region in VS Code
How did this happen? For example, a developer could create a working Smart Filters region that filtered a Classic Report. Later, they changed the filtered region to be an Interactive Report.
But why wasn’t this causing a problem in the original app? It’s easy to see in Page Designer. The lined-through region names in the rendering tree indicate the Search Departments Smart Filters region and the Departments Interactive Report region are both commented out. This means APEX ignores them during page rendering.
Observing Two Regions are Commented Out in Page Designer
When components in a page are commented out, they have a build option named Commented Out applied. In fact, if you have a second look at the two previous VS Code screenshots, you’ll notice the following in both:
⋮
config {
buildOption: @commented-out
}
⋮
APEXlang applies strict validation on every component, including the ones with build options applied to them. So, here the fix to apply is either:
Deleting the Departments and Search Departments regions if they aren’t really used any more, or
Changing the type of the Departments region to be a Classic Report or one of the other kinds that support facet filtering.
Illegal Defaults from a Plug-in
The last error is of type LOV_NOT_FOUND. It says the settings.options property at line 421 of p00003‑employee.apx does not correspond to one of the expected values.
Reviewing the page in VS Code, we inspect line 421. The options property of a FOS Tooltip dynamic action step related to the on-page-load dynamic action has the value: cache-result,escape-html
Dynamic Action Step Using FOS Tooltip Plug-in Has Illegal Value for options Property
This is an open-source dynamic action plug-in developers use to add a fancier runtime tooltip to their pages like this:
FOS Tooltip Dynamic Action Plug-in In Action in the Employee Page
To get more info on the valid values for this custom attribute of the FOS Tooltip plug-in, we can either study it in the APEXlang format or in the APEX Builder.
Opening the custom-attributes.apx file for the fosTooltip plug-in, we see the situation below. The string cache-result,escape-html is the default value the plug-in developer configured for this options custom plug-in attribute.
Examining optionsCustom Attribute of FOS Tooltip Plug-in in APEXlang Format in VS Code
Reviewing the same custom attribute in the Builder plug-in edit page, we see the same Default Value for the Options custom attribute.
Reviewing the Same FOS Tooltip Plug-in options Custom Attribute in APEX Builder
Either way you investigate, you discover that the options custom attribute is of type checkboxes. This means it can accept multiple values. The standard delimiter between multi-valued options is the : colon character, but here the plug-in developer has inadvertently used the , comma instead. Their obvious intent was to configure the default set of options to be both Cache Result and Escape HTML choices from the LOV.
The plug-in works correctly at runtime since its code tests for which options are specified using the instr() function. For example, it checks for the Cache Result option in the property value using:
However, APEXlang performs strict validation, so the fix entails two parts:
Part 1: Change the plug-in options custom attribute to have the default value of cache‑result:escape‑html with the : colon delimiter instead.
This will address any new usages of the plug-in in this app.
The second part of the fix requires adjusting how page 3 references the two options values from the custom attribute’s list of valid choices.
Part 2: Change the plug-in usage on Page 3 to reference the distinct choices correctly
To fix the default value in Builder, update the default value on the edit page above to use cache‑result:escape‑html and apply the changes.
To fix the plug-in usage on page 3, open Page Designer and select the dynamic action. Notice that due to the illegal comma-separated default value, Page Designer is not showing any values currently checked in the Options checkboxes group. To fix the values, just check the Cache Result and Escape HTML checkboxes, then save the page.
Reviewing FOS Tooltip Dynamic Action Plug-in Options in Page Designer
Alternatively, to fix in the APEXlang files, we would need to update the default value of the custom attribute in FOS Tooltip’s custom-attributes.apx file to be:
⋮
customAttribute attribute_14 (
apexlangName: options
attribute: 14
name: Options
type: checkboxes
appearance {
sequence: 140
}
default {
value: cache-result:escape-html
}
help {
helpText: <p>Additional options to adjust the plug-in even more.</p>
}
)
⋮
Then, we would update page 3 to change the value of the options property to an APEXlang array like this. Between opening and closing square brackets, list each value on its own line: cacheResult and escapeHtml.
⋮
action plugin-com-fos-tooltip (
action: plugin/fosTooltip
settings {
staticContent: Enter the date of hire
options: [
cacheResult
escapeHtml
]
}
affectedElements {
selectionType: items
items: P3_HIREDATE
}
execution {
sequence: 10
}
)
⋮
Summary
Hopefully, studying these examples gives you the techniques to find and fix any metadata inconsistencies your apps may have accumulated over the years.
Start by producing a list of any validation errors and warnings using spool with apex validate in SQLcl, or by downloading the error CSV from APEX Builder import.
Then for each error or warning message:
Visit .apx file and line number to understand which component is affected
Review it in the familiar APEX Builder for more context
Decide on the fix and make it in Builder or directly in the .apx file
After making some fixes, “rinse and repeat” to get an updated list of errors. Enjoy the warm glow that may wash over you once your app validates cleanly! For more information about APEXlang, see the APEXlang page on oracle.com.
A first look at new ways to edit, validate, and generate Oracle APEX apps
Take one small step with APEXlang to see why it’s a giant leap for low-code. In addition to the web-based Builder, now you can also maintain and generate Oracle APEX apps with your favorite coding tools and agents. This article gives a first peek at the following tools in action:
APEX Builder is a productive way to deliver apps quickly. You configure smart data components by choosing a table, view, or query and adjusting declarative options. Then, you add the business logic only you could write.
But editing apps with external tools was not as friendly. You could export an app to SQL for source control, but settings were buried in internal API calls and sprinkled with numerical IDs. Readable YAML simplified reviews and diffs, but you still couldn’t edit or import it. To preserve the integrity of your app definitions, APEX didn’t support manual updates since it couldn’t reliably validate these files. Until today.
Starting with APEX 26.1, you now have the best of both worlds.
Use APEX Builder for everything you love about it. Whenever useful, you can review and edit your app’s APEXlang definitions in your favorite editor. Use SQLcl to export, import, and validate apps from the command line or in scripts. In VS Code, the SQL Developer extension helps you export, edit, validate, and import your apps, showing problems proactively so they’re easy to fix.
Agentic Assistance: Generate, Review, Evolve
APEXlang is well-structured and easy to understand, so you can also partner productively with AI coding agents like Claude, Codex, and others. Ask them to review your app, suggest improvements, or edit existing definitions. With Oracle’s APEXlang skills from GitHub, just explain what you need to your AI collaborator. It can help generate pages and components, make incremental changes, and find and fix problems on its own using SQLcl’s apex validate command.
If you start projects by iterating with stakeholders on specifications, APEX 26.1 offers another advantage: blueprints turn functional specs and schema descriptions into fully scaffolded apps. Key users see the system sooner, catching gaps to avoid rework later. Generative AI skills and prompts produce a readable Markdown blueprint. APEX then deterministically generates a consistent app from it, using curated patterns proven by Oracle app teams. Once created, you can evolve the app in APEX Builder, with APEXlang, or through your favorite coding agent using the APEX skills.
NOTE: To see any image in full size, right click to open it in a new tab.
Exporting APEXlang in the Builder
Imagine using the Create App Wizard to build a simple APEX app with an Interactive Report page and modal Form page on the familiar EMP table.
Creating a Small Employees Application Using the Create App Wizard
To export the app in APEXlang, just choose that format on the Export page.
Choosing APEXlang Format for Your Application Export in the Builder
This downloads an employees.zip file. As shown below, it contains a file for each page and component. The application.apx file has app-level settings. The .apx files contain APEXlang. The deployments, pages, shared‑components subdirectories organize app components at the root level of the zip. Notice filenames for pages use both number and alias for additional clarity, and the compiler enforces the page alias matches the page number and alias in the file name. Static Application Files are included as-is along with a list of their names and metadata in static‑files.apx.
employees.zip
├── application.apx
├── page-groups.apx
├── deployments
│ └── default.json
├── pages
│ ├── p00000-global-page.apx
│ ├── p00001-home.apx
│ ├── p00002-employees.apx
│ ├── p00003-employee.apx
│ └── p09999-login.apx
├── shared-components
│ ├── static-files
│ │ └── icons
│ │ ├── app-icon-144-rounded.png
│ │ ├── app-icon-192.png
│ │ ├── app-icon-256-rounded.png
│ │ ├── app-icon-32.png
│ │ └── app-icon-512.png
│ ├── themes
│ │ └── universal-theme
│ │ └── theme.apx
│ ├── authentications.apx
│ ├── authorizations.apx
│ ├── breadcrumbs.apx
│ ├── build-options.apx
│ ├── component-settings.apx
│ ├── lists.apx
│ ├── lovs.apx
│ └── static-files.apx
└── .apex
└── apexlang.json
What About the Application ID?
In APEXlang, the app id goes into the defaults.json file in the deployments subdirectory. This lets the downloaded file employees.zip use a more descriptive name based on the employeesapp alias. This separation also gives you the option to personalize each deployment environment with an appropriate set of environment-specific settings.
{
"app" : {
"id" : 104,
"runtime" : {
"debugging" : true
}
}
}
APEXlang: Metadata-Driven Validation
The APEXlang compiler knows which APEX components are available as well as what properties and child components each one has. It validates property values, nested components, and constraints on when each applies. For example, one type of region component like an interactiveReport has nested column definitions, but a form type region doesn’t. Similarly, a chart type region has a zoomAndScroll property, but a classicReport does not.
The APEX Page Designer has always reacted dynamically to show and hide appropriate properties and child components. Now the APEXlang compiler leverages the same details to strictly validate your app components and pages.
This information about APEX’s app metadata is called meta-metadata (MMD). Over time, it will grow to reflect new APEX features, so each app stores the current APEX meta-metadata version used to export it. This lets the APEXlang compiler validate app files correctly. It also ensures SQL Developer offers accurate code completion in VS Code.
Your app’s .apex/apexlang.json file records this MMD version. It’s important this version remain unchanged until a future APEX version might export your app with a new value. In short, don’t modify it by hand.
{
"mmdVersion" : "26.1.0+3102"
}
Importing APEXlang in the Builder
You can import an app in APEXlang format in the Builder. Just drag or select its zip file on the Import page and click (Next >).
Importing an Application in APEXlang Format in the Builder
The Import wizard confirms the Type is APEXlang and gives the usual options to choose the application ID. Just click (Import Application) to proceed. APEX Builder validates and compiles the APEXlang source. If no errors are raised, the application imports successfully.
Previewing the Application to Import and Choosing an Application ID
Compiling APEXlang via ORDS Service
APEX Builder uses a dedicated ORDS endpoint with Builder authentication to compile APEXlang artifacts. So, the first time you import an APEXlang format app in a workspace, you may be prompted to enable your workspace’s parsing schema for ORDS REST services. Just click the (REST Enable Schema) button and the wizard proceeds to the next step to continue the import.
Enabling Workspace Parsing Schema for ORDS REST Services if Needed
First Peek at APEXlang Syntax
When you open an app folder with APEXlang files in your favorite editor, you can browse, search, and edit them like any project. For example, page 3 is an Employee form page with alias employee. Its filename incorporates both the page number and the alias. Opening p00003‑employee.apx, you see the wizard created a modalDialog page using the drawer template.
page 3 (
name: Employee
alias: EMPLOYEE
title: Employee
appearance {
pageMode: modalDialog
dialogTemplate: @/drawer
templateOptions: [
#DEFAULT#
js-dialog-class-t-Drawer--pullOutEnd
]
}
dialog {
chained: false
}
⋮
)
In the example above, it’s easy to see this page component’s pageMode property – in the appearance group – has the value modalDialog. For brevity, we can say appearance.pageMode is modalDialog.
APEXlang avoids unnecessary delimiters. Every component (e.g. page) contains its properties, grouped by function, as name:value pairs. Properties like name, alias, and title in the identification group can optionally appear with no group as shown below. Required properties must have a value, but files stay concise since any property with its default value can be omitted. Leave optional properties out until you need to assign them a value.
Values don’t require quotes in most cases, and a new line separates properties and array elements. For example, the value of appearance.templateOptions is an array with two entries. In this case, they are CSS class names the template defines with the special #DEFAULT# placeholder for “any default classes” the template specifies.
page 3 (
name: Employee
⋮
appearance {
⋮
templateOptions: [
#DEFAULT#
js-dialog-class-t-Drawer--pullOutEnd
]
}
⋮
)
Later in this article, you’ll see how multi-line text appears in APEXlang for common cases like SQL, PL/SQL, JavaScript, HTML, and CSS.
Understanding the Identifier Property
Each component type’s metadata nominates an identifier property. Most of the time, staticId plays this role, but sometimes it’s the name or alias. In APEX 26.1, all components now require a human-readable static ID so APEXlang files never need to reference internal ID numbers. This new requirement is automatically handled for existing apps. On instance upgrade or app import, APEX assigns any apps created with prior platform versions a meaningful static ID automatically.
The APEXlang compiler distinguishes each component within a given scope using its identifier property value. It enforces component uniqueness, depending on the type, across the app, a page, or just within a parent. For example:
an app can have only one page 3
page 2 and 3 can both have a region with identifier employees
page 3 can have employees & departments regions each with a DEPTNO column
the departments region on page 3 cannot have two DEPTNO columns.
A component’s identifier value follows its type, just before the opening parenthesis of its property list. In the example below 3 is the page component’s identifier, and employee identifies the region.
page 3 (
⋮
region employee (
⋮
)
⋮
)
Component References in APEXlang
Each @-prefixed value is a reference to the identifier property value of another component. Later in p00003‑employee.apx you find the page contains a Form region named Employee identified by employee. Notice its appearance.template property references the blank-with-attributes template.
page 3 (
⋮
region employee (
name: Employee
type: form
source {
location: localDatabase
tableName: EMP
}
layout {
sequence: 10
slot: contentBody
}
appearance {
template: @/blank-with-attributes
templateOptions: #DEFAULT#
}
edit {
enabled: true
allowedOperations: [
add
update
delete
]
}
)
⋮
)
Each page item appears in the page’s .apx file as well. Notice the hidden, primary key P3_EMPNO item based on the EMPNO column. It references the employee region as the value of both its layout.region and source.formRegion properties.
When a pageItem references an employee region on the same page, it uses @employee.
Similarly, when a pageItem references a shared component like an LOV in the same app, it uses @. Notice the P3_DEPTNO item below is a selectList referencing the sharedComponent LOV using @dept-dname.
page 3 (
⋮
pageItem P3_DEPTNO (
type: selectList
label {
label: Deptno
alignment: left
}
lov {
type: sharedComponent
lov: @dept-dname
}
layout {
sequence: 80
region: @employee
slot: regionBody
alignment: left
}
appearance {
template: @/optional-floating
templateOptions: #DEFAULT#
}
source {
formRegion: @employee
column: DEPTNO
dataType: number
}
)
⋮
)
Two exceptions to the @‑prefix rule use an @/‑prefix instead. They are the components that are either:
on the global page, or
part of the standard Universal Theme.
For example, in a regular page, reference the my‑global‑page‑header region from the global page using @/my‑global‑page‑header.
When a template comes from the standard Universal Theme set, use references like @/drawer or @/blank‑with‑attributes or @/optional‑floating. In contrast, when a theme template is defined locally within the app, use @my‑slideshow.
Using Both Builder and External Tools
You can use APEX Builder as you normally would, but be mindful that the unit of APEXlang export is the application. If you have made changes to your app outside the builder, importing its APEXlang replaces the Builder’s current version to reflect the latest external changes. Conversely, if you make changes in APEX Builder, external tools see them when you next export the app to APEXlang.
Maintaining your app’s development history over time by source controlling its external artifacts in a system like Git continues to be a best practice. APEXlang files work great for this purpose, too.
If you realize your external APEXlang and APEX Builder changes are out of sync, now that APEXlang files are editable, validatable, and importable, you can resolve the situation by:
Exporting the Builder app version to APEXlang
Unzipping it into a temporary folder
Unifying versions, resolving any conflicts, with your favorite diff/merge tool
Importing the unified APEXlang back into the Builder when appropriate.
The app only imports when it validates cleanly, so if it fails just review the errors, address them, and try again.
Updating Employees Page in APEX Builder
Imagine using Page Designer in APEX Builder to evolve the initial Employees page. Say you make the following enhancements:
switch the Interactive Report region to be a Content Row
change it to be based on a SQL query,
update the page help text with some HTML markup,
define an inline CSS rule to increase the font size by 20%
add a Raise Salary action button to run PL/SQL business logic, and
use JavaScript on the Page Load event to welcome the user back to your app.
The figure below shows your page after these modifications. Since you haven’t made changes yet in VS Code, you can simply export the app to APEXlang directly in VS Code to reflect the latest Builder changes in the project files.
Modified Employees Page in Page Designer
Exporting APEXlang with SQL Developer
In VS Code, SQL Developer shows APEX apps in a connection’s APEX folder. As an alternative to exporting from APEX Builder, choose Export… on an app’s context menu under the connection. Then pick a parent folder for the app’s APEXlang artifacts and click (Apply). If necessary, for our example here, SQL Developer creates the employees subdirectory in that parent folder and exports the app in APEXlang format into it.
Exporting Employees App (104) in APEXlang from SQL Developer
If you’re curious, the SQL tab in the Export… panel shows the SQLcl command to do this APEXlang app export (N.B. wrapped on separate lines here for legibility). The -dir parameter is optional in practice. If omitted, SQLcl exports the app into a folder named after the app alias to the current directory.
apex export -applicationid 104
-dir '/Users/smuench/Downloads'
-exptype APEXLANG
Previewing SQLcl Command to Export App 104 in APEXlang Format to a Parent Directory
Since the /Users/smuench/Downloads/employees folder is already open in the editor, the project immediately updates to reflect the latest changes.
SQL in APEXlang
Peeking at the p00002-employees.apx page, you see how a SQL query appears in APEXlang. It uses “fenced code blocks” with a language indicator. The opening triple-backtick delimiter includes sql so the editor can give language-sensitive assistance. The code block ends with a closing triple-backtick.
page 2 (
name: Employees
⋮
region employees (
name: Employees
type: themeTemplateComponent/contentRow
source {
location: localDatabase
type: sqlQuery
sqlQuery:
```sql
select EMPNO,
ENAME,
DNAME
from EMP_DEPT_V
```
}
⋮
)
⋮
)
PL/SQL in APEXlang
Similarly, PL/SQL in your app appears in a block with the plsql language indicator. In the example below, the logic is a single update statement that returns the adjusted employee salary into a hidden :P2_NEW_SALARY page item.
page 2 (
name: Employees
⋮
region employees (
name: Employees
⋮
action raise-salary (
position: primaryActions
template: button
label: Raise Salary
layout {
sequence: 10
}
behavior {
type: triggerAction
}
triggerAction increase-salary-by-100 (
name: Increase Salary by 100
action: executeServerSideCode
settings {
plsqlCode:
```plsql
update emp
set sal = nvl(sal,0) + 100
where empno = :EMPNO
returning sal into :P2_NEW_SALARY;
```
itemsToSubmit: EMPNO
itemsToReturn: P2_NEW_SALARY
}
execution {
sequence: 10
}
)
⋮
)
)
⋮
)
JavaScript in APEXlang
Your page-load JavaScript runs in the browser, so it has a javascript‑browser language indicator. Any server-side JavaScript would use javascript‑mle instead.
Finally, as shown below, any HTML and CSS you add to your page shows up in code blocks with html and css language indicators respectively. A CSS example looks like this:
page 2 (
name: Employees
⋮
css {
inline:
```css
body {
font-size: 1.2rem;
}
```
}
⋮
)
And page help HTML markup looks similar.
page 2 (
name: Employees
⋮
help {
helpText:
```html
<p>
Select an employee to edit it,
or click (Raise Salary) to increase
an employee's salary by 100.
</p>
```
}
⋮
)
Editing APEXlang in SQL Developer VS Code
Consider a simple example of editing an APEXlang page in VS Code. The SQL Developer extension adds APEXlang assistance for .apx files. For instance, in the figure below we’ve positioned the cursor in the existing value of the appearance.dialogTemplate property of page 3. The (Ctrl) + (Space) key combo summons code completion. Notice it shows the valid templates available for this page whose pageMode is modalDialog. We switch the page from a drawer template to a modal dialog one by choosing @/modal-dialog from this list.
Using APEXlang Code Completion While Editing an .apx File
Problems Panel Proactively Presents Errors
After changing the page to use the modal-dialog template, templateOptions gets a squiggly underline and the Problems panel shows “Invalid template option value…” This validation error indicates one of the existing values is no longer applicable to a modal dialog.
VS Code Problems Panel Shows APEXlang Validation Errors Proactively
Importing APEXlang into APEX Builder
After correcting the templateOptions value, optionally removing the square brackets for a one-element array, we’re ready to import the changed application into APEX Builder. As shown below, just click the “play” button in toolbar in the upper right of the editor. Since importing an APEX app requires a connection to your APEX workspace’s parsing schema, SQL Developer prompts you to choose one if needed. As always, importing an app overwrites the current version in APEX Builder so ensure all Builder changes are already reflected in the APEXlang you’re about to import.
Clicking the “Play” Button to Import Your APEXlang Application to APEX Builder
If the app validates successfully, then the import succeeds and a confirmation appears.
Confirmation of a Successful APEXlang Application Import in VS Code
Refreshing the application page in the browser, you see the Employee page has changed from a modal drawer to a modal dialog.
Verifying the Results of Importing the Latest Application Version from VS Code
Exporting APEXlang Using SQLcl
You can also export, import, and validate your APEX apps in APEXlang using the SQLcl command line.
To export an app in APEXlang format, connect to your workspace’s parsing schema, use the apex export command, and indicate the application id and export type ofapexlang.
To import an APEXlang application with SQLcl, connect to your workspace’s parsing schema, use apex import, and indicate the input to load as a path to a directory or zip file:
Importing an APEXlang app always validates it first, so any errors or warnings appear as with apex validate below. If no errors are detected, SQLcl proceeds. If the import is successful, you’ll see:
Importing application ID: 104 into workspace: DIVEINTOAPEX
Import successful.
Validating APEXlang Using SQLcl
To validate an APEXlang application in SQLcl without importing it use apex validate, and indicate the input to verify as a path to a directory or zip file:
While importing an application requires a connection to your workspace parsing schema, by design the APEXlang compiler does not require a connection. So, you can use the apex validate command with sqlcl /nolog.
If you find yourself validating frequently from the terminal, to save some typing, consider using a small bash script. The script below accepts an optional directory path as an argument. If none is provided, it defaults to the current directory. It then launches SQLcl in silent mode (-S) to suppress the startup banner, connects with /nolog so no database connection is made, and passes the apex validate ‑input command pointing at your chosen directory or zip file.
#!/usr/bin/env bash
# exit on error; treat unset variables as errors
set -eu
# default to current directory if no argument given
DIR="${1:-.}"
# validate the APEXlang with no connection
sql -S /nolog <<EOF
apexvalidate-input$DIR
exit
EOF
Save the script as apexval somewhere on your PATH, make it executable with chmod +x apexval, and then validate any APEXlang app directory or zipfile in one shot. For the current directory, type:
apexval
To validate an app anywhere else, type:
apexval /path/to/my/apex/project/or/zipfile
Reviewing APEXlang Errors in APEX Builder
Even when importing an APEXlang application using APEX Builder, if the APEXlang compiler detects any errors you can review them and optionally download them in CSV format. In this case, the import does not occur. If you had indicated to overwrite an existing app, it remains unchanged. If you work with an AI coding agent, try giving it the CSV file and ask for a clickable list of errors, or for help in resolving them!
Reviewing APEXlang Compiler Errors in the APEX Builder After a Failed Import
APEXlang Inside APEX Builder
Even if you continue doing all APEX development in the Builder, you still benefit from the readability of APEXlang: it replaces the readable YAML format when reviewing changes.
For example, while working on any page in Page Designer, see an APEXlang view of the page with the new APEXlang View menu option shown below.
Accessing an APEXlang View of the Current Page in Page Designer
This lets you review and search the page’s APEXlang whenever useful.
Reviewing the APEXlang Representation of the Current Page in Page Designer
In addition, when reviewing changes between a working copy and the main application, you now see the diffs between APEXlang artifacts. For example, as shown below, it’s easy to see that the P3_MGR page item changed from a selectList to a textField, losing its related lov in the process.
Reviewing APEXlang Diffs When Comparing Working Copy with Main App
Summary
APEXlang gives APEX developers a practical new option for working with their apps both inside APEX Builder and with external tools. It offers declarative definitions you can read, edit, validate, version, and move through your usual development flow with confidence. SQLcl, ORDS, and SQL Developer now share a common APEXlang compiler, driven off the latest meta-metadata that has powered Page Designer for years. This makes external editing feel natural and safe, with strict validation, code completion, and integrated import/export support.
And this is only the beginning. In my next article, we’ll explore using apex validate to find and fix common metadata inconsistencies that may be hiding undetected in mature APEX apps. In another installment, we’ll look more closely at how the new APEX skills for AI coding agents can help you use natural language to generate, review, and evolve APEX apps. To get the APEX skills, just use the new skills sync command in SQLcl 26.1.2. For more information about APEXlang, see the APEXlang page on oracle.com.
Approval Tasks in your APEX workflows have two built-in outcomes, either APPROVED or REJECTED. If your flow needs custom outcomes, use an Action Task with an updateable task parameter instead.
The sample app in this article lets the user assigned to the task click an (X), (Y), or (Z) button to indicate their custom response. Then the workflow uses Switch activities to perform different work depending on the task outcome.
The sample app you can download from here contains a home page that lets you start a workflow, and delete any development workflows you might have created for testing.
Sample App Home Page Lets You Start and Delete Workflow Instances
Additional Wizard-Created Pages
The sample also contains a My Tasks page I generated with the Create Page wizard by choosing Unified Task List and picking the context of My Tasks. It shows the user open tasks assigned to them. I also created the Workflows page using the same wizard, by picking Workflow Console instead.
In both the My Tasks page and the Workflows page, I made one minor change to the SQL query of the generated page’s Content Row region. I simply uncommented the ,p_application_id => :APP_ID parameter to pass the current application ID to the apex_human_tasks.get_tasks pipelined table function returning the data to display. Otherwise, by default the pages show tasks and workflows from all applications in the workspace.
For example, in My Tasks, in the SQL query of the Content Row region, I deleted the two hyphens that initially appeared in the the line:
--,p_application_id => :APP_ID
This two-character deletion made it look like this:
select task_id,
task_type,
⋮
badge_state
from table ( apex_human_task.get_tasks (
p_context => 'MY_TASKS',
p_show_expired_tasks => :P6_SHOW_EXPIRED
, p_application_id => :APP_ID /* <- */
) )
I made the same change to the Workflows page’s Content Row region to the apex_workflow.get_workflows function returning workflow instance data. These two changes make the pages show only the tasks and workflows for the current application.
Overview of the Workflow Definition
The workflow definition shown below waits for an Action Task, passing the APEX$WORKFLOW_INITIATOR built-in workflow variable value as the Task Initiator. Then the flow uses different Switch activities to take different paths depending on the task’s custom outcome. In this example, the different outcome values simply assign different values to two workflow variables INTERMEDIATE_RESULT and FINAL_RESULT, but in your own workflows the paths can contain any activities you need.
Workflow Waits for Action Task, then Uses Switch Activites to React to Custom Outcome Value
Key Ingredients of the Action Task
When I created the task, I noted that the default task type is Approval Task. You can’t change the task type later, so it’s important to pick Action Task up front.
To keep the example simple and runnable as a single developer, I configured the action task to use the :APEX$TASK_INITIATOR expression as both the Potential Owner and the Business Administrator. Of course, in your own apps you can use any participants that are appropriate.
The task definition also contains one updatable task parameter named OUTCOME as shown below. There is nothing special about choosing this name: in practice, use any name you like.
Action Task Definition Uses Expression-type Participants and an Updatable OUTCOME Parameter
Customizing the Task Details Page
I clicked the appropriate button in the Task Definition edit page to automatically generate a task details page. Then, I modified it in Page Designer to remove the default action task (Complete) button.
In its place, I added (X), (Y), and (Z) buttons to represent three custom outcomes named X, Y, and Z. They each simply submit the page using button names OUTCOME_X, OUTCOME_Y, and OUTCOME_Z respectively.
Customized Task Details Page Has (X), (Y), and (Z) Buttons That Submit the Page
Adding Custom Declarative Business Logic
I added a new Execution Chain to the task details page’s Processing section to group the logic that runs when the user presses any of the buttons named OUTCOME_X, OUTCOME_Y, or OUTCOME_Z. It has the Server-side Condition to only run if the REQUEST value –reflecting the submitting button name – is contained in the string:
|OUTCOME_X|OUTCOME_Y|OUTCOME_Z|
This condition ensures the chain’s child processes only run if the user presses one of the custom outcome buttons. The execution chain’s Run in Background switch is off, so when they run, they happen immediately in the current user session during page submit processing.
The Execution Chain contains three child processes shown below that run in order:
Configure Value to Set Task Parameter To
Update Task Parameter
Complete the Action Task
Execution Chain Groups Conditional Business Logic with a Common Condition
Configuring the Business Logic
The Update Parameter feature of the declarative Human Task – Manage page process expects the name of the task parameter to be stored in a page item. It also expects the parameter value to come from a page item. So, I’ve added two hidden page items shown below for that purpose. I set their Storage to In‑Memory Only so their values don’t persist across page visits:
P2_TASK_PARAMETER_NAME_TO_SET
P2_TASK_PARAMETER_VALUE_TO_SET
Hidden Page Items Provide Update Parameter Name and Value
Since we want to set the task parameter named OUTCOME, the P2_TASK_PARAMETER_NAME_TO_SET has a static default value: OUTCOME
The initial Configure Value to Set Task Parameter To child process in the chain is an Execute Code process that sets the value that we want to assign to the task parameter into the hidden page item:
:P2_TASK_PARAMETER_VALUE_TO_SET :=
case :REQUEST
when 'OUTCOME_X' then 'X'
when 'OUTCOME_Y' then 'Y'
when 'OUTCOME_Z' then 'Z'
end;
The second Update Task Parameter child process in the chain uses the declarative Human Task ‑ Manage page process, with type set to Update Parameter. It updates the parameter whose name is in P2_TASK_PARAMETER_NAME_TO_SET and whose value is in P2_TASK_PARAMETER_VALUE_TO_SET on the task instance whose ID comes from P2_TASK_ID.
Configuring the Update Task Parameter Page Process
Since the static value OUTCOME is in the name page item, and the dynamically assigned task parameter value is in the value page item, this configuration results in setting the OUTCOME updatable task parameter to either X, Y, or Z as determined by the first step in the execution chain above.
The final Complete the Action Task child process in the chain uses the Human Task ‑ Manage page process with type Complete (without Outcome) to complete the action task.
Switching Paths Based on Custom Outcome
In the workflow, you’ll find two different Switch activities.
One shows how you can reference the value of the task parameter directly using apex_human_task.get_task_parameter_value(:CUSTOM_TASK_ID,'OUTCOME')
I’ve used a “True/False” type switch using the boolean expression:
nvl(apex_human_task.get_task_parameter_value(
:CUSTOM_TASK_ID,'OUTCOME'),'@') = 'X'
The different True and False branches from the switch change course depending on whether the user provided an X outcome, or did not provide an X outcome.
Here, the CUSTOM_TASK_ID is a workflow version variable I added and then used as the Human Task ‑ Create activity’s Task ID Item as shown below. This makes it store the task instance ID into the CUSTOM_TASK_ID variable when the task owner completes the task.
Configuring the Human Task – Create Activity to Store Task ID in a Variable
A second switch activity shows how you can switch on a workflow variable value if you first add an Execute Code activity to the workflow that assigns the workflow variable to the value of the OUTCOME task parameter.
Giving the Sample a Spin
Try running the sample and creating three workflows by clicking three times on the (Start Workflow) button. Since the app configures the workflow title to include the current time, try waiting at least one second between button clicks so your three workflows have distinguishable titles.
In the Workflows page, you can see the three workflow instances you created.
Reviewing the Three Workflows You Created in the Workflows Console Page
Next, use the My Tasks page to respond to each of the action tasks assigned to you. In one each, try clicking the (X), (Y), and (Z) button.
Using the Task Details Page to Indicate Different Custom Outcomes for Each Assigned Task
Finally, use the Workflows console to inspect the details of each workflow instance. Study the workflow variable values to see how the different switch activities reacted to the custom outcome. For example, in the workflow instance where you indicated a custom Z outcome, the variables shown below are the result of the path the workflow took for that outcome.
Studying the Variable Values to See How Workflow Reacts to Different Outcomes
In the latest Google Chrome release (143.0.7499.41) a new Split View feature is handy for Oracle APEX developers. It lets you run APEX Builder and your runtime app side-by-side in the same tab. Here’s how to use it.
NOTE: To see a larger version of any image, right-click on it an open it in a new tab at full size.
Split View: Builder + App
Start by running your application to open it in a second tab…
APEX Builder in Separate Browser Tab from Running Application
With the Builder tab active, hover over the tab of your running application and right-click on it. Choose the New Split View with Current Tab option as shown below…
Right-Clicking on Running App Tab to Create New Split View with Builder & Running App
You now have a single tab, with the Builder on the left and your running app on the right. Both remain fully interactive…
Oracle APEX Builder on the Left and Running App on the Right in the Same Tab
If you ever need to just reload your application view, just right click anywhere on the page and choose the Reload option.
Reloading Only One of the Views When Necessary Using the Context Menu
You can use the new Arrange Split View toolbar icon as shown below to reverse the views, separate them, or close one of them…
Rearranging, Separating, or Closing Views in a Split View Tab
As shown below, you can hover over the splitter margin and drag it to resize the left and right views…
Dragging the Splitter to Adjust Left and Right View Width
That’s all there is to it!
Page Designer + Object Browser
Of course, the side-by-side tabs can also be two separate areas of the APEX Builder itself. For example, I find myself now working frequently with a Page Designer and Object Browser side by side. This lets me reference and evolve application logic in PL/SQL packages, and schema objects in database tables, views, and columns while I work on various pages in my app.
To setup this arrangement, start with the Page Designer open, and right-click on the SQL Workshop > Object Browser menu option to choose the Open Link in Split View option as shown below.
Opening Object Browser in a Split View with Page Designer
The Object Browser appears in the split view. You can keep its navigation pane collapsed to gain more screen space, or as shown below, click on the sidebar arrow to pop it open…
Clicking on the Collapsed Object Browser Navigator to Open It
This lets you choose a different package, table, view or other schema object for reference while working on a page.
Using the Object Browser Navigation Panel to Select a Schema Object
In APEX on Autonomous Database, use declarative, server-side geocoding to find coordinates for an address. Then you can see it on a map, measure its distance from another point, find it within a radius, and process it with other spatial data features.
Use an AI Configuration to create a smart web service with natural language. It taps into a large-language model’s knowledge to return a structured result.
To celebrate Joel Kallman Day 2025, we study an app that combines the two. It lets a user enter an address, then in the background finds its coordinates and whether it represents a well-known point of interest. Download the sample app at the end to try it yourself on your Autonomous Database or Oracle APEX Service.
Trying the App
For example, we can enter this South Dakota address and click (Create <GeoJSON>).
Entering an Address to Geocode Server-side in Keystone, South Dakota (USA)
After a few seconds, if we refresh the Interactive Report by clicking the (Go) button, we see that in the background it found the coordinates and determined this is the address of Mount Rushmore National Memorial.
New Address Geocoded in the Background, with Point of Interest Identified, Shows After RefreshMount Rushmore National Monument in Keystone, South Dakota (USA)
Server-Side Geocoding
Your apps can use client-side geocoding in any browser. However, server-side geocoding depends on APIs exclusive to Autonomous Database. So, you need to develop and run your APEX app in ADB or APEX Service to use it. Declarative Server Side Geocoding is available as a page process, a workflow activity, or an automation action. You provide an address as a single string, or as a structured set of different address element fields. You have two options for the resulting geocoded info. It can populate a page item you configure with a small GeoJSON document containing longitude and latitude. For more detailed results, configure the name of a collection to populate instead.
The (Create <GeoJSON>) button submits the form, triggering processing that saves the form data, then runs the Geocode and POI Lookup execution chain shown below. Notice that it’s set to Run in Background.
Geocode and POI Lookup Execution Chain Runs in the Background
If the CREATE_GEOJSON button submits the form, the execution chain below runs. As shown below, its first child process uses a Server Side Geocoding process type to find the coordinates. It configures its unstructured Address Item to come from P3_ADDRESS and its GeoJSON Coordinate Item to be returned into the hidden P3_GEOJSON page item.
Server-side Geocoding Can Accept an Address and Return a GeoJSON Point
The second child process uses the SQL statement below to extract the longitude and latitude from the GeoJSON document returned. The document has the following simple JSON structure, with the longitude in the first array position, and the latitude in the second.
{"coordindates":[-103.47297,43.88693]}
A simple JSON_TABLE query that extracts the coordinates into the P3_LONGITUDE and P3_LATITUDE items looks like this. Notice it uses the zero-based index to reference the zero-th (i.e. first) array element and second array element.
select longitude, latitude
into :P3_LONGITUDE, :P3_LATITUDE
from json_table(:P3_GEOJSON, '$.coordinates'
columns (
longitude number path '$[0]',
latitude number path '$[1]'
));
Extracting Point Coordinates from GeoJSON Using JSON_TABLE
Geocoding Into Collection for More Info
Inserting another address, this time we can geocode using a named collection. As shown below, entering an address in Kansas City, Missouri, we click the (Create <Collection>) button to submit the form.
Entering a Second Address to Geocode Server-side in Kansas, Missouri (USA)
Refreshing the page to see the background job’s handiwork, we see that it’s the iconic Union Station.
Collection-based Background Geocoding Result Shows in the Interactive Report After RefreshIconic Art Deco Union Station in Kansas City, Missouri (USA)
This time, the page submit runs the conditional logic the If Approach = COLLECTION execution chain contains. Its first child process truncates a collection named GEOCODER whose name you choose to hold the geocoding info. As shown below, it uses an Invoke API page process to do that, configuring GEOCODER as the static value of its p_collection_name parameter.
Clearing the GEOCODER Collection Using an Invoke API Page Process
Next, a Server Side Geocoding page process configures its Collection Name to the same GEOCODER name, and again uses P3_ADDRESS for the unstructured Address Item.
Geocoding an Address Server-side into the GEOCODER Collection
When configured with a collection name, the geocoding process populates more information than just the longitude and latitude. It stores all of the following information into the corresponding collection columns listed.
Longitude (N001)
Latitude (N002)
Street (C001)
House Number (C002)
PostalCode (C003)
City (C004)
City sub area (C005)
Region/State (C006)
Country (C007)
Match Vector (C011)
Timestamp of Geocoding (D001)
With this information, the third child process extracts the longitude and latitude using an Execute Code page process with the following PL/SQL:
select n001 as longitude_from_collection,
n002 as latitude_from_collection
into :P3_LONGITUDE,
:P3_LATITUDE
from apex_collections
where collection_name = 'GEOCODER'
fetch first row only;
Using Gen AI for Point of Interest Lookup
Now that we have the address, longitude, and latitude, we’d like to identify whether the address represents a well-known point of interest. A large-language model like ChatGPT is trained on a vast amount of data. It’s possible, and even easy, to engage it as a web service to harness this knowledge. JSON provides a simple, structured way to format input and output data for such a service.
An AI Configuration shared component encapsulates a system prompt. It describes the “mission” you want the LLM to accomplish. Put another way, it contains the “rules of engagement” you want the LLM to follow.
As shown below, the poi_lookup AI configuration in the sample app uses the following prompt. It describes the expected input in JSON format, and prescribes the expected JSON output:
You are an expert in United States points of interest.
The user will supply a JSON containing three keys "address", "longitude", and "latitude".
Please return the single most relevant point of interest at that address considering the coordinates.
You always and only reply using a well-formed JSON object.
If you can identify the most relevant point of interest, your reply will take the form:
{ "poi":"Your Point of Interest"}
Otherwise reply with the JSON:
{ "poi" : null }
The input JSON follows:
AI Configuration Defines System Prompt for Point of Interest Lookup Service
Invoking Generative AI Service from SQL
Using JSON_OBJECT and APEX_AI.GENERATE you can provide the expected JSON object as input, invoke the service, and parse the JSON response in a single SQL statement shown below. Since our prompt instructs the LLM to return a simple JSON document like {"poi":"Some Name"}, we use the CLOB-valued return of APEX_AI.GENERATE as the input to JSON_TABLE. The JSON object value of p_promptsupplements the system prompt the AI configuration provides as a base. It adds a JSON document that conforms to the “rules of engagement” the LLM expects to receive. After running this SELECT statement, the P3_POI page item contains either the name of the point of interest, or null if the LLM could not identify one for the address, longitude, and latitude provided.
select poi
into :P3_POI
from json_table(
apex_ai.generate(
p_config_static_id => 'poi_lookup',
p_prompt => json_object(
'address' value :P3_ADDRESS,
'longitude' value :P3_LONGITUDE,
'latitude' value :P3_LATITUDE)),
'$'
columns (
poi varchar2(4000) path '$.poi'
)
);
Invoking a Generative AI Service to Lookup Point of Interest
Updating the Address Row
The final step in the execution chain updates the EBA_DEMO_ADDRESSES table row with the new found longitude, latitude, and point of interest name – if one was found – using an Execute Code process with the PL/SQL:
update eba_demo_addresses
set longitude = :P3_LONGITUDE,
latitude = :P3_LATITUDE,
poi = :P3_POI
where id = :P3_ID;
Visualizing Addresses on a Map
Once addresses have coordinates, they are easy to show on a map. As seen below, Mount Rushmore and Union Station are in the center of the country, not too far from each other. But, can we tell exactly how far?
Using a Map Region to Show Addresses with Coordinates
The Map region supports various additional options you can tailor on its Attributes tab in the Property Editor. For example, as shown below, you can enable the Distance Tool.
Enabling the Distance Tool in the Attributes tab of the Property Editor for a Map Region
This adds a “ruler” tool to the map you can use to learn Mount Rushmore is about 900km from Union Station as shown below.
Using the Distance Tool to Measure How Far One Address is From Another
Exploring POIs with Claude & SQLcl MCP
As another productive way to work with the spatial data, I configured Claude to use SQLcl MCP Server and asked it this question:
Connect to "23ai [companion]" and find the distance in km between the Mount Rushmore National Memorial and Union Station using the EBA_DEMO_ADDRESSES table.
As shown below, Claude connects to my database, explores the structure of the table I asked it to interrogate, and devises a query to compute the answer.
Using Claude and SQLcl MCP to Verify Distance Between Two Points of Interest
Expanding the Run-sql tool execution for the distance calculation, we see Claude ran the following query to get the answer after first querying the table to access the coordinates of the two points of interest I asked about. It determines the distance is 913km.
SELECT
ROUND(
SDO_GEOM.SDO_DISTANCE(
SDO_GEOMETRY(2001, 4326,
SDO_POINT_TYPE(-103.47297, 43.88693,
NULL), NULL, NULL),
SDO_GEOMETRY(2001, 4326,
SDO_POINT_TYPE(-94.58637, 39.08524,
NULL), NULL, NULL),
0.005,
'unit=KM'
), 2
) AS distance_km
FROM dual
Learning Spatial Data Distance Calculation SQL from Claude
Iterating Further with Claude on SQL
Iterating with an LLM to improve a solution is fun, so next I ask Claude to rewrite the distance query to only use the names of the points of interest. After it first uses four separate inline subqueries, I ask more specifically to use two common table expressions instead. It came up with this evolved query that produces the same answer:
WITH source AS (
SELECT latitude, longitude
FROM EBA_DEMO_ADDRESSES
WHERE poi = 'Mount Rushmore National Memorial'
),
destination AS (
SELECT latitude, longitude
FROM EBA_DEMO_ADDRESSES
WHERE poi = 'Union Station Kansas City'
)
SELECT
ROUND(
SDO_GEOM.SDO_DISTANCE(
SDO_GEOMETRY(2001, 4326,
SDO_POINT_TYPE(source.longitude,
source.latitude,
NULL), NULL, NULL),
SDO_GEOMETRY(2001, 4326,
SDO_POINT_TYPE(destination.longitude,
destination.latitude,
NULL), NULL, NULL),
0.005,
'unit=KM'
), 2
) AS distance_km
FROM source, destination
Asking Claude to Refactor its SQL to Be More Reusable
Next I asked Claude to simplify the distance calcuation with a 23ai SQL Macro, so I could write a simpler query like the following. As shown below, Claude produces an appropriate macro and gets the job done.
SELECT distance_between(
'Mount Rushmore National Memorial',
'Union Station Kansas City') AS distance_km
Nudging Claude to Simplify SQL Using a 23ai SQL Scalar Macro
Automating the Same Technique
The sample also includes the Geocode Missing Coordinates and Identify Point of Interest automation. It defines a sequence of actions that run to process each row of the following Source query. Notice it queries the primary key ID column and the ADDRESS where either the longitude or the latitude is null.
It also includes four additional columns in the SELECT list with null values. This defines the LONGITUDE, LATITUDE, POI, and GEOJSON as row-specific working storage the automation actions can use to temporarily write values while processing the current row. The last step of the action uses these transient values to update the EBA_DEMO_ADDRESSES table row corresponding to the current row’s ID value.
select id,
address,
null as longitude,
null as latitude,
null as poi,
null as geojson
from eba_demo_addresses
where longitude is null
or latitude is null
The four action steps shown below are configured the same as the GeoJSON-based approach we used above. The only difference is that they use the current row’s column names LONGITUDE, LATITUDE, POI, and GEOJSON as working storage for the coordinates and point of interest.
Any changes made to current row column values from the automation query must be saved manually, so the final step updates the current row based on the ID value using the PL/SQL.
update eba_demo_addresses
set longitude = :LONGITUDE,
latitude = :LATITUDE,
poi = :POI
where id = :ID;
If we click on the (Find Missing Coordinates) button, it submits the form and runs the automation on-demand using an Invoke API to call the EXECUTE procedure in the APEX_AUTOMATION package. In a few seconds, the automation process finishes. Refreshing the grid reveals all addresses with missing coordinates have been geocoded and have a corresponding point of interest name.
Periodically Processesing or Performing Server-side Geocoding and POI Lookup on Demand
We now can confidentally conclude that it’s 4116km from Oracle Park where the San Francisco Giants play ball to the famous Flatiron Building in New York City.
4116km From Oracle Park in San Francisco to the Flatiron Building in New York City4116km From Oracle Park in San Francisco to the Flatiron Building in New York City
Tips for Installing the Sample
To try the sample in your APEX on Autonomous Database or APEX Service workspace, download it from here. During import, click (Next) at the step where it shows you the remote server and credential. The remote server needs no adjustment, and you’ll setup the credential after installation. Let the Supporting Objects installation run as well. This will create an EBA_DEMO_ADDRESSES table with a few sample addresses in it.
Navigate to Workspace Utilities, and select Web Credentials. Edit the credential named Credential for Open AI for POI Lookup. Ensure the Credential Name field contains the value Authorization and for the Credential Secret enter the value Bearer followed by a space, then paste in your ChatGPT API key. Save your entries by clicking (Apply Changes).
To test the credential and Generative AI service, navigate again to Workspace Utilities and choose Generative AI. Edit the generative AI service named Open AI for POI Lookup. Then click (Test Connection). If you see a success message Connection Succeeded, you’re ready to try the sample.
A colleague asked how he might sanitize client-submitted HTML to remove potential Cross-Site Scripting vulnerabilities. His use case was an ORDS REST service accepting HTML content, so he wanted to sanitize it using PL/SQL. After consulting the functions in the APEX_ESCAPE package, I convinced myself we needed a different strategy. The functionality could also come in handy for an APEX application, so I set out to implement a proof of concept solution.
Using XSL Transformations
Earlier in my career, I represented Oracle on the XSL Transformations working group at W3C. XSLT is an elegant, declarative standard to describe a transformation of an XML document into an alternative XML, HTML, or text result. The Oracle database supports XSLT 1.0 natively via the XMLTRANSFORM() SQL function and the DBMS_XSLPROCESSOR package. So, if I could convert the incoming HTML document into well-formed XML, I knew I could craft an appropriate XSLT stylesheet to sanitize the HTML document by selectively copying only a handful of allowed elements needed for basic content formatting and by suppressing undesirable attributes.
Using HTML Tidy
HTML Tidy is a utility Dave Raggett at the W3C created and released in 1998 to clean up HTML. You can read more about its history here, but the executive summary is that it has evolved over the years into an open source suite of command line tools for cleaning up HTML input. One kind of cleanup it supports is turning HTML into well-formed XHTML, which is perfect for XSLT to transform.
The JTidy project is an open-source Java port of HTML Tidy, so I decided to leverage the Oracle database’s support for Java stored procedures to incorporate JTidy into my HTML sanitizing solution.
I used the community edition of IntelliJ to create the HTMLToXHTML class you can find on my github with a public static toXML() method accepting a CLOB and returning a CLOB. I configured my project to use Maven to build a single JAR that contained my HTMLToXHTML class and the JTidy supporting classes. An abbreviated version of the Java source code appears below:
package com.stevemuench.utils;
import org.w3c.tidy.Tidy;
// ⋮
public class HTMLToXHTML {
static String toXHTML(String html) {
Tidy tidy = new Tidy();
tidy.setXHTML(true);
// ⋮ Set Tidy options and setup in/out streams
tidy.parseDOM(in, out);
// ⋮ Extract and return XHTML result
xhtml = out.toString("UTF-8");
// ⋮
return xhtml;
}
public static CLOB toXML(CLOB inputClob) throws Exception {
// Read CLOB into String
Reader reader = inputClob.getCharacterStream();
StringBuilder sb = new StringBuilder();
// ⋮ Convert CLOB HTML to XHTML
String cleanedXHTML = toXHTML(sb.toString());
// ⋮
// Create a new temporary CLOB for result
CLOB outputClob = CLOB.createTemporary(conn,
false,
CLOB.DURATION_SESSION);
outputClob.setString(1, cleanedXHTML);
return outputClob;
}
}
Loading Sanitizer JAR Into the Database
Using the Oracle loadjava utility, I took the htmltoxml-1.0-SNAPSHOT.jar that IntelliJ’s Maven package target produced and loaded the Java classes it contains into the Oracle database:
Next, I created the EBA_DEMO_SANITIZE package the sample app below installs, including the following private function in the package body that lets PL/SQL invoke the Java HTMLToXHTML.toXML() method:
-- Private helper function in EBA_DEMO_SANITIZE package body
Call to_xml() to get JTidy to turn HTML to sanitize into XHTML
Remove the XML default namespace for XHTML to simplify XSLT stylsheet processing
Create an XMLTYPE object using the JTidy-produced, default namespace-stripped XHTML CLOB
Create an XMLTYPE object using the sanitize-html.xsl XSLT stylesheet in Static App Files
Transform the XHTML using the stylesheet to produce the sanitized HTML
Return the sanitized result
Studying the XSLT Stylesheet
The sanitize-html.xsl stylesheet looks like this in Static App Files. It includes templates that match only a handful of allowed HTML tags – leaving everything else out, for example the <script> tag among many others. It includes a template that matches any attribute, and a higher-priority template that matches attributes whose name starts with on. This latter, more specific template prunes out any of these attributes that can contain JavaScript. This stylesheet shows one approach to selectively copy elements and suppress attributes. In other words, it shows the basic patterns. You would of course need to adjust this proof of concept to obtain a more robust sanitization.
The elements I chose to allow in my proof of concept were:
html, body, ul, ol, li, p, em, strong, b, i, img, a
The attributes I chose to suppress were:
All attributes whose names start with on (e.g. onclick, onblur, etc.)
NOTA BENE: This is only an example and does not represent a tested solution for sanitizing all forms of HTML JavaScript vulnerabilities! You’ll need to do more research and extend this proof of concept further to achieve that.
Sometimes it can be useful to include end-user input in a generative AI system prompt. You can do this with both the Generate Text with AI dynamic action, as well as the Show AI Assistant one. We’ll explore two simple APEX 24.2 sample apps that illustrates the respective techniques.
Generating Text with AI from User Input
Imagine you want to let a user find synonyms using a large language model. This is a simple use case for the Generate Text with AI dynamic action, and requires including the original word the user wants alternatives for in the system prompt. As shown below, we can use a P1_BASE_WORD Text Field page item to let the user type a word, and the (Find Synonyms) button to trigger the synonym lookup.
Simple Synonym Finder Page Using Generate Text with AI Dynamic Action
The (Find Synonyms) button has a dynamic action event handler on the Click event whose first True action step uses a Generate Text with AI dynamic action configured as shown below. Notice it includes a simple System Prompt asking the LLM to:
Find ten synonyms for word the user provides marked as <WORD>…</WORD> in a bulleted list formatted in markdown, answer only.
For the Input Value it uses the JavaScript Expression option with an expression that concatenates the $v('P1_BASE_WORD') value of the user’s page item input in between “<WORD>” and “</WORD>” literal strings to make the user input more clear to the LLM. And finally, it uses the response from the AI service by storing it into the hidden page item P1_SYNONYM.
Property Editor Showing Key Configuration of Generate Text with AI Dynamic Action
The second dynamic action step uses a Refresh dynamic action to refresh a Dynamic Content region whose function body returning CLOB is the one liner below. The region mentions the P1_SYNONYM hidden page item in its Page Items to Submit property.
To include user input in a chatbot, use an AI configuration with a RAG source that returns the page item value. The second sample app uses an AI configuration containing a system prompt:
You can only answer questions about fruit.
… and a RAG source with a description of:
Limit your questions to fruit contained in the following list:
… with a corresponding function returning CLOB that looks like the following one-liner:
return :P1_ADDITIONAL_USER_PROMPT;
As shown below, the Additional User Prompt Text Field page item lets the user type in a list of fruits that will influence the system prompt used by the chatbot. This works since the AI configuration’s RAG sources are evaluated before each interchange with the AI Service.
In the quick session I captured in the screenshot, I had first typed in “apples, oranges” into the field and asked the chatbot the first two questions, then I changed the Additional User Prompt field to the value “watermelon, figs” and asked the next question.
Since the APEX engine evaluates the RAG sources on the server side, it’s important to “push” the user-entered Additional User Prompt into server-side session state whenever it changes. This is easy to do using a dynamic action event handler on the text field page item’s Change event. It only needs an Execute Server-side Code dynamic action step with a “no-op” PL/SQL block of:
null;
…along with a Items to Submit property mentioning the P1_ADDITIONAL_USER_PROMPT page item.
The rest of the chatbot interaction is handled automatically by the combination of the following “ingredients” in the recipe:
A Static Content region on the page with a static id set to chatbot
A Dynamic action event handler on the page’s Page Load event with a single Show AI Assistant action step.
This Show AI Assistant action step uses the my_ai_configuration AI configuration explained above, has Display As set to Inline, and mentions #chatbot as the Container Selector.
An Oracle colleague asked how to create an editable Interactive Grid against an APEX collection. I share the solution here along with a few observations I learned while creating it.
Client Side Grid Model
An interactive grid manages an in-memory record set in the browser. This is called the model. An editable grid’s model also tracks rows the end user has added, modified, and deleted. When the user submits the page or clicks the grid toolbar’s (Save) button, the changes in the model are sent to the server for processing by the Interactive Grid – Automatic DML page process.
The client-side model uniquely identifies rows using the values of the region columns marked as Primary Key and assumes those values won’t change. For rows the user creates, it generates a temporary unique key.
Grid Primary Key Cannot Change
When the grid saves a new row, the grid-assigned temporary key gets replaced by the database-assigned key. That’s the only key-change situation the grid allows.
I had begun by using the APEX_COLLECTIONS view’s SEQ_ID as the grid’s primary key column, thinking it was a safe choice for an unchanging unique key. This wrong assumption was the gotcha I encountered when pairing the grid with an APEX collection. I learned that it’s definitely unique, but it’s not necessarily unchanging. In certain situations, the collection’s sequence ID can get reused or changed.
The more obvious way this can happen is if the developer calls any of the APEX_COLLECTION package’s procedures that affect the ordering of the collection like: SORT_MEMBERS, MOVE_MEMBER_UP, MOVE_MEMBER_DOWN, or RESEQUENCE_COLLECTION.
But what I ran into was something more subtle. Assume we use a collection for a simple shopping cart with columns:
C001 as ITEM_CODE
N001 as QUANTITY, and
D001 as NEED_BY_DATE
I ran into trouble with the following scenario:
Add “Banana” to the shopping cart grid
Click (Save) on the grid toolbar to persist that changes in an AJAX request
Delete “Banana” from the grid
Add “Cherry” to the shopping cart grid
Click (Save) on the grid toolbar again
At this point, I received an error in the JavaScript console:
Uncaught TypeError: Cannot read properties of null (reading 'serverOffset')
at Object._resequenceServerOffset (modelViewBase.min.js?v=24.2.2:4:40633)
at Object._clearChanges (modelViewBase.min.js?v=24.2.2:4:39956)
at modelViewBase.min.js?v=24.2.2:4:13563
How Collection Assigns SEQ_ID
The APEX_COLLECTION package assigns a unique sequence ID to each member as you add it. It uses the straightforward approach of using one greater than the maximum sequence ID currently in the collection, or one (1) if the collection is empty.
In the above scenario, my “Banana” row got assigned SEQ_ID = 1. Then when I deleted “Banana” and inserted “Cherry,” at save time APEX Grid Automatic DML page process performs deletes first. This removed “Banana” from the collection, leaving no members. Then the insert of “Cherry” was performed. At that time, since there were no members in the collection, “Cherry” was a assigned a sequence of one (1).
When the grid’s AJAX save request returned, it needs to consolidate the rows in its client-side model with the new data returned from the server. At that time, its in-memory row set it still has “Banana” with primary key value 1 (marked as deleted). But in the new data returned in the AJAX response, it finds a new row “Cherry” also with primary key value 1 marked as a new row. At this point the model gets confused because it assumes the primary keys never change unless it’s the grid-assigned temporary key changing to the database-assigned key value during insert.
Using a SYS_GUID Instead
The solution is to generate a unique key value and store that into the collection in one of the generic columns. Here we’ll use C002 to store a value we’ll assign using the SYS_GUID() function. By then configuring the grid to use this unchanging unique value, the above use case works fine.
Shopping Cart Package API
I created the following PL/SQL package API to encapsulate my use of the APEX Collections API behind a more meaningful set of function and procedure names related to adding items to a shopping cart:
The shopping cart API above hides the details of working with the APEX_COLLECTION package from the rest of my application pages. A final touch was creating a database view to hide references to the APEX_COLLECTIONS generic view from my pages as well. I created the following view:
create or replace view shopping_cart_v as
select c001 as item_code,
c002 as id,
n001 as quantity,
d001 as need_by_date
from apex_collections
where collection_name = 'SHOPPING_CART'
The Solution Sample App
This let me create a grid based on the SHOPPING_CART_V view and configure the grid’s DML page process to have custom PL/SQL-based save-time logic based on the SHOPPING_CART_API package. The Grid DML page process settings appear below. Notice I’ve disabled row locking and lost update protection. Since the collection data is private to the current user session, there’s no chance another user can lock or change a row.
The complete code for the PL/SQL Code to Insert/Update/Delete looks like this:
Since APEX treats all page items and grid columns as strings, I use the GET_NUMBER and GET_TIMESTAMP functions in the APEX_SESSION_STATE package to get a correctly typed number and date value, respectively. These functions automatically take into account any format mask configured on the item or column if present.
Declaratively Wiring Button to Grid Action
The demo includes two pages. Page 1 has a grid with the toolbar turned off and footer turned off, forcing the save to happen using a normal page submit (Save) button. Since the (Add Item) button is part of the grid toolbar, if the toolbar is hidden the end user has no way to add a row. So I wanted to put an (Add Item) button on the page that let users add a new row to the grid.
My colleague John Snyders taught me a declarative technique for doing this by:
Assigning the grid a static ID like shoppingcart
Setting the Action of the button to Defined by Dynamic Action, and
Configuring the button’s Custom Attributes property to be data-action="[shoppingcart]selection-add-row"
This instantly made my button perform the grid’s native add row action and let me position the button anywhere on my page instead of having to enable the grid toolbar just to have the (Add Item) button.
One Final Quandry Resolved
After configuring the (Add Item) button, I was puzzled when I noticed my button label “Add Item” got changed at runtime to “Add Row“. It turns out when you declaratively “wire” a button to an action, then on page load the button label takes on the action label automatically. The grid lets you configure the label of the (Add Row) toolbar button, however that property value is only visible and editable and persisted if the grid toolbar is on. So, with no developer-overridden action label saved for the “Add Row” operation in the grid region metadata, it reverts to the default label of “Add Row”.
I solved this by setting the label of the selection-add-row action to “Add Item” in my grid region’s JavaScript initialization function using the following tiny bit of code:
I later learned I could do avoid writing the line of JavaScript that sets the action label by adding an additional custom attribute data-no-update="true" to my (Add Item) button. John pointed me to the Buttons section of the grid JavaScript docs that mentions this tip in a sentence that says, “If you don’t want the label, title, or icon to be updated add [this] attribute…”
Processing the Shopping Cart Contents
The sample app does not do this, but just for completeness know that your app can process the contents of the shopping cart using a PL/SQL cursor for loop. This use case offers another compelling reason to create a view to hide direct access to the APEX_COLLECTIONS view. It makes your business logic easier to read as well. Such a loop in this shopping cart example would look like:
for j in (select item_code, quantity, need_by_date
You can download the sample app from here to install in APEX 24.2 or later. It includes Supporting Objects code for the SHOPPING_CART_API package you can study further. Enjoy!
When an APEX workflow assigns a human task to one or more potential owners, it shows up in their “Universal Task List” page of “todo” items. Sometimes an action task or an approval task requires the task owner to input additional data to inform the business process the workflow is orchestrating. The task detail page that you generate from the task definition edit page is the page end-users will navigate to when they act on a task in their “inbox”. This page is your starting point for exposing as much or as little of the built-in APEX human task functionality as you want to. You can also customize the page to capture additional data.
I created a small sample app illustrating several different techniques for collecting data from the task owner using customized task detail pages. In the video below, I explain some important APEX Workflow and Human Task concepts in the process of demoing the app and showing how I built it. The sample app imagines a simple business process of a shipping company for a shipped package being tracked in their system through several steps involving human tasks:
An associate determines the Value Added Tax due on the package
A customer service agent assigns a certain number of loyalty points
A marketing rep assesses whether to apply a discount to the shipment fees
A customs agent clears the package and adds any clearance fees
It was quickest for me to record an impromptu live webinar explaining the sample, so I include that video explaining the sample below. You can download the APEX 24.2 sample application from here, and follow along with the video that explains how the sample works.
One required setup step is editing the five app settings under Shared Components > Application Settings to change the SMUENCH username to the username you’d like the sample to use as the workflow and task owner instead. Nota Bene: please enter your username in UPPERCASE.
After you’ve adjusted these settings to reflect your own username in UPPERCASE, then you’re ready to run and follow along with the video explanation below. Hope it helps understand the various options available to you as you develop your business processes in your APEX apps.