<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Innovative Insights into AI, Oracle APEX, ORDS, Database and OCI]]></title><description><![CDATA[Posts about AI and delivering innovative enterprise-grade solutions with Cloud and Oracle Database technologies.]]></description><link>https://blog.cloudnueva.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1674092061429/gQLmQrS4z.png</url><title>Innovative Insights into AI, Oracle APEX, ORDS, Database and OCI</title><link>https://blog.cloudnueva.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 07 Jun 2026 04:43:18 GMT</lastBuildDate><atom:link href="https://blog.cloudnueva.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI Agents Need Boundaries, Not Bigger Prompts]]></title><description><![CDATA[Introduction
One of the first things you learn when building a useful AI agent is that the hard part is not calling the model. The harder part is deciding what the model should be allowed to see.
I re]]></description><link>https://blog.cloudnueva.com/ai-agents-need-boundaries-not-bigger-prompts</link><guid isPermaLink="true">https://blog.cloudnueva.com/ai-agents-need-boundaries-not-bigger-prompts</guid><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 04 Jun 2026 14:17:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/1a786fa4-9756-47e8-b235-ea165a5a2f2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>One of the first things you learn when building a useful AI agent is that the hard part is not calling the model. The harder part is deciding what the model should be allowed to see.</p>
<p>I recently ran into this while building an agent to manage Questions, Risks, and Issues (QRIs) in an Oracle APEX project management application. The agent can search existing QRIs, find people related to the project, create new QRIs, update existing ones, and help users navigate the project content.</p>
<p>That sounds straightforward until you remember that a project can contain a lot of data. Users naturally ask broad questions like:</p>
<ul>
<li><p>"Show me all open issues."</p>
</li>
<li><p>"What are the risks for this project?"</p>
</li>
<li><p>"Find anything related to payment terms."</p>
</li>
</ul>
<p>Those are reasonable requests. However, if the agent responds by dumping every matching row into the prompt, the UI, or the conversation history, the experience quickly deteriorates.</p>
<p>In this post, I will walk through some of the context management lessons from building this agent, how Retrieval-Augmented Generation (RAG) helped limit the result set, and a few practical patterns I would recommend for anyone building agents inside APEX or database-backed applications.</p>
<h1>More Context Is Not Always Better</h1>
<p>A common early instinct with AI agents is to give the model as much context as possible and let it figure it out. If the model has more data, surely it can give a better answer.</p>
<p>For an application agent, context has a cost:</p>
<ul>
<li><p>it consumes tokens</p>
</li>
<li><p>it slows down responses</p>
</li>
<li><p>it gives the model more opportunities to focus on the wrong thing</p>
</li>
<li><p>it makes debugging harder</p>
</li>
<li><p>it increases the chance of exposing internal identifiers or unrelated business data</p>
</li>
<li><p>it can overwhelm the user when the response mirrors the size of the input</p>
</li>
</ul>
<p>In my QRI agent, I had two context problems.</p>
<p><strong>First</strong>, the model needed sufficient information to answer questions and perform actions safely. If a user asked to update a QRI, the agent needed to resolve which QRI they meant, preserve the correct workflow status, and avoid using IDs supplied directly by the user.</p>
<p><strong>Second</strong>, users often wanted to "see more" than was useful. A user may request all matching data, but that does not mean the agent should become an export feature. The agent's goal is to help the user act, not to replace every report in the application.</p>
<h1>Start With Runtime Context, Not Database Dumps</h1>
<p>The agent prompt includes a small runtime context block with values such as:</p>
<ul>
<li><p>logged-in user display name</p>
</li>
<li><p>logged-in user person ID</p>
</li>
<li><p>active project ID</p>
</li>
<li><p>current date, time, and time zone</p>
</li>
</ul>
<p>This is the kind of context that should always be explicit and controlled. The agent should not infer that the project is active from the user's message. It should not trust a project ID typed into the chat. It should not accept person IDs from the user.</p>
<p>In the prompt, I made the runtime context authoritative:</p>
<blockquote>
<p>Always pass the active project ID from the runtime context. Never use a user-supplied project ID.</p>
</blockquote>
<p>That may sound simple, but it is an important boundary. The model can be flexible with language, but the application must be strict with authority.</p>
<p>That said, in APEX applications, the session already knows who the user is and which page or business object they are working with. The agent tools should <strong>always</strong> use that server-side context. It should not let the chat box become a backdoor for switching tenants, projects, users, or source records.</p>
<p>In APEX terms, the model should not become the authority for session state. APEX session state should supply the active project context. Page items, authorization schemes, application context, and PL/SQL APIs should still define what the user can see and change.</p>
<p>Authorization schemes should guard the target pages and actions. PL/SQL APIs should validate access independently of the model. Opaque links such as <code>qri://</code> can be resolved by the application into safe APEX URLs using <code>APEX_PAGE.GET_URL</code>. Debug logs should go into an application-specific AI interaction table, not just transient APEX debug output.</p>
<h1>Use Tools as Context Gates</h1>
<p>The QRI agent does not receive all project data up front. Instead, it receives tools that expose narrowly scoped slices of data:</p>
<ul>
<li><p>search subsections</p>
</li>
<li><p>list project team</p>
</li>
<li><p>search QRIs</p>
</li>
<li><p>get project details</p>
</li>
<li><p>create QRI</p>
</li>
<li><p>update QRI</p>
</li>
</ul>
<blockquote>
<p>This is one of the most useful mental models for agent design: tools are not only capabilities but also context gates.</p>
</blockquote>
<p>A tool defines what the agent can ask for, which filters it must provide, how many rows it can receive, and which fields are returned. That is much safer than letting the model query arbitrary SQL or injecting large JSON payloads into every conversation turn.</p>
<p>For example, the QRI search tool supports structured filters such as type, status grouping, owner, assignee, priority, section, subsection, and semantic search text. The prompt tells the agent to use the narrowest filters available before retrieval.</p>
<p>That instruction matters because users do not always phrase requests as filters, but the agent can often infer them:</p>
<ul>
<li><p>"open issues" means <code>type_code = ISSUE</code> and <code>status_code = OPEN</code></p>
</li>
<li><p>"high priority risks" means <code>type_code = RISK</code> and <code>priority_code = HIGH</code></p>
</li>
<li><p>"assigned to me" means the assignee should come from the runtime context</p>
</li>
</ul>
<p>This keeps the prompt smaller and the answer more relevant.</p>
<h1>RAG as a Limiting Mechanism</h1>
<p>The most useful change was using RAG to limit QRI search results.</p>
<p>For semantic searches, the agent does not retrieve and return every QRI. It generates an embedding of the user's search text and compares it against vector chunks representing questions, risks, and issues in the active project. It then selects the closest candidates, applies a distance threshold, and passes only matching QRI IDs into the main relational query.</p>
<p>In the package, this is controlled with constants like:</p>
<pre><code class="language-sql">gc_qri_vector_candidate_max CONSTANT PLS_INTEGER := 50;
gc_qri_vector_max_distance  CONSTANT NUMBER      := 0.55;   -- Cosine
gc_qri_search_result_max    CONSTANT PLS_INTEGER := 125;
gc_qri_content_max_chars    CONSTANT PLS_INTEGER := 300;
</code></pre>
<p>The exact numbers are application-specific, but the pattern is the important part.</p>
<p>The vector search is not the final answer. It is a narrowing step. Once the candidate QRI IDs are identified, the normal relational query still applies the project ID, QRI type, status, owner, assignee, priority, and section filters.</p>
<p>That combination is powerful:</p>
<ul>
<li><p>vector search handles fuzzy user language</p>
</li>
<li><p>SQL filters enforce business rules</p>
</li>
<li><p>row limits prevent oversized responses</p>
</li>
<li><p>field truncation keeps each result compact</p>
</li>
<li><p>the final ordering is predictable</p>
</li>
</ul>
<p>This is where RAG is easy to misunderstand. It is not just a way to "make the AI smarter." In business applications, RAG is also a way to avoid giving the AI too much.</p>
<h1>Return Counts, Not Just Rows</h1>
<p>The QRI search result includes values like:</p>
<ul>
<li><p><code>total_count</code></p>
</li>
<li><p><code>returned_count</code></p>
</li>
<li><p><code>max_allowed</code></p>
</li>
<li><p><code>has_more_matches</code></p>
</li>
</ul>
<p>The prompt then asks the agent to state how many matches were made overall and how many were returned. If more matches exist, the agent should show the returned records and suggest narrowing the filters.</p>
<p>This avoids a poor user experience in which the agent silently drops results. It also avoids the opposite problem, where the agent tries to be helpful by offering pagination inside the chat.</p>
<p>For this agent, I explicitly told it not to paginate results or offer the next range. That was intentional. If a user needs to review a large result set, the application should provide a report. The agent should help narrow and act.</p>
<p>There is a difference between "I found 125 of 600 matching records; narrow by section, priority, or status" and "Here are the first 125, would you like the next 125?" The second version turns the agent into a slow report viewer.</p>
<h1>Truncate Content Before It Reaches the Model</h1>
<p>Another practical choice was truncating QRI content and responses before returning them to the model. In the tool logic, QRI content is capped to a small number of characters using <code>DBMS_LOB.SUBSTR</code> after stripping HTML.</p>
<p>This is not only about token savings. It also changes the agent's behavior.</p>
<p>When the model receives short result summaries, it is more likely to summarize, compare, and guide the user. When it receives full long-form content for many records, it is more likely to drown in details or repeat them back.</p>
<p>For search results, the agent usually needs sufficient information to identify the item and determine its relevance. It does not need every character of every answer.</p>
<p>If the user truly needs the full record, the application can provide a link. In this agent, QRI results include an opaque <code>qri://</code> link that the UI converts into an APEX page URL using <code>APEX_PAGE.GET_URL</code>. The model can display the link, but it is instructed not to expose raw database IDs.</p>
<p>That gives users a way to drill down without sending the full payload to the chat.</p>
<h1>Separate Model Context From UI Context</h1>
<p>One design detail I liked in this implementation was the separation of what is stored, what is sent back to the model, and what is shown in the UI.</p>
<p>Tool results are stored in a log table, but the visible conversation only shows a reduced preview, such as "Tool result captured." The next model turn gets the structured <code>llm_context</code>, not necessarily the entire raw display payload.</p>
<p>The UI response also undergoes redaction to remove business IDs, including project, section, subsection, person, QRI, and client IDs.</p>
<p>That may seem defensive, but it is worth doing. Models are very good at repeating what they see. If internal IDs appear in tool results, prompts, or hidden messages, they will eventually make it into a user-facing response unless you actively prevent it.</p>
<p>A better pattern is:</p>
<ul>
<li><p>use internal IDs inside tool calls</p>
</li>
<li><p>return user-facing references in responses</p>
</li>
<li><p>expose links as opaque application links</p>
</li>
<li><p>redact accidental ID leakage before rendering</p>
</li>
<li><p>validate all IDs again in PL/SQL before writes</p>
</li>
</ul>
<blockquote>
<p>Redaction is useful, but it should be treated as a fallback. The better design is to avoid putting raw IDs into model-visible or user-visible text unless the model truly needs them.</p>
</blockquote>
<h1>Put Write Operations Behind Confirmation</h1>
<p>Context management is not only about reads. Writes need even stricter control.</p>
<p>For create and update tools, the agent queues the requested action and returns a confirmation request to the UI. The write is only executed after the user confirms. The tools also prevent mixing create and update requests in the same batch.</p>
<p>This creates a clean boundary:</p>
<ol>
<li><p>The model interprets the request.</p>
</li>
<li><p>The application prepares a pending action.</p>
</li>
<li><p>The user confirms.</p>
</li>
<li><p>PL/SQL executes the write after validating access, status, people, source location, and IDs.</p>
</li>
</ol>
<p>That pattern reduces the risk of the model acting on ambiguous context. It also gives the user a compact preview of what will change without dumping the entire record set into the conversation.</p>
<h1>Keep the Prompt Opinionated</h1>
<p>The agent prompt is fairly detailed. It defines authority, tool rules, retrieval behavior, status mappings, people resolution, section targeting, QRI references, write safety, and response style.</p>
<p>This is necessary because agent behavior is partly application behavior. If you leave too much open-ended, the model will choose differently from one turn to the next.</p>
<p>A few prompt rules that proved useful:</p>
<ul>
<li><p>use runtime context as authoritative</p>
</li>
<li><p>never trust user-supplied IDs</p>
</li>
<li><p>ask one concise clarification when required fields are missing</p>
</li>
<li><p>do not partially write a batch</p>
</li>
<li><p>preserve tool result ordering</p>
</li>
<li><p>state returned counts</p>
</li>
<li><p>do not expose raw IDs</p>
</li>
<li><p>use the narrowest retrieval filters</p>
</li>
<li><p>do not invent statuses, people, counts, or document facts</p>
</li>
</ul>
<p>These are not personality instructions. They are application rules.</p>
<h1>Other Context Management Practices</h1>
<p>First, treat conversation history as a liability after a certain point. Keep enough recent history to maintain continuity, but do not blindly send the entire conversation forever. Older tool results can be summarized, referenced, or re-fetched when needed.</p>
<p>Second, design tools around user intent, not tables. A <code>project_qri_search</code> tool is easier for an agent to use safely than a generic <code>run_sql</code> tool because it encodes the business boundary.</p>
<p>Third, return structured JSON to the model. The model can work with prose, but structured fields reduce ambiguity and make the prompt rules easier to enforce.</p>
<p>Fourth, use separate limits for different surfaces. A report may show 1,000 rows. A tool may return 125 compact rows. A confirmation message may preview only 8 items. Those are different jobs.</p>
<p>Fifth, log enough to debug the agent loop without storing more sensitive payload than necessary. In this package, each major step logs elapsed time: building chat messages, preparing prompt context, calling the model, parsing tool calls, and executing tools. When agents behave strangely, these timings and payload boundaries are extremely useful.</p>
<p>Finally, remember that security still belongs in the database and application layer. The model can be instructed to behave, but PL/SQL should still assert project access, validate people, check statuses, and reject invalid source locations.</p>
<h1>Conclusion</h1>
<p>Building this agent reminded me that context management is one of the core design skills for practical AI applications.</p>
<p>The goal is not to give the model everything. The goal is to give it the smallest useful slice of information, at the right time, through a tool that enforces the same business rules your application already depends on.</p>
<p>RAG helped because it limited broad semantic searches to relevant candidates before the relational filters and row limits were applied. But RAG was only part of the answer. The full solution also needed runtime authority, structured tools, truncation, count reporting, ID redaction, confirmation gates, and server-side validation.</p>
<p>That may sound like a lot of plumbing, but this is the difference between a demo agent and an application agent. A demo can be impressive with a large prompt and a few lucky examples. A real agent needs boundaries.</p>
]]></content:encoded></item><item><title><![CDATA[The anatomy of an APEX 26.1 APEXlang file]]></title><description><![CDATA[Introduction
If you have spent any time reviewing traditional APEX export SQL, you know the problem. The application is there, but it is buried inside hundreds of calls to internal APIs. You can versi]]></description><link>https://blog.cloudnueva.com/the-anatomy-of-an-apex-26-1-apexlang-file</link><guid isPermaLink="true">https://blog.cloudnueva.com/the-anatomy-of-an-apex-26-1-apexlang-file</guid><category><![CDATA[apex_lang]]></category><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Sat, 16 May 2026 18:56:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/40e6e75d-21e6-4a55-82bc-440e8f3a59ee.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>If you have spent any time reviewing traditional APEX export SQL, you know the problem. The application is there, but it is buried inside hundreds of calls to internal APIs. You can version it, search it, and deploy it, but reading it fluently is hard work.</p>
<p>To be fair, this is not the first time APEX has given us something better than one huge SQL export. Older versions of APEX could export an application using the <strong>Split into Multiple Files</strong> option, and APEX has also had a human-readable YAML export format. Those were useful, especially for review and source control, but the YAML format was read-only, and the split SQL export was fundamentally SQL export syntax.</p>
<p>APEXlang makes that structure easier to work with. It mirrors the APEX Builder mental model in a source format that is intended to be read as APEX metadata: applications contain pages, pages contain regions and components, shared components live together, and SQL, PL/SQL, JavaScript, CSS, and HTML still appear where you would expect them.</p>
<p>In this post, I will walk through the anatomy of an APEXlang <code>.apx</code> file and show how to build a mental model of an app quickly.</p>
<h1>The Folder Structure</h1>
<p>An APEXlang export is not one giant SQL file. It is exported as a zip file that expands into a folder structure.</p>
<p>For the <code>customers</code> sample app, the tree looks like this:</p>
<pre><code class="language-text">customers/
|-- application.apx
|-- page-groups.apx
|-- pages/
|   `-- p00001-dashboard.apx
|   `-- p00050-customer.apx
|-- shared-components/
|   |-- app-computations.apx
|   |-- app-items.apx
|   |-- app-processes.apx
|   |-- authentications.apx
|   |-- authorizations.apx
|   |-- breadcrumbs.apx
|   |-- build-options.apx
|   |-- classic-navigation-bar-entries.apx
|   |-- component-settings.apx
|   |-- legacy-data-load-definitions.apx
|   |-- lists.apx
|   |-- lovs.apx
|   |-- messages.apx
|   |-- plugins/region/APEXLANG-14855697926908483213/{custom-attributes.apx,plugin.apx}
|   |-- shortcuts.apx
|   |-- static-files.apx
|   |-- report-layouts/report-layouts.apx
|   |-- report-layouts/CUSTOMER-REPORT.rtf
|   |-- static-files/icons/app-icon-32.png
|   `-- themes/universal-theme/{template-option-groups.apx,theme.apx}
|-- supporting-objects/
|   |-- deinstall-script.sql
|   |-- install-scripts.apx
|   |-- install-scripts/activities.sql
|   |-- supporting-objects.apx
|   |-- substitutions.apx
|   |-- upgrade-scripts.apx
|   `-- upgrade-scripts/upgrade-eba-cust-spec-and-body.sql
|-- deployments/default.json
|-- .apex/apexlang.json
`-- workspace-components/app-groups/APEX-184853421316436653.apx
</code></pre>
<p><code>application.apx</code> is the application-level definition. This is where you find the application name, version, authentication, authorization, navigation, theme, globalization, security settings, substitutions, JavaScript, CSS, and other global configuration.</p>
<p><code>pages</code> contains one file per page. The filenames are practical: <code>p00001-home.apx</code>, <code>p00003-event-details-p3-event-name.apx</code>, <code>p00050-customer.apx</code>, and so on. You can usually tell the page number and purpose before opening the file.</p>
<p><code>shared-components</code> contains the things you would expect from APEX Builder: LOVs, lists, breadcrumbs, authentications, authorizations, app items, app processes, themes, plugins, messages, build options, static files, and report layouts.</p>
<p><code>supporting-objects</code> contains installation, upgrade, substitution, and deinstallation metadata. The actual SQL scripts can live under folders such as <code>supporting-objects/install-scripts</code> and <code>supporting-objects/upgrade-scripts</code>.</p>
<p><code>deployments</code> contains deployment configuration. In the examples, <code>default.json</code> maps the app to an application ID.</p>
<p><code>.apex</code> contains APEXlang metadata. In the examples, <code>.apex/apexlang.json</code> identifies the APEXlang metadata version.</p>
<p>This structure matters because it lets you navigate an app the same way you think about an app. That idea existed before APEXlang, but APEXlang makes the files easier to read. You can edit, validate, and import them back into APEX.</p>
<h1>What Lives in <code>application.apx</code></h1>
<p>Open <code>application.apx</code> first.</p>
<p>It starts with an <code>app</code> block:</p>
<pre><code class="language-apexlang">app TEAM-CALENDAR (
    name: Team Calendar
    version: 24.2.1
    authentication {
        publicUser: APEX_PUBLIC_USER
        authenticationScheme: @administration-rights
    }
    navigation {
        homeUrl: f?p=&amp;APP_ID.:1:&amp;SESSION.
    }
)
</code></pre>
<p>That reads much closer to a configuration file than an export script. You can scan it and answer basic questions quickly:</p>
<ul>
<li><p>What is this application called?</p>
</li>
<li><p>What authentication scheme does it use?</p>
</li>
<li><p>Is authorization configured globally?</p>
</li>
<li><p>What is the home page?</p>
</li>
<li><p>Which theme is current?</p>
</li>
<li><p>Are there global substitutions?</p>
</li>
<li><p>Is custom CSS or JavaScript included?</p>
</li>
</ul>
<p>You will also see the same APEX ideas you already know: session management, security, globalization, navigation, Progressive Web App settings, and substitutions.</p>
<p>Read <code>application.apx</code> like the App Definition screen in APEX Builder. Do not try to understand every reference yet. Get the global shape first.</p>
<h1>How Pages Are Modeled</h1>
<p>Page files are where APEXlang becomes especially useful.</p>
<p>A page starts with a <code>page</code> block:</p>
<pre><code class="language-apexlang">page 1 (
    name: Home
    alias: HOME
    title: &amp;APPLICATION_TITLE. - Home
    appearance {
        pageTemplate: @/standard
    }
)
</code></pre>
<p>After that, the file is organized around page components. In the examples, pages contain:</p>
<ul>
<li><p><code>region</code></p>
</li>
<li><p><code>item</code></p>
</li>
<li><p><code>button</code></p>
</li>
<li><p><code>dynamicAction</code></p>
</li>
<li><p><code>validation</code></p>
</li>
<li><p><code>computation</code></p>
</li>
<li><p><code>process</code></p>
</li>
<li><p><code>branch</code></p>
</li>
</ul>
<p>Again, this follows the APEX Builder mental model. To understand a page, scan the top-level component blocks first.</p>
<p>Regions often contain their own nested configuration. A report region may include a source block, layout settings, template choices, columns, actions, conditions, and attributes.</p>
<pre><code class="language-apexlang">region APEX$1553489748373581675 (
    name: Events Calendar
    type: calendar
    source {
        location: localDatabase
        type: sqlQuery
        sqlQuery:
            ```sql
            select e.event_id
            ,      e.event_name
            from   eba_ca_events e
            ```
    }
)
</code></pre>
<p>That is the key pattern: the declarative APEX component is modeled structurally, and the executable code remains embedded in a fenced block.</p>
<p>For form or dialog pages, I would scan in this order:</p>
<ol>
<li><p>Page name, alias, template, and security.</p>
</li>
<li><p>Regions, especially the main form region.</p>
</li>
<li><p>Items and their source/default/session state behavior.</p>
</li>
<li><p>Buttons and button positions.</p>
</li>
<li><p>Processes and branches.</p>
</li>
<li><p>Validations and dynamic actions.</p>
</li>
</ol>
<p>You can usually tell whether a page is display-only, a report, a modal form, or a complex transactional page in a couple of minutes.</p>
<h1>Shared Components Become Readable</h1>
<p>Shared components are split into focused files. That is one of the biggest readability wins.</p>
<p>For example, <code>shared-components/lovs.apx</code> contains <code>lov</code> blocks. A static LOV contains nested <code>entry</code> blocks. A SQL-based LOV contains a source block with the SQL query.</p>
<pre><code class="language-apexlang">lov APEX$14836072312031628364 (
    name: USERNAME_FORMAT
    source {
        location: staticValues
    }

    entry APEX$14836072618328628365 (
        sequence: 1
        display: Email Address
        return: EMAIL
    )
)
</code></pre>
<p>Lists work the same way. <code>shared-components/lists.apx</code> contains <code>list</code> blocks, and each list contains <code>entry</code> blocks with labels, icons, links, current-page logic, and parent-child relationships.</p>
<p>App processes live in <code>shared-components/app-processes.apx</code>. Authentication schemes live in <code>shared-components/authentications.apx</code>. Plugins live under <code>shared-components/plugins</code>, separated by plugin type and id. Themes live under <code>shared-components/themes</code>.</p>
<p>This is much easier to review than a traditional SQL export because the file boundaries match the APEX Builder navigation, and the file contents are not wrapped in API calls.</p>
<h1>Understanding References</h1>
<p>APEXlang uses references heavily, and learning the reference style is what makes the files click.</p>
<p>You will see <code>@...</code> references in exported files. These are references to APEX components by generated identifier. For example, an application may point to an authentication scheme or navigation list using an <code>@...</code> value.</p>
<p>You will also see more readable references based on static IDs and names:</p>
<pre><code class="language-apexlang">authentication {
    scheme: @oracle-apex-accounts
}
userInterface {
    currentTheme: @universal-theme
}
navigationMenu {
    list: @navigation-menu
}
</code></pre>
<p>That matters because APEX 26.1 adds static IDs across APEX components, and APEXlang uses those IDs to make references more stable and readable.</p>
<p>You will also see <code>@/...</code> references, especially for templates:</p>
<pre><code class="language-apexlang">pageTemplate: @/standard
listTemplate: @/side-navigation-menu
buttonTemplate: @/text-with-icon
</code></pre>
<p>These are easier to read because the component is referenced by a friendly path-like name.</p>
<p>Page item references still look like APEX page item references. SQL and PL/SQL blocks use bind variables such as <code>:P50_ID</code>, <code>:APP_ID</code>, and <code>:APP_USER</code>. URL targets still use familiar APEX substitution syntax, such as <code>f?p=&amp;APP_ID.:1:&amp;SESSION.</code>.</p>
<p>Named component references also appear naturally in places such as authorization checks, build option logic, template references, and links.</p>
<p>The practical rule is simple: if you see <code>@...</code>, you are looking at a declarative component reference. If you see <code>:Pxx_ITEM</code>, you are looking at the session state. If you see <code>&amp;APP_ID.</code> or <code>#COLUMN#</code>, you are in familiar APEX substitution territory.</p>
<h1>The Hybrid Reality</h1>
<p>APEXlang structures the declarative layer, but it does not remove code from APEX.</p>
<p>SQL queries still live in report sources, LOVs, conditions, and supporting object scripts. PL/SQL still lives in processes, validations, conditions, computations, and application processes. JavaScript can still appear in page JavaScript settings, dynamic actions, and static files. CSS can still be inline on a page or stored as a static file. HTML still appears in help text, templates, report HTML expressions, and PL/SQL output.</p>
<p>APEXlang is not pretending APEX is something it is not. It provides metadata with a readable structure while preserving the hybrid nature of real APEX applications. Just as important, that structure supports real round-tripping: you can search and replace across files, generate apps with AI, validate the result, and import the application back into APEX.</p>
<h1>How the Oracle APEXlang Skill Fits In</h1>
<p>The APEXlang files are only half the story. Oracle also released an <a href="https://github.com/oracle/skills/tree/main/apex/apexlang">APEXlang AI skill package</a> to help agents work with this format safely.</p>
<p>That package is not just a pile of examples. It includes routing metadata, component catalogs, templates, runtime helpers, SQLcl adapters, and validation tools. The skill provides an agent with a workflow for finding the app, loading the correct local context, choosing the appropriate templates, checking component properties against compiler-backed truth, validating the generated APEXlang, and importing only when explicitly approved.</p>
<p>That safety model matters. APEXlang is writable, but it is still application metadata. A good agent workflow should not guess table names, invent columns, silently pick a workspace, or import generated code because the prompt sounded confident. The skill expects authoritative context, such as table metadata, a data model, an API contract, or a live database connection, before it generates schema-dependent APEXlang.</p>
<p>For live validation or import, the practical requirements are also explicit: APEX 26.1 with APEXlang support, SQLcl 26.1.2 or newer, Java 17 or 21, a saved SQLcl connection name, and the corresponding APEX workspace name. The default workflow is check-only first. Import is a separate step that should happen only after the APEXlang check passes and the developer approves it.</p>
<p>For APEX 26.1, APEXlang import is an application import. Single-page APEXlang import is not supported in this release. That does not reduce the value of the format, but it does affect how you plan review, validation, and deployment workflows.</p>
<h1>Conclusion</h1>
<p>The most useful way to think about APEXlang is this:</p>
<blockquote>
<p>APEXlang is a readable and writeable source format for an Oracle APEX application.</p>
</blockquote>
<p>It builds on ideas APEX has already explored with split exports and readable YAML, but gives developers a practical source format for navigating application metadata without decoding traditional export SQL.</p>
<p>It is still APEX. Your pages, shared components, SQL, PL/SQL, JavaScript, CSS, and HTML are all still there. They are just arranged in a format that humans can read, search, diff, validate, and learn from before importing the application back into APEX.</p>
<p>Once you understand the folder structure, page model, shared component files, and reference syntax, <code>.apx</code> files become much less intimidating. You can open an unfamiliar application and start building a useful mental model quickly, which is exactly what a source format should help you do.</p>
]]></content:encoded></item><item><title><![CDATA[AI Hygiene for APEX Developers]]></title><description><![CDATA[Introduction
AI-assisted development for Oracle APEX work is moving beyond one-off prompts and into something much more structured.
We now have reusable skills, repo-level instruction files, agent con]]></description><link>https://blog.cloudnueva.com/ai-hygiene-for-apex-developers</link><guid isPermaLink="true">https://blog.cloudnueva.com/ai-hygiene-for-apex-developers</guid><category><![CDATA[orclapex]]></category><category><![CDATA[apex_lang]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Sun, 26 Apr 2026 23:33:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/8aabf09f-2bfc-4272-8aa5-23f610279f84.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>AI-assisted development for Oracle APEX work is moving beyond one-off prompts and into something much more structured.</p>
<p>We now have reusable skills, repo-level instruction files, agent configuration, project standards, prompt libraries, and more detailed tooling around code generation and review. That is useful. It also creates a new problem: your AI context can get messy very quickly.</p>
<p>In this post, I want to look at what I mean by AI hygiene, why it matters for APEX and PL/SQL developers, and where I have already seen it go wrong.</p>
<h1>What I Mean by AI Hygiene</h1>
<p>When I say AI hygiene, I mean keeping the instruction layer around the model clean, up to date, and free of conflicts.</p>
<p>For Oracle developers, that instruction layer can include:</p>
<ul>
<li><p>Oracle-provided skills: <a href="https://github.com/oracle/skills">oracle-skills</a> and APEXlang skills (when they are released)</p>
</li>
<li><p>community-developed skills</p>
</li>
<li><p>your own PL/SQL or APEX skills</p>
</li>
<li><p><code>CLAUDE.md</code> or <code>AGENTS.md</code></p>
</li>
<li><p>code that is already in the repo</p>
</li>
<li><p>repo README files</p>
</li>
<li><p>MCP tool descriptions</p>
</li>
<li><p>prompt libraries</p>
</li>
</ul>
<p>That is a lot of context for a model to reconcile. If those sources line up, the results can be very good. If they do not, the model will often produce something that looks plausible, compiles cleanly, and still doesn't do what you want it to.</p>
<p>That is why I think of AI hygiene as code hygiene for the instruction layer.</p>
<blockquote>
<p>Reducing unnecessary or conflicting context can lower token usage (and therefore cost) and improve time to first token, especially as tools, skills, and repo guidance accumulate.</p>
</blockquote>
<h1>Why This Matters More for Oracle Work</h1>
<p>Oracle development is unusually context-sensitive.</p>
<p>A decent answer is not just about knowing PL/SQL syntax or APEX component names. It also depends on version compatibility, security patterns, naming conventions, package structure, logging standards, deployment rules, and sometimes very specific local constraints around ORDS, APIs, and the database architecture.</p>
<p>For example, a generic Oracle skill might reasonably recommend things like:</p>
<ul>
<li><p>use packages for reusable business logic</p>
</li>
<li><p>avoid dynamic SQL where possible</p>
</li>
<li><p>prefer bind variables</p>
</li>
<li><p>use clear exception handling</p>
</li>
<li><p>account for version-specific syntax</p>
</li>
</ul>
<p>None of that is wrong. The problem is that your project may also require:</p>
<ul>
<li><p>all business logic to go through package APIs</p>
</li>
<li><p>no core logic in APEX page processes</p>
</li>
<li><p>a specific logging package or wrapper</p>
</li>
<li><p>compatibility with Oracle Database 19c</p>
</li>
<li><p>compatibility with a specific APEX version until a future upgrade is complete</p>
</li>
</ul>
<p>At that point, the question is no longer, "Is the model good at Oracle?" The real question is, "Which instruction wins?"</p>
<h1>The Problem Is Not Bad Syntax</h1>
<p>When people worry about AI-generated code, they often focus on whether it compiles. That is a fair concern, but it is not the one I worry about most. The more dangerous failure mode is plausible wrongness. In other words, code that is technically valid but wrong for your application.</p>
<p>That can look like:</p>
<ul>
<li><p>valid PL/SQL in the wrong package</p>
</li>
<li><p>valid SQL that ignores tenant isolation</p>
</li>
<li><p>valid APEX page logic placed in the wrong layer</p>
</li>
<li><p>valid ORDS handlers with the wrong response structure</p>
</li>
<li><p>valid exception handling that hides errors</p>
</li>
</ul>
<blockquote>
<p>In practice, that kind of output is more dangerous than obvious AI slop because it creates both review overhead and false confidence.</p>
</blockquote>
<h1>Where I Saw This Happen</h1>
<p>One of the clearest examples I have seen was with a PL/SQL-focused skill I was using while developing APEX applications.</p>
<p>The skill itself was helpful. It pushed the model toward better package structure, cleaner SQL, stronger exception handling, and more Oracle-aware output. On its own, that was a net positive.</p>
<p>The issue was that I also had <code>AGENTS.md</code> files sitting in individual repositories, which had grown organically over time. Some predated AI skills entirely. Some reflected older project conventions. Some made sense for one app but not another. Some were simply too broad.</p>
<p>So I had two different instruction sources operating at once:</p>
<ul>
<li><p>the skill was trying to enforce general Oracle and PL/SQL best practices</p>
</li>
<li><p>the repo-level <code>AGENTS.md</code> files were trying to enforce local architecture and workflow rules</p>
</li>
</ul>
<p>That sounds fine until those two sources disagree.</p>
<p>I would get output that mixed conventions:</p>
<ul>
<li><p>package structure from the skill</p>
</li>
<li><p>naming from the repo file</p>
</li>
<li><p>exception handling from an old example</p>
</li>
<li><p>architecture decisions from whichever instruction happened to dominate in that run</p>
</li>
</ul>
<p>Nothing about that output was obviously broken. That was the problem. It was technically reasonable code, but it still required cleanup because the instruction environment was dirty. That kind of issue becomes more common as we add more reusable AI context, not less.</p>
<h1>Why APEX Makes This Even More Interesting</h1>
<p>This becomes even more relevant for APEX developers when APEX 26.1 (and APEXlang) is released, enabling us to use AI to generate APEX applications.</p>
<p>Once your application structure, component metadata, and declarative configuration are more accessible to AI-driven workflows, the upside is obvious. You can imagine better review tooling, better automation, better generation, and better assistance around large APEX applications.</p>
<p>However, that also increases the number of instruction surfaces that can conflict.</p>
<p>For example, you may end up balancing:</p>
<ul>
<li><p>a general Oracle skill</p>
</li>
<li><p>an APEX-specific skill</p>
</li>
<li><p>project architecture rules</p>
</li>
<li><p>UI conventions</p>
</li>
<li><p>security constraints</p>
</li>
<li><p>task-specific prompts</p>
</li>
</ul>
<p>That is exactly why AI hygiene matters. The more an AI can touch, the more important it becomes to control the rules that guide those changes.</p>
<h1>A Better Mental Model: Instruction Layers</h1>
<p>The cleanest way I have found to think about this is to separate instruction authority from repository reality.</p>
<p>Some things are explicit instructions. Some are evidence of how the application actually works today. Those are not the same, and treating them as the same is where much of the confusion starts.</p>
<h2>Instruction Authority</h2>
<pre><code class="language-text">    Vendor or ecosystem skills
                |
                v
      Internal reusable skills
       and community skills
                |
                v
             AGENTS.md
                |
                v
            Task prompt

Reality check

    Existing repository code
    (current implementation)
</code></pre>
<h2>1. Vendor or Ecosystem Skills</h2>
<p>These provide broad technology competence.</p>
<p>This is where I would want Oracle-focused guidance, SQL and PL/SQL best practices, documentation-backed patterns, and general platform knowledge to live.</p>
<p>These skills should make the model better at Oracle. They should not try to encode every project-specific convention you have.</p>
<p>This includes Oracle-provided skills when they exist, but I would not automatically assume vendor-provided skills are more authoritative than a well-maintained internal skill. The real distinction is between broad platform knowledge and local rules.</p>
<h2>2. Internal Reusable Skills and Community Skills</h2>
<p>This is where your own reusable skills and outside community skills usually sit.</p>
<p>For example, this layer may include:</p>
<ul>
<li><p>internal APEX architecture patterns</p>
</li>
<li><p>shared package templates</p>
</li>
<li><p>organization-specific API conventions</p>
</li>
<li><p>approved approaches for ORDS, JSON, or security wrappers</p>
</li>
<li><p>coding, logging, security, or naming standards reused across repositories</p>
</li>
</ul>
<p>This is often the most important layer in practice because it bridges the gap between general Oracle competence and the needs of a real delivery team.</p>
<h2>3. Project or Repository Instructions</h2>
<p>This is where <code>CLAUDE.md</code> or <code>AGENTS.md</code> sits. The job of this layer is to explain how this specific application works:</p>
<ul>
<li><p>project context</p>
</li>
<li><p>project naming conventions</p>
</li>
<li><p>technology versions (APEX 26.1, Oracle DB 19c, etc.)</p>
</li>
<li><p>project-specific architecture rules</p>
</li>
<li><p>testing and deployment expectations</p>
</li>
<li><p>approved exceptions to broader standards</p>
</li>
</ul>
<p>This file should not become a junk drawer for every Oracle best practice you have ever liked.</p>
<blockquote>
<p>Ideally, your AGENTS.md should be less than a dozen bullet points. The bulk of the instructions should come from skills that can be reused for all of your projects.</p>
</blockquote>
<h2>4. Existing Repository Code</h2>
<p>This is not just another instruction file. It is an implementation reality.</p>
<p>The codebase shows:</p>
<ul>
<li><p>what patterns are actually in use</p>
</li>
<li><p>what package boundaries already exist</p>
</li>
<li><p>what naming is really present</p>
</li>
<li><p>what versions and compatibility assumptions the project appears to follow</p>
</li>
<li><p>where the written instructions may already be stale</p>
</li>
</ul>
<p>If <code>AGENTS.md</code> says one thing and the repository consistently does another, that is not a simple question of precedence. It is a hygiene problem that needs to be resolved deliberately.</p>
<p>Vendor skills should teach the model the platform. Internal reusable skills should teach your conventions. <code>AGENTS.md</code> should teach the project. The codebase should teach reality.</p>
<h2>5. Task Prompt</h2>
<p>This is the immediate ask:</p>
<ul>
<li><p>review this package</p>
</li>
<li><p>generate this API</p>
</li>
<li><p>refactor this page process</p>
</li>
<li><p>propose an APEX app structure</p>
</li>
</ul>
<p>The prompt should clearly describe the job, but it should not be forced to restate everything already in the higher layers.</p>
<p>The rule I would use is simple:</p>
<blockquote>
<p>Put instructions at the lowest stable layer where they belong.</p>
</blockquote>
<p>And when written instructions conflict with the existing codebase, stop pretending the hierarchy is clean. That is the moment to decide whether the code needs refactoring or the instructions need updating.</p>
<h1>Common Sources of AI Context Rot</h1>
<p>Once you start looking for it, instruction drift shows up everywhere.</p>
<p>The usual problems are:</p>
<ul>
<li><p>stale <code>AGENTS.md</code> files that describe an older architecture</p>
</li>
<li><p>older examples that teach the model outdated patterns</p>
</li>
<li><p>duplicated rules spread across skills, READMEs, and agent files</p>
</li>
<li><p>version assumptions that no longer match production reality</p>
</li>
<li><p>copied prompt fragments nobody wants to delete</p>
</li>
<li><p>unclear authority between skill guidance and repo guidance</p>
</li>
</ul>
<p>Typically, the worst cases are not dramatic. They are subtle. You just start seeing output that feels slightly off. The model is not exactly wrong, but it is clearly pulling from incompatible instructions.</p>
<h1>A Checklist for Better AI Hygiene</h1>
<p>If you want to improve this without turning it into a process nightmare, I would start with a short checklist.</p>
<h2>Inventory your instruction sources</h2>
<p>List every place your AI tooling can pull guidance from:</p>
<ul>
<li><p>skills</p>
</li>
<li><p><code>AGENTS.md</code></p>
</li>
<li><p>README files</p>
</li>
<li><p>coding standards</p>
</li>
<li><p>templates</p>
</li>
<li><p>prompt libraries</p>
</li>
<li><p>examples</p>
</li>
<li><p>existing code</p>
</li>
</ul>
<h2>Decide what wins</h2>
<p>For each category, define the default source of truth and what counts as the reality check.</p>
<p>For example:</p>
<ul>
<li><p>PL/SQL style usually comes from internal reusable skills or shared templates</p>
</li>
<li><p>generic Oracle best practice comes from a vendor, ecosystem, or internal skill</p>
</li>
<li><p>cross-project architecture conventions come from your own reusable skills</p>
</li>
<li><p>project-specific knowledge, software versions, and local constraints come from <code>AGENTS.md</code></p>
</li>
<li><p>output formatting comes from the task prompt</p>
</li>
</ul>
<p>If you do not define authority, the model will improvise it for you. If you do not check that authority against the current codebase, you will miss drift.</p>
<h2>Search for conflict words</h2>
<p>Look for words like:</p>
<ul>
<li><p>always</p>
</li>
<li><p>never</p>
</li>
<li><p>must</p>
</li>
<li><p>avoid</p>
</li>
<li><p>required</p>
</li>
<li><p>deprecated</p>
</li>
<li><p>version</p>
</li>
<li><p>exception</p>
</li>
<li><p>security</p>
</li>
<li><p>logging</p>
</li>
</ul>
<p>Those words tend to reveal hidden conflicts very quickly.</p>
<p>In an APEX repo, a quick example is a project rule that says all business logic must live behind packaged APIs while the prompt or a reusable skill keeps generating logic directly in page processes. Both outputs may be valid. Only one matches the architecture.</p>
<h2>Remove duplicate rules</h2>
<p>If the same instruction exists in five places, it will eventually drift.</p>
<p>I would rather have:</p>
<ul>
<li><p>a skill that provides broad Oracle guidance</p>
</li>
<li><p>an internal reusable skill or shared template that defines mandatory patterns</p>
</li>
<li><p>a repo file that only documents local exceptions and architecture</p>
</li>
</ul>
<p>That is much easier to maintain.</p>
<h2>Test your instruction stack</h2>
<p>This is the part I think many people will skip, and they should not.</p>
<p>Use a few repeatable prompts such as:</p>
<ul>
<li><p>generate a package for expense approval logic</p>
</li>
<li><p>review this APEX page process for security issues</p>
</li>
<li><p>create an ORDS GET handler for employee expenses</p>
</li>
<li><p>write SQL compatible with Oracle Database 19c</p>
</li>
</ul>
<p>Then check whether the output actually follows your standards.</p>
<p>If the same prompt produces different architectural decisions depending on which repo you run it in, you probably have an AI hygiene problem.</p>
<h1>Conclusion</h1>
<p>AI-assisted Oracle development is not just about better models and better prompts anymore. It is also about managing the instruction environment around the model.</p>
<p>That means skills, standards, repo rules, examples, and prompts all need to work together rather than compete for control. I have already seen how a useful PL/SQL skill can become less useful when it collides with inconsistent <code>AGENTS.md</code> files spread across repositories. The model was not the weak link there. My instruction layer was.</p>
<p>As Oracle-focused skills become more common and AI-driven workflows become more practical for APEX and PL/SQL teams, I think this only gets more important.</p>
<p>Your instructions are no longer just notes for the model. They are part of your development environment now, and they deserve to be maintained with the same care as code.</p>
]]></content:encoded></item><item><title><![CDATA[If English is the New Programming Language, then Markdown is the New Format]]></title><description><![CDATA[Introduction
AI is changing how we build software. We are moving from a world where developers primarily describe systems in code to one where we increasingly describe intent in natural language. Prom]]></description><link>https://blog.cloudnueva.com/markdown-is-the-new-format-for-ai</link><guid isPermaLink="true">https://blog.cloudnueva.com/markdown-is-the-new-format-for-ai</guid><category><![CDATA[orclapex]]></category><category><![CDATA[apex_lang]]></category><category><![CDATA[markdown]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 16 Apr 2026 11:57:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/a942bbdd-d5da-4392-a8b5-01f0d0c0c4fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>AI is changing how we build software. We are moving from a world where developers primarily describe systems in code to one where we increasingly describe intent in natural language. Prompts, instructions, specifications, and structured text are becoming part of the development process itself. In that sense, English is becoming a specification interface for software generation.</p>
<blockquote>
<p>But English alone is not enough.</p>
</blockquote>
<p>Natural language is flexible, expressive, and easy for humans. It is also messy. It drifts. It is inconsistent. It leaves room for interpretation. That is fine for conversation. It is less fine when you want an AI system to reliably generate an application, a page definition, a requirements document, or a presentation.</p>
<blockquote>
<p>That is why Markdown matters.</p>
</blockquote>
<p>If English is becoming the language of AI-driven development, Markdown is becoming one of the most practical formats for making that language usable. It gives natural language just enough structure to be repeatable, parsable, lightweight, and machine-readable without turning it back into code.</p>
<p>For APEX developers, this matters more than most people realize. APEX has always been about metadata, declarative development, and reducing friction between business intent and working software. APEXlang appears to push that same idea further. Instead of hand-building every artifact, we will increasingly define applications, pages, workflows, and requirements in structured natural language and let AI turn those definitions into implementation.</p>
<p>That is why Markdown is such a strong fit for the AI era, and especially for where APEX appears to be heading.</p>
<h1>Markdown hits the sweet spot</h1>
<blockquote>
<p>Markdown is powerful because it is simple.</p>
</blockquote>
<p>A heading is a heading. A list is a list. A table is a table. A code block is a code block. You can read it as plain text, write it quickly, version it easily, and transform it into other formats without carrying the overhead of a heavyweight document format.</p>
<blockquote>
<p>That makes Markdown ideal for AI.</p>
</blockquote>
<p>Large language models work best when the input is mostly meaning rather than formatting noise. Markdown preserves structure, but it does not bury the meaning inside layers of layout instructions, visual positioning, embedded objects, theme metadata, and export artifacts. The model sees the content clearly.</p>
<p>This is where Markdown has a big advantage over Word documents, slide decks, and PDFs. Those formats were designed primarily for human consumption and visual rendering. Markdown is much closer to an authoring format for both humans and machines.</p>
<p>For APEX, this is especially interesting because so much of what we build already begins as semi-structured intent: application descriptions, page definitions, data requirements, business rules, acceptance criteria, UX notes, and workflow descriptions.</p>
<p>Traditionally, those things are scattered across Word docs, slides, emails, tickets, and whiteboards. In an AI-driven workflow, that fragmentation becomes a real problem. AI works better when the source material is clean, consistent, and structured.</p>
<p>Markdown gives you that structure without forcing you into a rigid syntax that business users or developers will resist.</p>
<h2>Structure</h2>
<p>Large language models do not benefit from Markdown just because it removes formatting noise. They also benefit from the predictable hierarchy Markdown provides. Headings define topic boundaries, sections group related ideas, nested lists show parent-child relationships, tables make structured comparisons explicit, and code fences clearly separate executable or literal content from prose. That consistent structure makes the content easier for a model to parse, chunk, and reason over. In practice, Markdown works well because it preserves meaning in a form that is both human-readable and machine-readable.</p>
<h1>APEXlang &amp; Blueprints</h1>
<p>APEXlang (available in APEX 26.1) will be the new syntax for APEX. At APEX World this year (also mentioned in the APEX <a href="https://apex.oracle.com/en/learn/resources/roadmap/">statement of direction</a>), we learned a little about another aspect of APEXlang called Blueprints.</p>
<p>Based on what was shown and what Oracle has signaled publicly, Blueprints are a move toward more structured, specification-driven app generation. Blueprints will likely depend on a defined Markdown structure or syntax. The fact that APEX Blueprints can be created in Markdown should mean they are both human and machine-readable.</p>
<h2>Getting off to a fast start</h2>
<p>Based on the information available, I assume Blueprints will help you accelerate version 1 of your app, and then you can iterate from there. Iteration on top of the initial build would then happen in APEX Builder, VS Code, or APEXlang.</p>
<blockquote>
<p>A business analyst writes a Blueprint spec in Markdown. This is converted to a first draft of an APEX app, SQL objects, validations, and test cases. The developer reviews and refines. The Markdown spec remains the source of intent.</p>
</blockquote>
<h2>Benefits</h2>
<p>This approach has several benefits:</p>
<ul>
<li><p>A blueprint-driven approach has the potential to be more deterministic than unconstrained AI generation.</p>
</li>
<li><p>It could shift more early-stage specification work toward analysts and product owners.</p>
</li>
<li><p>You can use version control and diffs on Blueprints as you evolve the first version of your app.</p>
</li>
</ul>
<h1>A Practical Example: Marp</h1>
<p>A tangible example of this approach (which I recently started using) is the Markdown Presentation Ecosystem, or <a href="https://marp.app/">Marp</a>. Marp is an open-source Markdown presentation ecosystem that lets you write slide decks in Markdown and turn them into presentation-ready output. It includes tools, a CLI, and can export decks to HTML, PDF, and PowerPoint. As with APEX Blueprints, you write the content, and the Marp CLI converts it to HTML, PDF, or PPTX.</p>
<blockquote>
<p>Building presentations in Markdown allows you to focus completely on the content of your presentation rather than the format.</p>
</blockquote>
<h2>Using Marp</h2>
<p>Using Marp is straightforward. You write a normal Markdown file, and each slide is separated by a horizontal rule (---). That means a deck is just a sequence of Markdown sections. You can then add Marp front matter and directives for things like theme selection, pagination, background images, layout tweaks, and presenter-friendly formatting. The official ecosystem includes the Marp CLI for converting Markdown files from the command line.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/4c1dd8bd-6c73-414b-92b7-e71e42c532b4.png" alt="How Marp Works" style="display:block;margin:0 auto" />

<div>
<div>🚀</div>
<div>The fact that Marp has a CLI means you can integrate it with your LLM via <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://agentskills.io/home" style="pointer-events:none">agent skills.</a> This allows you to generate professional presentations from a prompt!</div>
</div>

<h3>Simple Example</h3>
<pre><code class="language-markdown">---
marp: true
theme: default
paginate: true
---

# My Presentation

A slide written in Markdown.

---

## Second Slide

- Bullet one
- Bullet two
- Bullet three
</code></pre>
<h3>Generate Output</h3>
<p>Once you have your markdown, you can convert it to PDF, HTML, or PPTX from the command line.</p>
<pre><code class="language-shell"># Generate an HTML presentation using a custom CSS style
marp --theme nueva.css AI_Functions_Presentation.md -o AI_Functions_Presentation.html

# Generate a PDF presentation using a custom CSS style
marp --theme nueva.css AI_Functions_Presentation.md -o AI_Functions_Presentation.pdf

# Generate a PPTX presentation using a custom CSS style
marp --theme nueva.css AI_Functions_Presentation.md -o AI_Functions_Presentation.pptx
</code></pre>
<h1>Markdown Saves on Tokens</h1>
<p>The unit of measure of AI is tokens. Tokens are the small chunks of text that an AI model reads and generates, such as words, parts of words, punctuation, or symbols. They are the basic units of input and output, so token count affects cost, speed, and the amount of context the model can handle at once.</p>
<blockquote>
<p>The fewer tokens you use, the less your AI costs and the faster it runs.</p>
</blockquote>
<p>To test this theory, I used the Codex CLI to build a deck in Markdown and another in PPTX format. I ran both scenarios using the <code>gpt-5.4-mini</code> with <code>high</code> reasoning.</p>
<details>
<summary>Markdown/Marp Prompt</summary>
<p>Create a 10-slide presentation in valid Marp markdown.</p><p>Topic: Quarterly AI Product Strategy Review Audience: senior leadership Style: concise, analytical, executive-ready</p><p>Requirements:</p><ul><li><p>Output markdown only</p></li><li><p>Use <code>---</code> between slides</p></li><li><p>Include: title, agenda, 3 market slides, 2 product slides, 1 architecture slide, 1 roadmap slide, 1 risks slide, 1 summary slide</p></li><li><p>Use short bullets, not paragraphs</p></li><li><p>Use markdown tables where helpful</p></li><li><p>Add speaker notes for the architecture and roadmap slides</p></li><li><p>Include footer text: Cloud Nueva | Q2 Review</p></li></ul>
</details><details>
<summary>PPTX Prompt</summary>
<p>Create a 10-slide presentation as an actual PPTX file, not markdown, not HTML, and not JSON.</p><p>Topic: Quarterly AI Product Strategy Review Audience: senior leadership Style: concise, analytical, executive-ready</p><p>Requirements:</p><ul><li><p>Generate the presentation in .pptx format as part of the process</p></li><li><p>Include exactly 10 slides:</p><ol><li><p>Title</p></li><li><p>Agenda</p></li><li><p>Market Trends</p></li><li><p>Competitive Landscape</p></li><li><p>Customer Demand Signals</p></li><li><p>Product Priorities</p></li><li><p>Product Gaps and Risks</p></li><li><p>Architecture Overview</p></li><li><p>Roadmap</p></li><li><p>Summary</p></li></ol></li><li><p>Use short bullets, not paragraphs</p></li><li><p>Use a professional business theme</p></li><li><p>Add footer text on each slide: Cloud Nueva | Q2 Review</p></li><li><p>Add speaker notes for the Architecture Overview and Roadmap slides</p></li><li><p>Include at least one comparison table where appropriate</p></li><li><p>Keep wording consistent across slides</p></li><li><p>Return only the content needed to produce the PPTX file and complete the PPTX generation workflow</p></li></ul>
</details>

<h3>Results / Token Usage</h3>
<table>
<thead>
<tr>
<th>Format</th>
<th>Input Tokens</th>
<th>Output Tokens</th>
</tr>
</thead>
<tbody><tr>
<td>Markdown</td>
<td>35.4K</td>
<td>1.73K</td>
</tr>
<tr>
<td>PPTX</td>
<td>293K</td>
<td>13.8K</td>
</tr>
</tbody></table>
<blockquote>
<p>The token savings are significant.</p>
</blockquote>
<p>Now, let's see what happens to token usage when we summarize the outputs from the above...</p>
<details>
<summary>Marp Prompt</summary>
<p>Summarize this Marp markdown presentation deck.</p><p>Requirements:</p><ul><li><p>Read the full deck, slide by slide</p></li><li><p>Produce a concise executive summary</p></li><li><p>Include:</p><ul><li><p>the main thesis of the deck</p></li><li><p>the key business priorities</p></li><li><p>the major risks or constraints</p></li><li><p>the roadmap or next-step themes</p></li></ul></li><li><p>Then provide a slide-by-slide summary with 1 to 2 sentences per slide</p></li><li><p>Preserve the terminology used in the deck</p></li><li><p>Do not rewrite the deck</p></li><li><p>Do not comment on formatting unless it affects meaning</p></li></ul>
</details><details>
<summary>PPTX Prompt</summary>
<p>Summarize this PowerPoint presentation deck.</p><p>Requirements:</p><ul><li><p>Read the full deck, slide by slide, including titles, bullets, tables, and speaker notes if present</p></li><li><p>Produce a concise executive summary</p></li><li><p>Include:</p><ul><li><p>the main thesis of the deck</p></li><li><p>the key business priorities</p></li><li><p>the major risks or constraints</p></li><li><p>the roadmap or next-step themes</p></li></ul></li><li><p>Then provide a slide-by-slide summary with 1 to 2 sentences per slide</p></li><li><p>Preserve the terminology used in the deck</p></li><li><p>Do not rewrite the deck</p></li><li><p>Do not comment on visual design unless it affects meaning</p></li></ul>
</details>

<h3>Results / Token Usage</h3>
<table>
<thead>
<tr>
<th>Format</th>
<th>Input Tokens</th>
<th>Output Tokens</th>
</tr>
</thead>
<tbody><tr>
<td>Markdown</td>
<td>110K</td>
<td>1.99K</td>
</tr>
<tr>
<td>PPTX</td>
<td>232K</td>
<td>4.87K</td>
</tr>
</tbody></table>
<blockquote>
<p>Again, this test showed a dramatic reduction in token usage.</p>
</blockquote>
<p><strong>Note</strong>: Much of the additional token usage likely comes from the extra processing needed to extract usable structure and text from a binary PPTX workflow.</p>
<h1>Markdown for Specifications</h1>
<p>A useful Markdown specification does more than describe an idea at a high level. It should define the feature's purpose, business context, data involved, required behavior, constraints, and acceptance criteria. In practice, that means clearly naming entities, inputs, outputs, rules, edge cases, assumptions, and non-functional requirements where they matter. The goal is to remove ambiguity without making the document heavy or unreadable. A good Markdown spec gives both humans and AI a structured source of intent that can be reviewed, versioned, and turned into implementation with less guesswork.</p>
<p>I have <a href="https://blog.cloudnueva.com/avoiding-the-vibe-coding-rabbit-hole">written before</a> about providing AI with detailed specifications to improve AI outcomes. These specifications should be written in Markdown to allow the AI to focus on intent rather than formatting.</p>
<div>
<div>💡</div>
<div>I believe humans can also benefit from focusing on intent and not formatting!</div>
</div>

<h1>Markdown for Agents</h1>
<p>Markdown is also emerging as a practical format for presenting content to AI agents. Although HTML is well-structured, it is bulky and includes tags and formatting that add noise for agent workflows. Markdown offers a cleaner interchange format when the goal is to expose content rather than presentation.</p>
<p>Cloudflare is at the forefront of this transition. You can read more in their <a href="https://blog.cloudflare.com/markdown-for-agents/">blog post</a> on the subject.</p>
<h1>Caution</h1>
<h2>Markdown is not enough on its own</h2>
<p>Markdown is useful because it adds structure without adding much friction. But on its own, it is still just text. If you want reliable AI output, Markdown usually requires conventions.</p>
<p>That may include standard section headings, front matter, templates, naming rules, required fields, examples, and acceptance criteria. Without that extra discipline, two Markdown documents about the same thing can still vary wildly in quality and completeness.</p>
<p>In other words, Markdown is not the full solution. It is the foundation. The real value comes when teams combine Markdown with consistent patterns that make intent easier for both humans and AI to interpret.</p>
<h2>Where Markdown breaks down</h2>
<p>Markdown works best when the goal is to capture meaning, structure, and intent. It works less well when the output depends heavily on precise visual layout or rich interaction.</p>
<p>For example, Markdown is not a great fit for pixel-perfect UI design, complex diagrams, drag-and-drop experiences, or documents that rely on detailed formatting and review features such as tracked changes. It can describe those things, but it cannot fully replace the tools built for them.</p>
<p>That is the tradeoff. Markdown is an excellent lightweight source format, but not every artifact should remain in Markdown forever. In many cases, it is most valuable at the intent stage, before being transformed into something more specialized.</p>
<h1>Conclusion</h1>
<p>Markdown matters because it separates intent from presentation. It gives natural language enough structure to be reused, versioned, reviewed, and processed reliably by AI.</p>
<p>That makes it useful well beyond note-taking. It is a strong format for specifications, blueprints, prompts, presentations, and agent-facing content. Not because it is perfect, but because it is simple, structured, and efficient.</p>
<p>For APEX developers, that matters. APEX has always been about turning metadata and intent into working software. As AI and APEXlang push that model further, Markdown looks like a practical way to define what we want before tools generate what we build.</p>
]]></content:encoded></item><item><title><![CDATA[From Spreadsheet to Enterprise APEX System with AI]]></title><description><![CDATA[Introduction
This is not another post about creating an APEX app from a spreadsheet using the Create app Wizard. This post is about using AI to design and accelerate the build of an enterprise APEX sy]]></description><link>https://blog.cloudnueva.com/from-spreadsheet-to-enterprise-apex-system-with-ai</link><guid isPermaLink="true">https://blog.cloudnueva.com/from-spreadsheet-to-enterprise-apex-system-with-ai</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Sat, 11 Apr 2026 02:41:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/6512a378-9d6e-45cd-b133-3a60e2c40ace.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>This is not another post about creating an APEX app from a spreadsheet using the Create app Wizard. This post is about using AI to design and accelerate the build of an enterprise APEX system from a spreadsheet.</p>
<h1>Background</h1>
<p>A client recently asked me to build an APEX system to replace their existing spreadsheet-based expenses system. The goals were clear:</p>
<ul>
<li><p>Reduce the time from expense submission to payment</p>
</li>
<li><p>Improve the accuracy of taxes calculated for expenses</p>
</li>
<li><p>Allow management to track expenses</p>
</li>
<li><p>Reduce errors caused by manually entering AP invoices into Oracle E-Business Suite</p>
</li>
<li><p>Improve anomaly detection</p>
</li>
<li><p>Improve user experience</p>
</li>
</ul>
<p>The company’s expense rules were already embedded in the Excel template. The template included formulas for mileage reimbursement, Canadian tax calculations, and finance summaries used to enter AP invoices into Oracle EBS.</p>
<h1>An AI First Approach</h1>
<p>One of my goals for 2026 is to adopt an AI-first approach. It will not always work, but using it as the default starting point is helping me understand its limitations and get much more out of it.</p>
<blockquote>
<p>One word of caution. Just because AI cannot do something well today doesn't mean it won't be able to when the next frontier model is released. It is important that we regularly re-evaluate our perceptions of what AI is capable of.</p>
</blockquote>
<h1>Architecture</h1>
<p>At this stage, it is worth describing the client's APEX environment. They have an on-premises APEX instance running on the Oracle e-Business Suite (EBS) instance. This sits behind a firewall accessible only via VPN. They also have an OCI APEX Service instance running externally-facing APEX apps. The plan was to have expense report entry and approval run in the OCI instance, and then pull approved expense reports into the on-premises EBS instance for review and payment in Accounts Payable by finance.</p>
<ul>
<li><p>OCI owns the approval state of the expense reports</p>
</li>
<li><p>EBS owns the payment state of the expense reports</p>
</li>
<li><p>SharePoint owns receipt attachments</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/a12784d7-d4d1-428b-835a-23c3e23e9c70.png" alt="Oracle APEX Expenses System built with AI" style="display:block;margin:0 auto" />

<h1>Design</h1>
<h2>Business Rule Extraction</h2>
<p>The legacy expense report Excel template's tabs, tables, fields, and formulas essentially contained all the business rules. I started the design phase by asking Codex to analyze the Excel template and draft a Product Requirements Document (PRD) based on it. Codex produced a four-page Markdown PRD covering the business rules, entities, fields, data types, and lists of values.</p>
<h2>Verification</h2>
<p>I took the PRD and asked Codex to review it against business best practices and current Canadian tax rules. It augmented the design with rules not already in their spreadsheet. For example, Codex suggested additional mileage-rule considerations that were not explicit in the spreadsheet, which we then validated against current Canadian guidance and the client’s reimbursement policy.</p>
<h2>User Interface</h2>
<p>I then asked Codex to create graphical wireframes of the APEX pages needed for the solution. I attached several screenshots from existing APEX apps, so it could match the corporate look and feel. Codex created the wireframes using real data from an old expense report Excel. I incorporated screenshots into the design document.</p>
<h2>Business Review</h2>
<p>I then reviewed the PRD with business users to get their feedback. Being able to present the business rules in a clearly laid out document (instead of embedded in Excel formulas), and being able to show them what the new app was going to look like was significant. We made some minor updates to the design based on feedback from this review.</p>
<blockquote>
<p>At this stage, we had an approved design document and a clear path forward after only 10 hours of effort.</p>
</blockquote>
<h1>Build</h1>
<p>Using Codex, I attached the approved design document and provided an extensive prompt detailing what I wanted it to do. I split this into two prompts, one for the OCI expense entry side of the app (running on OCI) and a separate one for the on-premises side of the app. Each prompt requested:</p>
<ul>
<li><p>A comprehensive data model.</p>
</li>
<li><p>Secure views and views that abstract table-join complexity from APEX.</p>
</li>
<li><p>PL/SQL utility packages with APIs to manage email generation, REST integrations, workflow functions, and managing attachments in SharePoint.</p>
</li>
<li><p>ORDS APIs to allow the On-Premises EBS environment to fetch approved expenses for payment, and to post back to let employees know when their expenses have been paid.</p>
</li>
</ul>
<p>Along with the prompt, I included:</p>
<ul>
<li><p>Sample tables from previous apps to teach the model the table creation standards.</p>
</li>
<li><p>PL/SQL code from previous apps that had APEX workflow approvals and that used a SharePoint attachments common package that we use.</p>
</li>
<li><p>An <code>AGENTS.md</code> file to provide product versions, coding standards, formatting standards, etc.</p>
</li>
</ul>
<p>The outcome was:</p>
<ul>
<li><p>A roughly 90% complete data model with foreign keys, constraints, appropriate data types (and sizes), and comments.</p>
</li>
<li><p>Scripts to load the current tax and mileage rates from the template Excel into the new tables.</p>
</li>
<li><p>Abstraction of Canadian Provinces and Territories into Jurisdictions applicable to mileage and tax rates, which is something I would not have thought of.</p>
</li>
<li><p>Cloud and On-Premises PL/SQL packages with the helper procedures and functions that I requested.</p>
</li>
<li><p>A SQL script to create an ORDS OAuth2 Credential, ORDS module, privilege, templates and handlers.</p>
</li>
<li><p>Twenty unit test scripts to test both sides of the app.</p>
</li>
</ul>
<p>There were a few issues which were resolved with another hour or so of follow-up prompts and clarifications.</p>
<h2>APEX</h2>
<p>All that was left was to build the APEX app. This part was less fun because, at the time of writing in March 2026, <code>APEXlang</code> was not yet available. Frankly it took longer to build the APEX app than all of the other artifacts created up to this point.</p>
<p>Overall, I would estimate that what would normally have been an 80-hour project was reduced to about 40 developer hours with the help of AI. We will have to see how much lower this can go when <code>APEXlang</code> comes along.</p>
<h1>Keys to Success</h1>
<p>The following points were key to the success of this project:</p>
<ul>
<li><p>90% of the business rules were explicitly baked into the Expense Report Excel. This made it easy for the AI to extract the rules and for us to verify it had done it accurately.</p>
</li>
<li><p>Presenting the business with a PRD within a few days of starting the project (with realistic wireframes) inspired confidence that we are heading down the right path with minimal investment of time.</p>
</li>
<li><p>Splitting the build phase between on-premises and OCI allowed the model to focus on each build separately and reduced the risk of confusion between the different database versions, APEX versions, and EBS-specific coding standards.</p>
</li>
<li><p>Models respond really well to examples. Pointing AI to a package and saying "create a procedure to do X that follows the same pattern as procedure Y from another package" works very well.</p>
</li>
<li><p>Being specific about the output you expect is also key. For example, you need to specifically request test scripts, and request that they include edge cases as well as happy path tests. Combine this with SQLcl and its MCP server, and you can prompt the AI to run the test suite after every change.</p>
</li>
</ul>
<h1>Conclusion</h1>
<p>AI did not build this system on its own, but it removed a large amount of the slow, repetitive work at the start of the project. The spreadsheet already contained most of the business rules. AI helped extract those rules, turn them into a usable design, and generate much of the database and PL/SQL foundation.</p>
<p>The real value was speed and clarity. We were able to review a proper design with the business early, make corrections before build started, and cut the overall development effort significantly. The parts that still needed the most hands-on work were the APEX application itself, validation of the generated output, and the final handling of edge cases.</p>
<p>For this type of project, AI worked best as a force multiplier, not as a replacement for experience. It was useful because the source material was structured, the prompts were specific, and every output was reviewed before being used.</p>
]]></content:encoded></item><item><title><![CDATA[AI SKILLS as a Thin Layer Over MCP Tools]]></title><description><![CDATA[Introduction
I have been experimenting with using AI Skills as a thin layer on top of MCP-backed tools, and I think this pattern is more useful than it first appears.
At a technical level, MCP gives t]]></description><link>https://blog.cloudnueva.com/ai-skills-as-a-thin-layer-over-mcp-tools</link><guid isPermaLink="true">https://blog.cloudnueva.com/ai-skills-as-a-thin-layer-over-mcp-tools</guid><category><![CDATA[ords]]></category><category><![CDATA[skills]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 02 Apr 2026 11:49:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/4f7227df-25db-4377-8300-df1a1a1e7f48.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>I have been experimenting with using AI Skills as a thin layer on top of MCP-backed tools, and I think this pattern is more useful than it first appears.</p>
<p>At a technical level, MCP gives the model standardized access to external tools and context. That is valuable, but raw tool access is not always enough. A model may know that a tool exists, but still needs guidance on when to use it, how to use it, what the tool is for, and what “good usage” looks like in the context of a specific prompt.</p>
<p>That is where I am finding Skills useful.</p>
<p>Rather than thinking of a Skill as replacing an MCP server, I think of it as a focused instructional layer on top of one or more MCP tools. The Skill captures intent, usage conventions, and domain-specific behavior. In practice, that makes tool use more reliable and reduces the amount of prompting I need to do each time.</p>
<div>
<div>💡</div>
<div>In Codex, you can invoke a Skill explicitly using <code>$skill_name</code>. MCP servers do not provide that same kind of direct user-facing invocation.</div>
</div>

<h2><strong>An example with Oracle ORDS</strong></h2>
<p>To make this more practical, I built a small STDIO MCP server that exposes an Oracle ORDS REST web service on a table called <code>JD_SB_ENTRIES</code>. This table stores records in a second brain app. By “second brain,” I mean the usual personal knowledge tasks: capturing notes, storing ideas, tracking follow-ups, organizing knowledge, and retrieving things later in a structured way.</p>
<p>The ORDS side is straightforward. I registered a template for the table with handlers for <code>GET</code>, <code>POST</code>, <code>PUT</code>, and <code>DELETE</code> that map to CRUD database operations. I secured the ORDS module that the template was created in using an OAuth 2.0 client.</p>
<div>
<div>💡</div>
<div>You could also use ORDS <code>ORDS.ENABLE_OBJECT</code> to <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://www.thatjeffsmith.com/archive/2017/03/auto-rest-with-ords-an-overview-and-whats-next/" style="pointer-events:none">Auto-REST</a> enable the <code>JD_SB_ENTRIES</code> table. This generates the entire CRUD API instantly, allowing you to focus entirely on the MCP/Skill interaction layer rather than writing PL/SQL backend handlers.</div>
</div>

<p>I built an STDIO MCP Server in Python using the <a href="https://chatgpt.com/codex">Codex desktop app</a>. STDIO MCP servers run locally on your machine. The MCP server then exposes the REST APIs to the model through a tool interface.</p>
<p>The model can use the MCP tool to create, read, update, and delete rows through ORDS, without needing to know the low-level details of the HTTP call each time.</p>
<p>It works.</p>
<p>But I soon found that getting the MCP server to act on my prompts was erratic (at best). I also have Office 365 linked to my Codex desktop app setup, so the model would often choose Microsoft Planner over my tool. It would also conflict with Office 365 Calendar. It's understandable, really.</p>
<blockquote>
<p>A request like "add a task for tomorrow to clean the car" would make sense for MS Outlook just as much as for my second brain.</p>
</blockquote>
<p>This confusion from the LLM occurs despite clear instructions in the MCP server services definition on how to use the tool.</p>
<details>
<summary>YAML for the MCP Server Services</summary>
<pre class="not-prose"><code class="language-yaml">oauth:
  token_url: https://example.adb.us-chicago-1.oraclecloudapps.com/ords/demo/oauth/token
  scopes: ""

<p>services:</p>
<ul>
<li>id: jd_sb_entries
name: JD Second Brain Tasks, Notes, and Reminders
base_url: <a href="https://example.adb.us-chicago-1.oraclecloudapps.com/ords/demo/mcp/">https://example.adb.us-chicago-1.oraclecloudapps.com/ords/demo/mcp/</a>
description: "Manage second-brain entries from natural user requests. Use this service when the user wants to add, create, save, list, review, update, or delete notes, tasks, ideas, knowledge entries, or reminder-style entries with a due date. Create new entries at jd_sb_entries and update or delete existing entries at jd_sb_entries/{entry_id}. This stores reminders as second-brain tasks or notes; it does not create real Planner or calendar reminders. Infer the correct action from conversational requests whenever possible."
default_headers:
  Accept: application/json
timeout_seconds: 30
pagination:
  default_page_size: 100
  max_page_size: 250
  max_pages_per_call: 10
  max_items_per_call: 500
examples:<ul>
<li>"Use rest_mcp_server to add a todo for tomorrow: clean car."</li>
<li>Add a new task reminding me to review the ORDS spec tomorrow.</li>
<li>Save a reminder for tomorrow to review the ORDS spec.</li>
<li>Create a note about MCP server pagination and save the full details.</li>
<li>"Add this to my second brain: review the ORDS spec tomorrow."</li>
<li>Show me my second-brain entries.</li>
<li>Update entry 123 to mark it high urgency.</li>
<li>Delete entry 456.
columns:</li>
<li>name: entry_id
data_type: NUMBER
nullable: false
writable: false
description: Primary key identity column.</li>
<li>name: subject
data_type: VARCHAR2(255)
nullable: false
writable: true
description: Short subject line.</li>
<li>name: entry_type
data_type: VARCHAR2(30)
nullable: false
writable: true
description: Entry classification.
enum_values:<ul>
<li>IDEA</li>
<li>TASK</li>
<li>NOTE</li>
<li>KNOWLEDGE</li>
</ul>
</li>
<li>name: ai_summary
data_type: VARCHAR2(32767)
nullable: false
writable: true
description: AI-generated summary.</li>
<li>name: user_content
data_type: CLOB
nullable: false
writable: true
description: Full entry body.</li>
<li>name: urgency
data_type: VARCHAR2(30)
nullable: true
writable: true
description: Optional urgency.
enum_values:<ul>
<li>LOW</li>
<li>MEDIUM</li>
<li>HIGH</li>
</ul>
</li>
<li>name: action_required
data_type: VARCHAR2(1)
nullable: false
writable: true
description: Whether action is required.
enum_values:<ul>
<li>Y</li>
<li>N</li>
</ul>
</li>
<li>name: due_date
data_type: DATE
nullable: true
writable: true
description: Optional due date in YYYY-MM-DD format.</li></ul></li></ul></code></pre>



</details>

<p>The model still needs to understand what the API represents in business terms, how it should behave when used, and which requests should trigger a call. A generic CRUD interface is flexible, but also vague.</p>
<div>
<div>💡</div>
<div>One enhancement I thought of was to reference the OpenAPI/Swagger endpoint that ORDS makes available in the <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="http://skill.md" style="pointer-events:none">SKILL.md</a> file. This makes the skill more resilient to changes in the API.</div>
</div>

<h2><strong>Using a Skill for a second brain workflow</strong></h2>
<p>To counter this vagueness, I decided to create a <a href="https://agentskills.io/home">skill</a> focused specifically on the second brain ORDS API.</p>
<blockquote>
<p>Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently.</p>
</blockquote>
<p>This turned out to be more useful than I expected.</p>
<p>The Skill did three important things.</p>
<h3><strong>1. It guided the use of the REST API</strong></h3>
<p>The MCP tool exposed the API's mechanics. The Skill explained how to use it.</p>
<p>That distinction matters.</p>
<p>The tool knew how to call the endpoint. The Skill told the model when to create a note, when to update an existing item rather than insert a new one, which fields mattered, and how to interpret user requests in the context of a second brain.</p>
<p>Without that layer, the model has to infer too much from the tool signature and endpoint description. Sometimes that works. Sometimes it does not. The more domain-specific the workflow becomes, the more that gap shows up.</p>
<p>In practice, the Skill reduced a lot of that ambiguity.</p>
<h3><strong>2. It documented second brain functionality</strong></h3>
<p>The Skill also became a compact form of documentation.</p>
<p>Instead of only documenting the REST API as a technical interface, the Skill documented the behavior around the API. It explained what the second brain supports, the kinds of operations it is intended for, and the conventions the model should follow.</p>
<p>That is useful for the model and for me.</p>
<p>It gave me a single place to describe the intended workflow in practical terms rather than just API terms. In other words, it documented capability, not just transport.</p>
<p>I think this is an underrated part of Skills. They are not only prompt helpers. They can also serve as executable documentation for an AI-facing workflow.</p>
<h3><strong>3. It allowed explicit invocation with $skill</strong></h3>
<p>This was the third benefit, and in some ways, the most practical.</p>
<p>Because the behavior was packaged as a Skill, I could explicitly invoke it with $skill_name.</p>
<p>That gave me a clean way to direct the model toward a very specific behavior package. I was not just hoping the model would choose the right MCP tool based on a vague request. I could point it at the exact Skill that I knew would work with that second brain API.</p>
<p>That explicit invocation made the interaction more predictable.</p>
<details>
<summary>SKILL.md</summary>
<pre class="not-prose"><code class="language-markdown">---
name: "second-brain"
description: "Use when the user wants to add, update, list, or delete second-brain notes, tasks, ideas, knowledge items, or reminder-style entries through the local rest-mcp server. Prefer this skill when the user explicitly says $second-brain."
---

<h1>Second Brain</h1>
<p>Use this skill for second-brain CRUD work through the local <code>rest-mcp</code> MCP server.</p>
<h2>Core Rules</h2>
<ul>
<li>Use <code>service_id: "jd_sb_entries"</code>.</li>
<li>Use <code>path: "jd_sb_entries"</code> for create and list. Use <code>path: "jd_sb_entries/{entry_id}"</code> for a specific row.</li>
<li>For filtered <code>GET</code> requests, use ORDS <code>q</code> filter syntax, not ad hoc column query params.</li>
<li>Preferred form: pass <code>query</code> as a native object and pass <code>query.q</code> as a native object. The MCP server will JSON-encode <code>q</code>.</li>
<li>Accepted alternate form: pass <code>query</code> as a raw query string such as <code>q={"entry_type":{"$eq":"TASK"}}&amp;amp;limit=25</code>.</li>
<li>Do not send second-brain filters as top-level keys like <code>"entry_type": "TASK"</code> unless the service explicitly documents that parameter.</li>
<li>For structured arguments, verify <code>body</code> and <code>headers</code> are native objects before calling the tool. For <code>query</code>, prefer a native object unless a raw query string is more direct.</li>
<li>Use <code>page_limit</code> and <code>item_limit</code> for pagination.</li>
<li>If fields, enum values, or filter keys are unclear, call <code>rest-mcp.describe_service</code> once. Do not retry blindly with alternate query formats.</li>
<li>Use ORDS operators inside <code>q</code> as needed: <code>\(eq</code>, <code>\)ne</code>, <code>\(instr</code>, <code>\)like</code>, <code>\(gte</code>, <code>\)lte</code>, <code>\(or</code>, <code>\)and</code>.</li>
<li>Keep list results compact and results-focused.</li>
<li>Never paste raw MCP response JSON into the user-facing reply. Extract the needed fields and summarize.</li>
</ul>
<h2>Fixed Playbooks</h2>
<ul>
<li>If the user asks to show, list, pull, or review active todos/tasks/reminders, make exactly one <code>GET</code> call with:</li>
</ul>
<pre><code class="language-json">{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "entry_type": {
        "$eq": "TASK"
      },
      "action_required": {
        "$eq": "Y"
      }
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}
</code></pre>
<ul>
<li>For that active-task flow, do not probe with alternative query formats, do not call <code>describe_service</code>, and do not say "retrying" unless an unexpected runtime error actually occurred.</li>
<li>After fetching active tasks, sort by <code>due_date</code> ascending before replying unless the user asks for a different order.</li>
<li>Reply with only the compact task list: <code>#entry_id subject — due YYYY-MM-DD</code>.</li>
</ul>
<h2>Batch Rules</h2>
<ul>
<li>Default to single-item mode.</li>
<li>Enter batch mode only when the user clearly asks for multiple items or refers to a concrete earlier list.</li>
<li>For prior-thread items, restate a compact working list in the current turn before writing.</li>
<li>If the earlier items are missing or ambiguous, ask the user to narrow the scope or restate them.</li>
<li>Process at most 5 items per turn unless the user explicitly asks for more.</li>
<li>Create or update sequentially, one <code>request_resource</code> call per item.</li>
<li>If a batch partially succeeds, report completed items and the first failure clearly.</li>
</ul>
<h2>Field Mapping</h2>
<ul>
<li><code>todo</code>, <code>task</code>, <code>reminder</code> -&gt; <code>entry_type: "TASK"</code></li>
<li><code>note</code> -&gt; <code>entry_type: "NOTE"</code></li>
<li><code>idea</code> -&gt; <code>entry_type: "IDEA"</code></li>
<li><code>knowledge</code> -&gt; <code>entry_type: "KNOWLEDGE"</code></li>
<li>For todos/reminders, default <code>action_required</code> to <code>"Y"</code>.</li>
<li>Default <code>urgency</code> to <code>"LOW"</code> unless the user says otherwise.</li>
<li>Use title case for <code>subject</code> unless the user specifies exact casing.</li>
<li>Use the raw user text or a slightly cleaned version for <code>user_content</code>.</li>
<li>Create a short <code>ai_summary</code> from the request.</li>
<li>Convert relative dates like <code>tomorrow</code> into an absolute <code>YYYY-MM-DD</code> date using the user's locale timezone.</li>
<li>Treat returned <code>due_date</code> values as ISO timestamps and present them back to the user as dates when only the date matters.</li>
</ul>
<h2>Request Patterns</h2>
<p>Create:</p>
<pre><code class="language-json">{
  "service_id": "jd_sb_entries",
  "method": "POST",
  "path": "jd_sb_entries",
  "body": {
    "subject": "Clean car",
    "entry_type": "TASK",
    "ai_summary": "Reminder to clean the car tomorrow.",
    "user_content": "clean car",
    "urgency": "LOW",
    "action_required": "Y",
    "due_date": "2026-03-15"
  }
}
</code></pre>
<p>List active tasks:</p>
<pre><code class="language-json">{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "entry_type": {
        "$eq": "TASK"
      },
      "action_required": {
        "$eq": "Y"
      }
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}
</code></pre>
<p>Search for entries containing a phrase:</p>
<pre><code class="language-json">{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "$or": [
        {
          "subject": {
            "$instr": "ORDS"
          }
        },
        {
          "user_content": {
            "$instr": "ORDS"
          }
        }
      ]
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}
</code></pre>
<p>Read one row:</p>
<pre><code class="language-json">{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries/32"
}
</code></pre>
<p>Bad <code>query</code> examples:</p>
<pre><code class="language-json">"query": {
  "entry_type": "TASK",
  "action_required": "Y"
}
</code></pre>
<pre><code class="language-json">"query": "{\"entry_type\":{\"$eq\":\"TASK\"}}"
</code></pre>
<p>Good raw query-string example:</p>
<pre><code class="language-json">"query": "q={\"entry_type\":{\"\(eq\":\"TASK\"},\"action_required\":{\"\)eq\":\"Y\"}}&amp;amp;limit=25"
</code></pre>
<h2>Response Style</h2>
<ul>
<li>For simple creates, reply with the created <code>entry_id</code>, subject, and due date.</li>
<li>For list/read requests, return the concise result only. Do not echo tool payloads, headers, links, or pagination blobs.</li>
<li>Keep the response short.</li>
<li>If the request is ambiguous, ask one concise clarifying question.
</li></ul></code></pre></details>

<h1>Demo</h1>
<p>This recording shows a brief interaction with my 2nd brain after introducing the skill.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/9ee8b60f-d79b-40cb-9536-ba1035f8a6c8.gif" alt="Demo showing use of 2nd brain from the Codex app" style="display:block;margin:0 auto" />

<h1><strong>Why this pattern matters</strong></h1>
<p>The broader point is that MCP and Skills solve different problems.</p>
<div>
<div>💡</div>
<div>MCP is about tool access. Skills are about tool usage.</div>
</div>

<p>I like this analogy from the Anthropic "<a href="https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf">The Complete Guide to Building Skills for Claude</a>".</p>
<blockquote>
<p><strong>The kitchen analogy.</strong></p>
<p><strong>MCP provides the professional kitchen</strong>: access to tools, ingredients, and equipment. <strong>Skills provide the recipes</strong>: step-by-step instructions on how to create something valuable.</p>
</blockquote>
<p>If you only expose a tool, you are giving the model capability. If you add a Skill, you are giving it operating guidance. For simple tools, that extra layer may not matter much. For anything with workflow, conventions, or domain context, it matters a lot.</p>
<p>That is why I think Skills work well as a thin layer on top of MCP-backed tools.</p>
<ul>
<li><p>They do not replace the server.</p>
</li>
<li><p>They do not replace the API.</p>
</li>
<li><p>They do not replace good tool design.</p>
</li>
</ul>
<p>What they do is close the gap between “the model can call this” and “the model knows how this should be used here.”</p>
<h1><strong>Conclusion</strong></h1>
<p>A lot of MCP discussions focus on exposing tools, which makes sense. But once you start building real workflows, raw tool exposure is only the starting point. You also need a way to shape behavior around those tools.</p>
<p>For me, Skills are proving to be a good way to do that.</p>
<p>In this case, a simple STDIO MCP server exposed ORDS REST APIs for CRUD operations on a table. The Skill sitting on top of one of those APIs made the setup much more usable by guiding the workflow, documenting the behavior, and providing an explicit invocation surface via $skill.</p>
<p>That is a small design choice, but it has made the overall system feel much more intentional.</p>
]]></content:encoded></item><item><title><![CDATA[Will AI Agents Replace UI, or Redefine It?]]></title><description><![CDATA[Introduction
In a previous post, Adding an AI Agent to an Existing APEX App, I described how I added an AI agent to an existing APEX app. The goal was to simplify the user interface by providing an ag]]></description><link>https://blog.cloudnueva.com/will-ai-agents-replace-ui-or-redefine-it</link><guid isPermaLink="true">https://blog.cloudnueva.com/will-ai-agents-replace-ui-or-redefine-it</guid><category><![CDATA[orclapex]]></category><category><![CDATA[AI]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 19 Mar 2026 11:11:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/df1eca2c-d5ac-456f-bca9-66a2463c3b70.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>In a previous post, <a href="https://blog.cloudnueva.com/adding-ai-agent-to-apex-app"><strong>Adding an AI Agent to an Existing APEX App</strong></a><strong>,</strong> I described how I added an AI agent to an existing APEX app. The goal was to simplify the user interface by providing an agent driven by a simple text interface.</p>
<p>As an APEX developer, this got me thinking: Are we heading towards a future where there is less focus on building APEX pages and more focus on building AI agents and the controls they require? Could agents completely replace UI?</p>
<p>I do not think agents will replace the user interface. But I do think they will redefine it.</p>
<h1>Why enterprise apps look the way they do today</h1>
<p>For years, we have built APEX apps around a simple assumption: a user operates the app. They open a page. They find the right menu. They enter data into a form. They click save. Then they move to the next screen and repeat.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/7a83ee5e-4304-424e-b9fe-62dcd47435a2.png" alt="APEX Page Illustrating a Traditional Enterprise App" style="display:block;margin:0 auto" />

<p>That model is so familiar that it feels permanent. But it is not. It is mostly a workaround for the fact that traditional software has needed humans to drive every step.</p>
<div>
<div>💡</div>
<div>AI agents call that assumption into question.</div>
</div>

<p>If an agent can understand an instruction, gather context, decide what steps are required, and carry them out across one or more systems, what exactly is left for the user interface to do?</p>
<p>That is no longer a theoretical question. It is becoming a practical one.</p>
<p>A lot of enterprise software still revolves around transaction entry, status updates, approvals, routing, and repetitive record management. In many cases, the interface is not valuable because it is a great experience. It is valuable because it is the mechanism the system uses to make the user do the work.</p>
<p>That is where agents become disruptive.</p>
<p>Instead of forcing a user to navigate five screens and populate twelve fields, the interaction could start with something much closer to natural intent:</p>
<blockquote>
<p>Create a new customer for Acme (details for Acme can be found in the CRM system), generate a sales order for 1,000 Aztec 100's using standard new customer pricing, send it for approval, and remind me next Tuesday if it has not been signed.</p>
</blockquote>
<p>We don't need many APEX pages to implement this!</p>
<h1><strong>From manual operation to delegated execution</strong></h1>
<p>The most important change is not that software becomes conversational. The real change is that software no longer requires the user to translate business intent into system steps.</p>
<p>That translation has defined enterprise UX for decades. Users have had to know where to go, what fields matter, what sequence to follow, what validations apply, and which screen comes next. The interface has been the place where human intention gets broken down into machine-friendly actions.</p>
<div>
<div>💡</div>
<div>Agents can absorb a lot of that burden.</div>
</div>

<p>That means the APEX app no longer has to be organized primarily around pages. It can be organized around goals, actions, and outcomes.</p>
<p>That is a major shift.</p>
<h1>Where the “single text box” idea becomes useful</h1>
<p>Once you accept that agents can handle more of the operational work, the obvious next question is whether the app can be reduced to a simple prompt box.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/f1158f29-a5dd-4767-8651-f021449ae635.png" alt="APEX Page Showing Simple Text Box Agent" style="display:block;margin:0 auto" />

<p>Probably not, but for some tasks, that actually makes sense.</p>
<p>Routine work is a strong candidate:</p>
<ul>
<li><p>Create a new supplier using the attached Invoice.</p>
</li>
<li><p>Open a support request for this issue...</p>
</li>
<li><p>Summarize sales by cost center for last month and compare it to this time last year</p>
</li>
<li><p>Put a credit hold on Acme Corp</p>
</li>
<li><p>Inactivate Item ABC</p>
</li>
</ul>
<p>In those cases, the old interface often exists only because the system required structured, manual interaction. If the agent can reliably handle that structure, the screen becomes optional.</p>
<p>That is why this topic is not far-fetched. It points to a real weakness in much current software: too much of the interface exists because the software is rigid, not because the user actually benefits from the interaction.</p>
<h1><strong>The future is probably not just a text box</strong></h1>
<p>A text box is excellent for expressing intent. It is weak for verification, comparison, supervision, and control. That matters.</p>
<p>It is easy to say:</p>
<blockquote>
<p>Reconcile these transactions and close the period.</p>
</blockquote>
<p>It is much harder to trust that outcome without seeing:</p>
<ul>
<li><p>What exceptions were found</p>
</li>
<li><p>Which records were changed</p>
</li>
<li><p>What assumptions were made where confidence was low</p>
</li>
<li><p>What could not be completed cleanly</p>
</li>
<li><p>What still needs human approval</p>
</li>
</ul>
<p>That is why I do not buy the lazy version of the argument that “UI is dead” or that “everything becomes chat.”</p>
<p>The better argument is that AI agents may eliminate a large percentage of the UI that exists purely for manual execution, while making a different kind of UI more important than ever.</p>
<h1><strong>The UI does not disappear. Its job changes.</strong></h1>
<p>I think that is the real story. The interface of the future is less about entering data and more about supervising action.</p>
<p>That means the valuable parts of the UI become things like:</p>
<ul>
<li><p>previewing what the agent is about to do</p>
</li>
<li><p>approving consequential actions</p>
</li>
<li><p>inspecting reasoning or decision traces</p>
</li>
<li><p>handling exceptions</p>
</li>
<li><p>reviewing changes across systems</p>
</li>
<li><p>enforcing policy and permissions</p>
</li>
<li><p>reversing or correcting bad outcomes</p>
</li>
<li><p>understanding what happened and why</p>
</li>
</ul>
<p>That is still UI. It is just no longer centered on the idea that the user must manually drive every step of the workflow.</p>
<p>In fact, once agents take over more of the mechanical burden, the remaining interface becomes more strategic. It becomes the place where trust is earned.</p>
<h1><strong>Enterprise systems will change unevenly</strong></h1>
<p>Some interfaces are much more vulnerable than others.</p>
<p>Low-risk, repetitive, high-volume workflows are the easiest targets. Administrative tasks, routine service requests, report generation, standard approvals, record creation, and straightforward updates are all likely to be heavily compressed by agentic interaction.</p>
<div>
<div>💡</div>
<div>But high-stakes systems are different.</div>
</div>

<p>Finance, healthcare, procurement, compliance, and regulated workflows require more than just correct execution. They need visibility, auditability, traceability, and control.</p>
<p>In those environments, the agent may do more of the work, but the interface is not going away. It is becoming the control surface.</p>
<p>That is a very different design challenge from building page flows and forms, and it is more interesting.</p>
<h1><strong>What does this mean for us?</strong></h1>
<p>For a long time, the default design question has been: What pages do we need? That question is starting to look outdated.</p>
<p>A better set of questions is:</p>
<ul>
<li><p>Which parts of this workflow truly require human judgment?</p>
</li>
<li><p>Which inputs are genuinely necessary?</p>
</li>
<li><p>Which fields exist only because the system cannot infer context?</p>
</li>
<li><p>Where can intent replace navigation?</p>
</li>
<li><p>Where can the agent act safely on the user’s behalf?</p>
</li>
<li><p>What needs to be visible before a human will trust the result?</p>
</li>
<li><p>How do we design for intervention, not just execution?</p>
</li>
</ul>
<p>That changes how we think about app design.</p>
<p>It pushes us away from page-centric systems and toward systems built around delegation, observability, and recovery.</p>
<p>For enterprise platforms in particular, that is a serious shift. The future is not just better forms. It is designing the boundary between autonomous action and human control.</p>
<h1>What happens to APEX?</h1>
<p>If the future of enterprise software relies on agents executing tasks and humans supervising them, APEX is still positioned well; if we change how we build.</p>
<p>We need to stop thinking of APEX primarily as a rapid CRUD builder and start treating it as an <strong>Agent Control Plane</strong>. The infrastructure to build this supervisory UI already exists within the APEX ecosystem; it simply needs to be repurposed.</p>
<p>Here is how APEX architecture must adapt to an agent-driven model:</p>
<ul>
<li><p><strong>From Page Processes to Agent-Ready APIs:</strong> An agent cannot click a button to fire an APEX Page Process. Business logic must be rigorously decoupled from the UI. We need to expose strict, deterministic Oracle REST Data Services (ORDS) or self-contained PL/SQL packages. These become the literal "tools" the agent invokes to interact with the database.</p>
</li>
<li><p><strong>Human-in-the-Loop via the Approvals Component:</strong> When an agent attempts a high-stakes action or encounters ambiguity, it should not fail silently. Instead, the agent's backend process can start an APEX workflow instance. The Unified Task List becomes the "Supervisory UI," where humans review the agent's proposed action, inspect its reasoning, and approve or reject the action.</p>
</li>
<li><p><strong>Handling Asynchronous Agent State:</strong> Many AI agents operate asynchronously, often taking seconds or minutes to multi-step through a problem. Traditional APEX pages are synchronous. To bridge this gap, we can use APEX Background processes and APEX Automations to run agents in the background and use push notifications to send status updates to the client.</p>
</li>
<li><p><strong>Auditability:</strong> In regulated environments, auditability requires more than a record of what changed. Future APEX apps will need dedicated agent log tables to capture the task, supporting evidence, tool invocations, confidence signals, performed validations, and a concise decision summary. That trace should surface alongside the business record in the APEX UI to establish trust and traceability.</p>
</li>
</ul>
<h1>Conclusion</h1>
<div>
<div>❓</div>
<div>So is this the end of user interfaces as we know them?</div>
</div>

<p>If by “user interface” we mean page-heavy, form-heavy, navigation-heavy systems built around manual data entry and procedural interaction, then AI agents probably do mark the beginning of the end for that model in many cases.</p>
<p>But if by “user interface” we mean the layer where humans express intent, review actions, manage risk, resolve ambiguity, and stay in control, then no. The UI is not ending. It is being redefined.</p>
]]></content:encoded></item><item><title><![CDATA[APEX + OCI Email Logs: Track Bounces, Complaints, Suppression]]></title><description><![CDATA[Introduction
I am sure many of you are already using the OCI Email Delivery Service to send emails from your APEX Applications. It offers a convenient and inexpensive way to handle emails that integra]]></description><link>https://blog.cloudnueva.com/oci-email-service-next-level</link><guid isPermaLink="true">https://blog.cloudnueva.com/oci-email-service-next-level</guid><category><![CDATA[orclapex]]></category><category><![CDATA[OCI]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 12 Mar 2026 13:06:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/f3466926-c917-4cb9-a985-a60329f41a4b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>I am sure many of you are already using the OCI Email Delivery Service to send emails from your APEX Applications. It offers a convenient and inexpensive way to handle emails that integrates easily with APEX. <a href="https://hashnode.com/@lufcmattylad" class="user-mention" data-type="mention" title="Matt Mulvaney">Matt Mulvaney</a> wrote a step-by-step guide to setting it up <a href="https://mattmulvaney.hashnode.dev/page/about">here</a>.</p>
<p>If you are using this service and have asked yourself these questions, then this post is for you:</p>
<ul>
<li><p>How do I know if my email was bounced?</p>
</li>
<li><p>How do I know if my emails are getting marked as spam?</p>
</li>
<li><p>Basically, did the recipient receive the email?</p>
</li>
</ul>
<p>To answer these questions, you must enable logging for your OCI Email Service. In this post, we will:</p>
<ul>
<li><p>Enable Email Delivery logs (OutboundAccepted/OutboundRelayed)</p>
</li>
<li><p>Query logs via Logging Search API</p>
</li>
<li><p>Surface results in APEX (Interactive Report) and/or sync to a table for history</p>
</li>
</ul>
<h1>Suppression Vs Bounce</h1>
<p>Before we start, it is important to understand what the two types of email delivery logs reveal.</p>
<p>A bounce is a downstream delivery failure reported by the recipient’s mail system after an attempt is made (typically seen in the <strong>OutboundRelayed</strong> log) and usually points to issues such as an invalid mailbox, a missing domain, or temporary recipient-side problems.</p>
<p>Suppression often happens before any delivery attempt; your send can look “fine” from APEX’s perspective, but Email Delivery may block or drop the message due to policy, reputation, or suppression-list conditions (often visible in <strong>OutboundAccepted</strong> and sometimes reflected in log messages indicating a suppressed recipient). Practically, this is the difference between “the destination rejected it” and “we never really tried,” and it changes your remediation: bounces drive address hygiene and retry rules, while suppression drives sender/domain configuration and suppression-list/deliverability review.</p>
<h1>What Can I Learn</h1>
<p>I use these logs for <a href="https://apps.cloudnueva.com/apexblogs">APEX Developer Blogs,</a> which has over 300 subscribers. Here are some examples of errors from these logs:</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/bf2c012d-abcd-4009-9ff8-f319b190f969.png" alt="APEX Page Showing Email LOgs" style="display:block;margin:0 auto" />

<h1>Setup Logging</h1>
<p>Let's start by setting up logging from the OCI Console.</p>
<p>Navigation: Developer Services &gt; Email Delivery &gt; Click on Your Domain</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/f7a87b43-fa1d-4016-9441-a49746a38bf9.png" alt="OCI Email Delivery Setup for Domain" style="display:block;margin:0 auto" />

<p>Then click on the 'Monitoring' tab and scroll down to the 'Logs' section, click the ellipses for the 'Outbound Relayed' log, and click 'Enable Log'.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/2c769c0c-abc6-4c8e-b818-77257a3d363f.png" alt="OCI Email Delivery Monitoring Logging" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>Enable both <strong>OutboundAccepted</strong> and <strong>OutboundRelayed</strong> to detect both suppression and delivery outcomes.</div>
</div>

<p>If you don’t already have a log group set up, click 'Create new group':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/ea69e33f-e16c-413a-81a0-1263897ae79a.png" alt="OCI Email Delivery - Enable Resource Log 1" style="display:block;margin:0 auto" />

<p>Enter a log group name and description, and click 'Create':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/18281bdb-ea27-4b0e-aa1d-0ccfe5283fb6.png" alt="OCI Email Delivery - Enable Resource Log 2" style="display:block;margin:0 auto" />

<p>Once back on the Enable resource log page, click 'Enable log':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/46c3026b-198e-4a2d-a12d-b97e0d6d2db9.png" alt="OCI Email Delivery - Enable Resource Log 3" style="display:block;margin:0 auto" />

<p>After a few seconds, your log should be active:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/5c8030e9-282b-4931-9a81-b6be5a288346.png" alt="OCI Email Delivery - Log Group and Log Active" style="display:block;margin:0 auto" />

<p>Adjust the retention period to match your audit needs/cost constraints.</p>
<div>
<div>💡</div>
<div>Make a note of the OCIDs for the log group and the log. We will use these later.</div>
</div>

<h2>Test the Logs</h2>
<p>Send a test email from your instance to make sure it shows up in the logs:</p>
<pre><code class="language-sql">DECLARE
  l_body  CLOB;
BEGIN
  l_body := '&lt;h1&gt;Testing APEX Mail&lt;/h1&gt;';
  apex_mail.send
   (p_to        =&gt; 'test@example.com',
    p_from      =&gt; 'info@example.com',
    p_body      =&gt; l_body,
    p_body_html =&gt; l_body,
    p_subj      =&gt; 'Testing APEX Mail');
  apex_mail.push_queue;
END;
</code></pre>
<div>
<div>💡</div>
<div>Remember to set <code>p_from</code> to an email address that is on your OCI Email Delivery Approved Sender List.</div>
</div>

<p>After a few seconds, you should see the message in the logs:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/d5bc4be6-b205-4da6-83cf-d4bb12c8a406.png" alt="OCI Email Delivery -Explore Log" style="display:block;margin:0 auto" />

<p>If we send an email to an invalid email address, we can see the bounce from the destination email server. <strong>Note</strong>: I have changed the OCIDs in the sample JSON below to 'AAA' and the domains to <a href="http://example.com">example.com</a>.</p>
<pre><code class="language-json">{
  "datetime": 1771705254189,
  "logContent": {
    "data": {
      "action": "bounce",
      "bounceCategory": "bad-mailbox",
      "bounceCode": "5.1.10",
      "errorType": "hard",
      "message": "Suppressed recipient sam@example.com for email from info@example.com: bad-mailbox hard bounce",
      "messageId": "4B5C41B678F782A0E063E815000AC99A@apps.example.com",
      "originalMessageAcceptedTime": "2026-02-21T20:20:39.614Z",
      "receivingDomain": "example.com",
      "recipient": "sam@example.com",
      "reportGeneratedTime": "2026-02-21T20:20:41Z",
      "sender": "info@example.com",
      "senderCompartmentId": "AAA",
      "senderId": "AAA",
      "smtpStatus": "550 5.1.10 RESOLVER.ADR.RecipientNotFound; Recipient sam@example.com not found by SMTP address lookup"
    },
    "id": "4e81ce60-b8c7-40be-8a19-79448a3f4f2d",
    "oracle": {
      "compartmentid": "AAA",
      "ingestedtime": "2026-02-21T20:20:56.815Z",
      "loggroupid": "AAA",
      "logid": "AAA",
      "tenantid": "AAA"
    },
    "source": "example.com",
    "specversion": "1.0",
    "time": "2026-02-21T20:20:54.189Z",
    "type": "com.oraclecloud.emaildelivery.emaildomain.outboundrelayed"
  },
  "regionId": "us-phoenix-1"
}
</code></pre>
<p>In the above example, we received a hard bounce, indicating that the email address was invalid.</p>
<div>
<div>💡</div>
<div>Knowing that an email was not delivered can be critical to your workflow. Knowing why it was not delivered allows you to address the issue.</div>
</div>

<h2>Documentation</h2>
<ul>
<li><p><a href="https://docs.oracle.com/en-us/iaas/Content/Logging/Reference/details_for_emaildelivery.htm">Details for Email Delivery Logging</a> - JSON Examples and field descriptions.</p>
</li>
<li><p><a href="https://docs.oracle.com/en-us/iaas/Content/Identity/policyreference/emailpolicyreference.htm">Email Delivery Policies</a> - Setting up access to view the logs.</p>
</li>
<li><p><a href="https://docs.oracle.com/en-us/iaas/Content/Email/Reference/log-guide.htm">Email Log Searching</a> - Syntax for searching the email logs.</p>
</li>
<li><p><a href="https://docs.oracle.com/en-us/iaas/api/#/en/logging-search/20190909/SearchResult/SearchLogs">Using the Logging Search API</a>.</p>
</li>
<li><p><a href="https://docs.oracle.com/en-us/iaas/Content/Logging/Reference/query_language_specification.htm">Logging Query Language Specification</a>.</p>
</li>
</ul>
<h1>Access the Logs from a REST API</h1>
<p>Even though the OCI console includes a deliverability dashboard and a UI to access logs, it would be much easier if we could get these logs into the database so we can view them from an APEX page. In this section, I will cover how to set up an OCI service account to access the OCI Logging REST API.</p>
<h2>Create an OCI User</h2>
<p>Navigation: Identity and Security &gt; Domains &gt; Select your domain &gt; Click Create</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/9a02aceb-edd5-4081-8fdc-46804aee2068.png" alt="Create OCI User - Step 1" style="display:block;margin:0 auto" />

<p>Enter a username and click 'Create':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/9ad2e0ca-fb0a-4dbc-9a79-87627dbeb97a.png" alt="Create OCI User - Step 2" style="display:block;margin:0 auto" />

<p>Click Actions &gt; Edit User Capabilities:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/58ebe2ca-24ec-41ba-bdb0-b791f61b1e8d.png" alt="Create OCI User - Step 3" style="display:block;margin:0 auto" />

<p>Uncheck all options except 'API Keys' and click 'Save Changes':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/abaff83b-eeaf-4f81-8d6c-2cd5b4c9d683.png" alt="Create OCI User - Step 4" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>Keep both key files safe. You will use the content of the private file in the APEX Web Credential below.</div>
</div>

<p>On the user page, select the 'API keys' tab, then click Actions &gt; Add API key</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/9e8a4a54-2a12-461c-8e22-746f714e34e2.png" alt="Create OCI User - Step 5" style="display:block;margin:0 auto" />

<p>Download the public and private key, then click 'Add':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/aac90bc0-db27-44da-80f6-3707432ce20e.png" alt="Create OCI User - Step 6" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>Copy the resulting '<strong>Configuration file preview' </strong>details and keep them safe. You will use these values in the APEX Web Credential below.</div>
</div>

<h2>Create an OCI Group</h2>
<p>Back under the User Management tab, scroll down to Groups:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/a6c83c52-7370-4b7f-8a6c-17518477ffae.png" alt="Create OCI Group - Step 1" style="display:block;margin:0 auto" />

<p>Create a new Group:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/1c73010f-76a8-4a34-8663-f4bbe8c6a184.png" alt="Create OCI Group - Step 2" style="display:block;margin:0 auto" />

<p>Add the new user to the group:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/96a8dd97-b34a-44b1-a9ab-2dcc224a3179.png" alt="Create OCI Group - Step 3" style="display:block;margin:0 auto" />

<h2>Create an OCI Policy</h2>
<p>Navigate to: Identity &amp; Security &gt; Policies &gt; Click 'Create Policy':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/0319416b-91ad-41e3-ac35-b37ce6aa5412.png" alt="Create OCI Policy - Step 1" style="display:block;margin:0 auto" />

<p>Complete the Policy details and click the 'Create' button:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/79899530-e479-4018-be0b-84db47d9bbad.png" alt="Create OCI Policy - Step 2" style="display:block;margin:0 auto" />

<p>Policy Statements:</p>
<pre><code class="language-plaintext">allow group apex_rest_api_access_grp to read log-groups in tenancy
allow group apex_rest_api_access_grp to read log-content in tenancy
</code></pre>
<div>
<div>💡</div>
<div>If you prefer least privilege, scope the policy to the compartment that contains the log group (instead of the tenancy-wide scope).</div>
</div>

<h1>The Logging REST API</h1>
<p>The OCI logging service offers a REST API you can use to consume any logs. My instance is in the Phoenix region, so my endpoint is:</p>
<p><a href="https://logging.us-phoenix-1.oci.oraclecloud.com/20190909/search">https://logging.us-phoenix-1.oci.oraclecloud.com/20190909/search</a></p>
<p>You can see a full list of Logging Endpoints <a href="https://docs.oracle.com/en-us/iaas/api/#/en/logging-search/20190909/">here</a>, and details on using the search API <a href="https://docs.oracle.com/en-us/iaas/api/#/en/logging-search/20190909/SearchResult/SearchLogs">here</a>. You will also need to understand the <a href="https://docs.oracle.com/en-us/iaas/Content/Logging/Reference/query_language_specification.htm">logging query language</a>. This query language allows you to filter results to see only bounces if that is what you are interested in.</p>
<p>The endpoint requires that you send a POST request with a payload like this:</p>
<pre><code class="language-json">{
  "timeStart": "2026-01-19T01:02:29.600Z",
  "timeEnd":   "2026-01-19T02:02:29.600Z",
  "searchQuery": "search \"&lt;tenancy_ocid&gt;/&lt;log_group_ocid&gt;/&lt;log_ocid&gt;\" | sort by datetime desc",
  "isReturnFieldInfo": false
}
</code></pre>
<div>
<div>💡</div>
<div>You must replace the &lt;value&gt; placeholders with the actual OCIDs for your Tenancy, Log Group, and Log, respectively.</div>
</div>

<h2>API Limits</h2>
<p>The Logging Search API returns up to 1000 entries per call and supports paging via a next-page token/header (client-managed). Searches/exports are limited to a maximum 14-day time window per request.</p>
<h1>Consuming the Logging API from APEX</h1>
<h2>Create an APEX Web Credential</h2>
<p>You will need an APEX Web Credential of type 'OCI Native Authentication' to access the REST API from APEX.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/c7c38c70-3f14-42a3-b800-6114fc33c332.png" alt="Create APEX Web Credential - Step 1" style="display:block;margin:0 auto" />

<p>Enter the details from your OCI user's 'Configuration file preview' above. For the OCI Private Key, copy and paste the value from the private key file downloaded above. Click Create to complete the creation of the APEX Web Credential.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/2fbbbf68-3909-491c-9e5c-795ba3554846.png" alt="Create APEX Web Credential - Step 2" style="display:block;margin:0 auto" />

<h2>Consumption Options</h2>
<h3>APEX REST Data Source</h3>
<p>The obvious first choice for consuming a REST API is to use a REST Data Source. I won't get into the step-by-step, but here are a few things that tripped me up when I created one.</p>
<p>In the 'POST' Operation, set the 'Database Operation' field to 'Fetch rows':</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/5673bb12-4ebc-4b0e-91c5-3feca31cacd8.png" alt="REST Data Source Setup - Step 1" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>You will want to replace hardcoded timeStart and timeEnd with variables such as #START_TS# and #END_TS#.</div>
</div>

<p>Delete the GET, PUT, and DELETE Operations; we do not need them.</p>
<p>Set up the following parameters:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/e9f72f3e-8fda-4f43-8342-1e995d9645e5.png" alt="REST Data Source Setup - Step 1" style="display:block;margin:0 auto" />

<p>In the Data Profile, set the 'Row Selector' field to 'results'.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/4e1d6e4b-8265-4dde-b7b1-0a3a180e5613.png" alt="REST Data Source Setup - Step 2" style="display:block;margin:0 auto" />

<p>Once all the above is in place, click 'Rediscover Data Profile', then click 'Replace Data Profile'.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/719fa155-5e52-43b0-9b49-9e9fd613d653.png" alt="REST Data Source Setup - Step 3" style="display:block;margin:0 auto" />

<p>Unfortunately, REST Source Types for 'Oracle Cloud Infrastructure (OCI) REST Service' do not automatically walk OCI Logging Search paging tokens. If you are only expecting a few hundred emails a week, that should not be an issue, as you can fetch up to 1,000 log entries at a time. You could create an Interactive Report based on the REST Source, add some Start and End Date Time parameters, and you have everything you need.</p>
<p>If you expect higher email volumes, you could use <a href="https://blog.cloudnueva.com/apexwebservice-the-definitive-guide">APEX_WEB_SERVICE</a> to retrieve the data and loop through the pages yourself, or build a <a href="https://blog.cloudnueva.com/apex-rest-source-connector-plug-ins">REST Source Connector plug-in</a>.</p>
<h2>Syncing to a Table</h2>
<p>If you expect to send fewer than 1,000 emails per hour, it may be easier to use a <a href="https://blog.cloudnueva.com/dynamic-parameters-in-oracle-apex-rest-data-source-synchronizations">REST Source Sync</a> to sync the last hour's logs to a local table. This has the advantage of circumventing the 14-day window limit of the logging API.</p>
<p>You will need to use the REST Source Sync 'Steps' feature to pass the limit, START_TS, and END_TS parameters. In the example below, I am looking back 1 day. You may need to adjust this based on the number of emails you expect to receive.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/626b62127d5d27b992e4cf90/5af3c39d-ff8a-401d-aff5-407ac0bd55fa.png" alt="REST Source Sync Steps" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>Remember to add a purge routine to periodically clear old records from the sync table.</div>
</div>

<h1>Conclusion</h1>
<p>Enabling OutboundAccepted and OutboundRelayed logs gives you a reliable way to determine whether an email was delivered, bounced, complained about, or suppressed.</p>
<p>For low-volume use, a REST Data Source plus an Interactive Report is usually sufficient, as long as you stay within the 1,000 records-per-call limit. For higher volume or longer retention, use a REST Source Sync (or PL/SQL paging) to persist results to a table and work around the 14-day query window. Once the data is local, you can join it to your tables and build deliverability views and alerts that meet your requirements.</p>
]]></content:encoded></item><item><title><![CDATA[Adding an AI Agent to an Existing APEX App]]></title><description><![CDATA[Introduction
Modern frontier LLMs are now reliable enough to support practical agent workflows when paired with strong orchestration and guardrails. Adding an agent to an existing APEX app allows you ]]></description><link>https://blog.cloudnueva.com/adding-ai-agent-to-apex-app</link><guid isPermaLink="true">https://blog.cloudnueva.com/adding-ai-agent-to-apex-app</guid><category><![CDATA[orclapex]]></category><category><![CDATA[AI]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 05 Mar 2026 13:11:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/1211a45c-084c-4de8-841e-78fa4821f686.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>Modern frontier LLMs are now reliable enough to support practical agent workflows when paired with strong orchestration and guardrails. Adding an agent to an existing APEX app allows you to:</p>
<ul>
<li><p>Simplify user workflows</p>
</li>
<li><p>Simplify the user interface</p>
</li>
<li><p>Automate repetitive tasks</p>
</li>
<li><p>Leverage your existing data model, views, APIs, etc.</p>
</li>
<li><p>Get the experience of building agents with minimal investment</p>
</li>
</ul>
<p>In this post, I will use an example of an APEX-based project management application I have been building for a client off and on over the past few years. Over time, it has grown to more than fifty pages and thousands of lines of PL/SQL. A month ago, we started on a project to introduce an AI agent to simplify the app.</p>
<h1>Introducing AI Agents</h1>
<p>By combining a frontier model with tools (PL/SQL APIs, Web Services) and strong governance (permissions, auditing, guardrails), you can build an AI agent that executes multi-step business tasks, not just chats, safely within defined constraints.</p>
<p>Agents built for APEX use a PL/SQL framework to manage the 'Agentic Loop'. That's right; when you boil it down, an agent is a loop. Within this loop, the LLM makes suggestions as to which tools it wants to run; your code decides whether to run them. <strong>Your code is in control</strong>.</p>
<p>The diagram below illustrates the agentic loop.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/1c3ea020-3c94-456f-ba76-1e8ef8fdd3ae.png" alt="Agentic Loop in APEX" style="display:block;margin:0 auto" />

<p>The <strong>orchestrator</strong> controls the agentic loop, maintains state (in a database table) between tool calls, and decides when to hand control back to the user and when the loop should finish. The <strong>dispatcher</strong> receives tool requests, checks what data the user is allowed to see, performs schema and business validations, calls the tool, and returns the response to the orchestrator to feed back to the LLM during the next iteration.</p>
<p>This table illustrates the differences between a standard APEX approach and an Agentic approach:</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Feature</strong></p></td><td><p><strong>Standard APEX Integration</strong></p></td><td><p><strong>Agentic Framework</strong></p></td></tr><tr><td><p><strong>Logic Location</strong></p></td><td><p>Hardcoded in Page Processes</p></td><td><p>Dynamic in Orchestrator Loop</p></td></tr><tr><td><p><strong>User Input</strong></p></td><td><p>Structured (Forms/Pickers)</p></td><td><p>Unstructured (Natural Language)</p></td></tr><tr><td><p><strong>Validation</strong></p></td><td><p>On-Submit / Client-side</p></td><td><p>Dispatcher-level / Pre-execution</p></td></tr><tr><td><p><strong>Flexibility</strong></p></td><td><p>Rigid workflow</p></td><td><p>Multi-step "Reasoning" capability</p></td></tr><tr><td><p><strong>Security</strong></p></td><td><p>Session/ACL based</p></td><td><p>ACL + Intent Validation</p></td></tr></tbody></table>

<h1>Simplifying Project Management</h1>
<p>So, let's get back to the project management app use case. The app handles everything related to project management, including questions, risks, issues, requirements, design documents, emails, and meeting notes. As the app grew and we introduced new pages, fields, and buttons, users started to get frustrated by how long it takes to navigate to where they need to go. I am sure Jira users can relate!</p>
<div>
<div>💡</div>
<div>The AI agent reduces this complexity by providing a chat-style interface that makes it easier to find information, automate repetitive actions, and surface project insights that were previously buried behind layers of menus.</div>
</div>

<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/6069dca1-1b3a-4043-811a-5186c5eaedff.png" alt="Screenshot of the APEX AI Agent Chat Interface" style="display:block;margin:0 auto" />

<h2>Agent Scope</h2>
<p>For phase one of this project, we decided to limit the scope to allowing users to inquire about, create, and update project questions, risks, and issues.</p>
<div>
<div>💡</div>
<div>Reducing the scope to this limited (<strong>but still useful</strong>) set of activities was critical to its success.</div>
</div>

<p>Too often, we try to cover every use case and end up shipping nothing. That risk is higher with emerging technologies, where outcomes are uncertain. Narrowing scope doesn’t mean lowering the bar, it means delivering a genuinely useful slice that also proves whether the technology will scale to the entire app.</p>
<h2>Tools</h2>
<p>We gave the Agent the following tools:</p>
<ul>
<li><p>Show the project structure. This tool provides the information about the project, such as the customer, the project's structure, the project lead, etc.</p>
</li>
<li><p>List project team members. This tool provides details of all the people associated with the project. This is used by the LLM to assign tasks, return tasks for specific people, etc.</p>
</li>
<li><p>Search questions, risks, and issues. This tool allows users to perform searches using assigned to, status, question, risk, or issues text (via Vector Search).</p>
</li>
<li><p>Create questions, risks, and issues</p>
</li>
<li><p>Update questions, risks, and issues</p>
</li>
</ul>
<p>Each tool is a PL/SQL function or procedure that either returns some JSON or performs an action. Because we started with a fully functional App, we were able to leverage existing tables, views, and PL/SQL APIs.</p>
<div>
<div>💡</div>
<div>The AI tools were essentially wrappers around the code we already had.</div>
</div>

<p>One of the most underrated parts of designing tools is the descriptive tool metadata that you send to the LLM with your prompt. The tool call metadata must clearly describe each tool along with its parameters. You should also resist the urge to include too many tools with each request to the LLM. Only send the tools that are relevant to the activity you are trying to perform. This prevents the LLM from having to look through and 'understand' tools it will never need.</p>
<div>
<div>⚠</div>
<div>Tools should be deterministic and side-effect controlled. The LLM should never be responsible for enforcing business rules or data integrity. That logic must live inside the tool implementation.</div>
</div>

<h2>The Orchestrator</h2>
<p>The orchestrator is a PL/SQL procedure that controls the agentic loop. Essentially, the process involves looping, calling tools, and providing results back to the LLM until the user's request is completed (i.e., no further tool requests from the LLM).</p>
<p>We use a database table to maintain state between tool calls. This table allows us to re-construct the chat history and pass it to the LLM with each API call.</p>
<h2>The Brain</h2>
<p>The orchestrator passes a system prompt to the LLM during each request. This acts as the agent's brain. The system prompt provides background about the Application, the agent's objectives, rules for tool use, required behaviors, and how responses should be formatted.</p>
<div>
<div>💡</div>
<div>Expect to iterate on the system prompt throughout the build of your agent. A well-formed system prompt is vital to the agents performance.</div>
</div>

<h2>Dispatcher</h2>
<p>When the LLM requests a tool, the dispatcher does the following:</p>
<ul>
<li><p>Verifies the tool exists</p>
</li>
<li><p>Verifies the validity of the parameters the LLM requested</p>
</li>
<li><p>Verifies the user has access to the action, and or data being requested</p>
</li>
<li><p>Executes the PL/SQL Function or Procedure</p>
</li>
<li><p>Shapes the JSON response and hands it back to the Orchestrator to pass back to the LLM</p>
</li>
</ul>
<h2>Creates and Updates</h2>
<p>With an emerging technology like this, you may be nervous about allowing the AI to request tool calls that create or update records in your database. Sure, you write the tool so you can ensure whatever record is created is valid, but what if the AI decides it wants to create 100 valid records when it should have been just one? To allay fears, we set up the agent and the tools with a flag indicating if a particular tool call requires human approval before it can run. Initially, we set all the create/updated tools to require human approval. Down the road, we expect that we may want to turn this confirmation off for some write tools.</p>
<h2>Vector Search</h2>
<p>I briefly mentioned that the Search tool uses Vector search so users can perform semantic searches on the text from questions, risks, issues (and the associated answers/responses). This allows users to perform powerful searches from a prompt. e.g., 'Find Open questions assigned to Jon related to California'.</p>
<p>We established a queueing mechanism that queues new and updated content. An APEX Automation picks up the queued content every 15 minutes. The automation chunks the content using the SQL function <code>VECTOR_CHUNKS</code>. The chunks are then vectorized using the SQL function <code>VECTOR_EMBEDDING</code>. We use the ONNX model <code>ALL_MINILM_L12_V2</code> (available <a href="https://blogs.oracle.com/machinelearning/use-our-prebuilt-onnx-model-now-available-for-embedding-generation-in-oracle-database-23ai">here</a>) in the database to create the embeddings (vectorize the chunks).</p>
<h2>Instrumentation</h2>
<p>One of the most useful things we included at the beginning is comprehensive logging and diagnostics. Every LLM API call, every tool request, and every tool response is logged for each conversation. This allows us to replay conversations for audit and troubleshooting purposes.</p>
<p>It even allows us to troubleshoot strange behaviors using AI. For example, using the Oracle SQLcl MCP tool connected to the Codex App. I can say something like, "Review conversation ID 123 and find out why only 1 of the 3 provided issues were created by the agent." Codex can then use SQLcl to query the conversations table, and iterate until it determines whether the issue is code-related or system-prompt-related.</p>
<h1>Lessons Learned</h1>
<ul>
<li><p>Never trust the model to handle security or data integrity. Your code (orchestrator and dispatcher) and your data model should handle them. Always!</p>
</li>
<li><p>Log everything and make conversations replayable for audit and troubleshooting.</p>
</li>
<li><p>Make tools configurable to easily toggle human-in-the-loop confirmations.</p>
</li>
<li><p>When writing CRUD PL/SQL APIs, don't assume the consumer is APEX. Your PL/SQL APIs must be hardened to handle calls from unexpected future sources, such as agents.</p>
</li>
<li><p>Watch the context. Each call to the LLM passes the completed conversation history. As a conversation builds (especially if you have multiple tool calls returning large amounts of JSON), the LLM has to wade through more and more context to figure out what the latest request is. Consider capping the number of turns or preventing further turns after the context reaches a certain size.</p>
</li>
<li><p>Enable parallel tool calls when calling the LLM API to reduce turns (switching between the user and the model in the Agent Loop). For example, if you want to copy-paste 10 questions to add, enabling parallel tool calls allows the agent to request that the create tool be called 10 times in one turn rather than one at a time. This allows the user to confirm creation of the items once, not 10 times, and reduces token usage. Parallel tool calls also reduce the amount of time the user must wait for their request to complete.</p>
</li>
<li><p>When something fails (e.g., you get a PL/SQL exception during a tool call), do not pass the Oracle error message back to the LLM. Instead pass something meaningful like "the project team tool is not responding". This allows the LLM to fail gracefully and inform the end user.</p>
</li>
<li><p>Do not allow your agentic loop to run forever. Set a maximum number of iterations where you end the loop no matter what. Make this configurable so you can adjust it during testing.</p>
</li>
<li><p>Each LLM API call takes between 2 and 10 seconds to run. If the agent has to call the LLM several times during a request, the overall duration can add up quickly. You can influence this by playing with the model and the reasoning level (the higher the reasoning level the more the model thinks and the longer it takes). Use the fastest model with the lowest reasoning level which still gives good results for your use case. You can also help by improving the user experience while they wait. As you will see in the demo below, we took the time to build a custom blocking spinner that is displayed while the agent is working.</p>
</li>
</ul>
<h1>Demo</h1>
<p>A picture is worth a thousand words, as they say. This short video shows a typical session with the Agent.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/00728044-633f-4078-8c58-73080a0a4d8c.gif" alt="Demo of Agent for Project Management App" style="display:block;margin:0 auto" />

<ul>
<li><p>Projects are structured by sections and sub-sections.</p>
</li>
<li><p>The context area provides context for the user's prompt.</p>
</li>
<li><p>I did not show it in the demo, but the response includes links that let users open Questions, Risks, and Issues directly from the agent. This makes use of existing APEX pages.</p>
</li>
</ul>
<p>As you can see, the user can take a question through its full lifecycle without leaving the page. This demo only shows you a fraction of what is possible with just five tools. Some other sample prompts:</p>
<ul>
<li><p>Find questions related to VAT Tax</p>
<ul>
<li>The search tool uses vector search to find questions, risks, and issues related to VAT.</li>
</ul>
</li>
<li><p>Review the attached meeting transcript (copy-paste it into the Context field), extract all questions, risks, and issues, and organize them into subsection 20.</p>
<ul>
<li>This is pretty powerful. We used the LLM to analyze the meeting transcript and extract all questions, risks, and issues raised during the meeting. The LLM extracted them and then invoked the create tool multiple times to populate the database with questions, risks, and issues.</li>
</ul>
</li>
</ul>
<h2>Behind the Scenes</h2>
<p>Privileged users can enable diagnostics. Diagnostics show all of the records tracked during the conversation, including requests for tool calls and responses from tool calls. In the screenshot below, you can see the diagnostics for one turn from the demo video. The diagnostic records are identified with the brown '...' avatar.</p>
<img src="https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/3a0c654e-be11-4b52-8da4-358e10940f20.png" alt="Screenshot showing diagnostics from the Agent" style="display:block;margin:0 auto" />

<ul>
<li><p>We submitted a request, "answer question 110662 with yes"</p>
</li>
<li><p>The model took the question along with the system prompt and broke out the answer "yes" from the request. It then looked at the provided tools and requested that we call the <code>qri_search</code> tool to find the question</p>
</li>
<li><p>We ran the tool (after checking the user had access), and returned the JSON result containing details of the questions we found</p>
</li>
<li><p>The LLM interpreted this tool response JSON, confirmed there is just one question, then requested we call the <code>update_qri</code> tool</p>
</li>
<li><p>The <code>update_qri</code> tool requires a human confirmation, so the Orchestrator saved the tool request from the model and stopped to allow the user to click the Confirm/Reject button.</p>
</li>
<li><p>After clicking Confirm, the Orchestrator called the LLM one last time with the result from calling the <code>update_qri</code> tool.</p>
</li>
<li><p>The LLM decided it didn't require any more tool calls, which ended the turn.</p>
</li>
</ul>
<h1>Conclusion</h1>
<p>Adding an AI agent to an existing APEX application is a practical way to introduce AI capabilities without rewriting your system. Most applications already have the hard parts in place: a data model, business APIs, validation logic, and security rules. An agent simply becomes another consumer of those APIs.</p>
<p>The key is to keep the architecture straightforward. Let the LLM interpret user intent and suggest actions, but keep control in your code. The orchestrator manages the loop, the dispatcher validates and executes tools, and your existing PL/SQL APIs enforce business rules and data integrity.</p>
<p>Start with a limited scope, a small set of well-defined tools, and strong instrumentation. Once the architecture is in place, you can expand the agent’s capabilities incrementally.</p>
<p>In our case, just five tools were enough to let users search, create, and update project questions, risks, and issues directly from a chat interface. The result was a simpler workflow for users and a new way to interact with the application without changing the underlying system.</p>
<p>For teams already building applications with Oracle APEX, agents are a natural extension of the platform. The important part is not the model, it is the architecture around it.</p>
]]></content:encoded></item><item><title><![CDATA[Avoiding the Vibe Coding Rabbit Hole]]></title><description><![CDATA[Introduction
A few weeks ago, I started building an APEX 2nd brain to practice Agentic AI in APEX and PL/SQL, and hopefully create a useful tool to supplement my aging brain.
I am writing this post while Codex is rebuilding my APEX 2nd brain Applicat...]]></description><link>https://blog.cloudnueva.com/avoiding-the-vibe-coding-rabbit-hole</link><guid isPermaLink="true">https://blog.cloudnueva.com/avoiding-the-vibe-coding-rabbit-hole</guid><category><![CDATA[orclapex]]></category><category><![CDATA[AI]]></category><category><![CDATA[vibe coding]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Sun, 15 Feb 2026 16:29:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771172772261/44195aa6-40e3-46b0-bd24-0478932a7f01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>A few weeks ago, I started building an APEX 2nd brain to practice Agentic AI in APEX and PL/SQL, and hopefully create a useful tool to supplement my aging brain.</p>
<p>I am writing this post while Codex is rebuilding my APEX 2nd brain Application from scratch. This post is a cautionary tale about what happens when you go down the vibe coding rabbit hole.</p>
<h1 id="heading-the-rabbit-hole">The Rabbit Hole</h1>
<p>The first version of my 2nd brain APEX App included a simple text box on a single APEX page. When the user clicks the submit button, I pass a predefined prompt that provides instructions for filing the entry, along with the entry itself, to an LLM for classification. There was also an APEX Automation, which ingested my personal and work emails and calendar entries from Gmail and MS Office.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The goal here was to create an automated filing system, along with daily and weekly digests, to surface to-do items and ideas.</div>
</div>

<p>It worked OK, but I soon found the features limiting (especially with all the news about what people we are achieving using <a target="_blank" href="https://openclaw.ai/">Open Claw</a>). For the record, I do not use Open Claw.</p>
<p>With the new <a target="_blank" href="https://openai.com/index/introducing-the-codex-app/">Codex App</a> and a prompt first strategy, I started making enhancements. It was going great… I would ask for feature after feature, and they would get built and work 90% of the time. After a couple of hours, I stepped back and looked at the actual code that had been written:</p>
<ul>
<li><p>I ended up with 800 lines of JavaScript in the main APEX page (that’s more JavaScript than I write in a year).</p>
</li>
<li><p>Instead of using my AI config tables, which store system prompts, tool calls, etc., the AI wrote the system prompts and tool calls JSON and hard-coded them into the code.</p>
</li>
<li><p>The quality of the data model had degraded over time as fields were added, removed, and repurposed, and tables were abandoned. Each new table or set of tables was very well thought out, but there was no consideration for tidying up the old tables.</p>
</li>
<li><p>The AI was overly cautious about dropping old code. By the end, I was left with several views and packages that were no longer used. This amounted to more than 2,000 lines of unused code.</p>
</li>
</ul>
<p>The other side effect was that the overall architecture had drifted and become overly complex and bloated. The issue isn’t the AI; it’s unbounded iteration without thought.</p>
<p>Essentially, it was the work of a competent and overly eager junior programmer. There were no egregious issues (other than not cleaning up old code), but it was not the way I would have done it.</p>
<h1 id="heading-stepping-back-amp-resetting">Stepping Back &amp; Resetting</h1>
<h2 id="heading-write-a-specification">Write a Specification</h2>
<p>I decided to take a step back and reassess what I actually wanted, and spent an hour writing a detailed specification. You can read the specification <a target="_blank" href="https://gist.github.com/jon-dixon/5c7b35b23e6ef17c0d698359293c40ba">here</a>.</p>
<p>This produced two benefits:</p>
<ol>
<li><p>It forced me to think about what features were important to me.</p>
</li>
<li><p>It provided the AI with much clearer guidance on what it was supposed to do. Instead of 10 disjointed prompts with feature requests, it had a single specification on which to build a solid architecture.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">High-quality specs for AIs are the area I think we, as developers, can improve (and keep our jobs for a little longer). More on that <a target="_self" href="https://blog.cloudnueva.com/an-ai-shift-for-apex-developers">here</a>.</div>
</div>

<h2 id="heading-agentsmd">AGENTS.md</h2>
<p>I also updated my <a target="_blank" href="https://agents.md/">AGENTS.md</a> to include some additional instructions:</p>
<ul>
<li><p>Prefer PL/SQL over JavaScript. When JavaScript is necessary, prefer Dynamic Actions over Ajax Callbacks.</p>
</li>
<li><p>Utilize AI Configuration tables gen_ai* to specify new prompts and tools.</p>
</li>
<li><p>Tell me if I suggest changes that contravene APEX and PL/SQL best practices.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Although it’s painful having <code>AGENTS.md</code> files littering your file system, it is important that you actively keep them up to date with the latest constraints and guardrails.</div>
</div>

<h1 id="heading-lessons-learned">Lessons Learned</h1>
<ul>
<li><p>Always write a spec first. However simple it is, writing it out first helps you organize your thoughts and provides valuable guidance and structure for the AI.</p>
</li>
<li><p>LLMs ❤️ JavaScript more than PL/SQL. Unless you instruct them otherwise, they will generate far too much unnecessary JavaScript. They also love Ajax Callbacks; it’s not that they don’t know about Dynamic Actions, they just prefer Ajax Callbacks.</p>
</li>
<li><p>AGENTS.md is a live document; every time the LLM does something you don’t like, tell it by updating AGENTS.md.</p>
</li>
<li><p>After implementing a new feature, always follow up with a prompt to have the LLM check for unused code. More importantly, ask it to follow the entry points to your app and suggest entire branches of unused code. Also, run a check for data model drift. <strong>Codex is very good at doing this; it just needs to be told to do it.</strong></p>
</li>
<li><p>When using plan mode in Codex or Claude (which I highly recommend), read the plan! This may sound obvious, but when I started out, I would just skim the plan and hit Go. Providing input after the plan is produced is often the last chance to direct the LLM once implementation begins. Adjustments made at this stage can save you hours later on.</p>
</li>
<li><p>If you start a thread with an LLM and get to around 5 turns, press pause ⏸️ to think. Ask yourself whether you are on the edge of the AI 🐇 🕳️❓ Ask yourself: Will the next prompt really get me there, or should I start again with a better spec? The answer is usually the second one, but it is hard to step back!</p>
</li>
</ul>
<h1 id="heading-time-for-controversy">Time for Controversy</h1>
<div data-node-type="callout">
<div data-node-type="callout-emoji">❓</div>
<div data-node-type="callout-text">Do I really need to ‘know’ the code I write?</div>
</div>

<p>With the major improvements made to coding in Claude Opus 4.5/4.6 and Codex 5.2/5.3, I have been asking myself whether I really need to ‘<strong>know’</strong> all the code I create. If AI generated it, then surely AI will be better at maintaining it than I am?</p>
<p>My answer (at least for now) is that I do need to understand the code I / the AI creates.</p>
<ul>
<li><p>I am responsible for the code, not the AI. It will be a dark day indeed when developers stop being responsible for the code they produce.</p>
</li>
<li><p>I still feel that my taste/instincts/intuition are better than the AIs. This is the main advantage humans have over humans (at least right now); we should make the most of it.</p>
</li>
<li><p>We are not close to finding all of the edge cases. Even for this personal project, I added three edge cases to my AGENTS.md and went back to my Spec a few times to guide the AI. We are still a long way off from a fire-and-forget approach to AI development.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Vibe coding is a superpower; right up until it quietly turns into your architecture.</p>
<p>The problem wasn’t Codex. It was me letting a long thread become the design process. The AI will happily keep shipping “reasonable” changes forever, but it has no instinct for simplicity, no taste, and no discomfort about leaving dead code and abandoned tables behind.</p>
<p>The fix also wasn’t “use less AI.” It was <strong>put the AI back inside guardrails</strong>: a written spec, a clear APEX-first strategy (Dynamic Actions over callbacks, PL/SQL over page-level JavaScript), and an <a target="_blank" href="http://AGENTS.md">AGENTS.md</a> that I actually maintain. Once those constraints are in place, AI is great, not just at building features but at tracing entry points, finding dead branches, and calling out drift. It just needs to be told to do it.</p>
<p>And on the “do I need to know the code?” question: for now, yes. Maybe I don’t need to know every generated line, but I absolutely need to own the architecture, the data model, and the edge cases, because the day something breaks (or leaks), it’s my name on it, not the model’s.</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic Post-Logout URLs in APEX]]></title><description><![CDATA[Introduction
When using single sign-on type authentication schemes like Social Sign-In, you need to define a Post-Logout URL in APEX so that the Authentication provider redirects your APEX App to a public page after it completes the logout. When you ...]]></description><link>https://blog.cloudnueva.com/dynamic-post-logout-urls-in-apex</link><guid isPermaLink="true">https://blog.cloudnueva.com/dynamic-post-logout-urls-in-apex</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 12 Feb 2026 12:52:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765763034810/b9995646-2c45-4cc0-9fdb-4fc73a7ebef3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>When using single sign-on type authentication schemes like Social Sign-In, you need to define a Post-Logout URL in APEX so that the Authentication provider redirects your APEX App to a public page after it completes the logout. When you deploy your App to TEST and PROD, the Post-Logout URL (Public Page) changes with your instance URL. This introduces a challenge: the Post-Logout URL setup in APEX does not easily support dynamic values, so you must manually update it after deploying your App.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In this post, I will show you how to make the Post-Logout URL dynamic so you can change it as part of your CI/CD pipeline, or change it once per instance and never have to change it again.</div>
</div>

<h1 id="heading-background">Background</h1>
<p>The diagram below shows a typical logout flow for a Social Sign-In type Authentication Scheme. In my use case, I want the Authentication provider to redirect to a public page in my APEX App after the logout completes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765731692645/8710db64-4e38-4fd1-b788-c80fef8bb179.png" alt="Diagram showing the typical APEX Social Sign-On Logout Flow" class="image--center mx-auto" /></p>
<p>In the APEX Authentication Scheme, we can specify where we want APEX to go after logout is complete:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765731843985/32aa0b53-62ec-4a60-9f9b-2f9b24fc2261.png" alt="Authentication Scheme setup for Post-Logout URL" class="image--center mx-auto" /></p>
<p>You can specify either:</p>
<ul>
<li><p><strong>Home Page</strong> - Attempts to go to the home page after logout; because the session is invalid, it then redirects to the login page. This is not suitable for Social Sign-In because it will just trigger another login with the Authentication Provider.</p>
</li>
<li><p>URL - You can specify a URL APEX should go to after the logout. Unfortunately, the Post-Logout URL field does not support APEX-style runtime substitution such as <code>f?p=&amp;APP_ID.:9999</code>. On the surface, the best you can do is enter a hard-coded URL, e.g., <code>https://example.com/ords/dev/logout-page</code>. When deploying to TEST or PROD, we must change this URL manually (there is no API).</p>
</li>
</ul>
<h1 id="heading-the-solution">The Solution</h1>
<p>The best workaround I have come up with is as follows.</p>
<h2 id="heading-1-create-an-application-item">1 - Create an Application Item</h2>
<p>Create an Application Item to store the Post-Logout URL. Here is a screenshot of the Application Item, which, for my example, I have called <code>AI_POST_LOGOUT_URL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765732318966/7a915a63-5a5d-4fe8-8033-f850ce4ebb2d.png" alt="AI_POST_LOGOUT_URL Application Item" class="image--center mx-auto" /></p>
<h2 id="heading-2-create-an-application-setting">2 - Create an Application Setting</h2>
<p>Create an Application Setting to store the Post-Logout URL. Here is a screenshot of an Application Setting named <code>POST_LOGOUT_URL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765732418165/8c9b88ec-9270-4ccd-9ad1-7d07c88ae9e7.png" alt="APEX Application Setting to store the Post-Logout URL" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Be sure to set the ‘On Upgrade Keep Value’ option to Yes. This will ensure that when you deploy your App from DEV &gt; TEST &gt; PROD, the current value will not get overridden during the deployment.</div>
</div>

<p>This means the first time you deploy your App to a new instance, you will need to change the URL to the appropriate URL for the target instance. Moving forward (as long as you have ‘On Upgrade Keep Value’ set to Yes), you will no longer have to change it.</p>
<h2 id="heading-3-populate-the-application-item-for-new-sessions">3 - Populate the Application Item for New Sessions</h2>
<p>You will need to set the application item <code>AI_POST_LOGOUT_URL</code> to the value of the Application setting when creating a new session. The easiest place to do this is in an ‘After Authentication’ Application Process. In the screenshot below, I am calling apex_session_state.set_value directly from the ‘After Authentication’ Process of my App.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765732791460/b4b5f71c-fe29-48e5-81f7-cd2cf3a1f219.png" alt="APEX After Authentication Application Process to set Application Item" class="image--center mx-auto" /></p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Copy the environment-specific setting into session state once per login</span>
  apex_session_state.set_value 
    (p_item  =&gt; <span class="hljs-string">'AI_POST_LOGOUT_URL'</span>, 
     p_value =&gt; apex_app_setting.get_value(<span class="hljs-string">'POST_LOGOUT_URL'</span>));
<span class="hljs-keyword">END</span>;
</code></pre>
<h2 id="heading-4-set-the-post-logout-url-to-the-value-of-the-application-item">4 - Set the Post-Logout URL to the Value of the Application Item</h2>
<p>Finally, we must set the Post-Logout URL to the value of the Application Item <code>AI_POST_LOGOUT_URL</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765732929336/a734c6a4-03ed-4df5-b659-b8af966f4615.png" alt="Set the Post-Logout URL to the value of the Application Item AI_POST_LOGOUT_URL" class="image--center mx-auto" /></p>
<h2 id="heading-alternatives">Alternatives</h2>
<p>Of course, you do not have to use the APEX Application Setting to store the URL. You could store the URLs in your own table keyed on the instance SID/Service Name, but I think storing them in an APEX Application setting is more compact and standard APEX.</p>
<p>Because the Post-Logout URL ultimately controls the redirect, you should ensure it is fully trusted and not user-modifiable. Application Settings are ideal here because they are developer-controlled and not influenced by runtime user input.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This pattern has proven reliable and eliminates a common manual deployment step when using Social Sign-In in APEX.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💬</div>
<div data-node-type="callout-text">I would love to hear if you have a different way to do this.</div>
</div>]]></content:encoded></item><item><title><![CDATA[An AI Shift for APEX & PL/SQL Developers]]></title><description><![CDATA[Introduction
I have been using AI to help me build APEX Apps for well over a year now, and I’ve shared a lot about the tools and workflows I use. But something happened this week that felt like a genuine shift.
Usually, I use AI for things like autoc...]]></description><link>https://blog.cloudnueva.com/an-ai-shift-for-apex-developers</link><guid isPermaLink="true">https://blog.cloudnueva.com/an-ai-shift-for-apex-developers</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Sat, 31 Jan 2026 14:19:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769869089674/b0d74de9-bf3f-470d-ba39-5369f6eed11e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>I have been using AI to help me build APEX Apps for well over a year now, and I’ve shared a lot about the tools and workflows I use. But something happened this week that felt like a genuine shift.</p>
<p>Usually, I use AI for things like autocomplete, understanding a codebase, and code reviews. I even use it for creating new procedures and functions, but the results vary in quality. This week, on two separate occasions, I had the AI generate hundreds of lines of PL/SQL, resulting in production-ready code (after review and testing). It included validations, handled edge cases I hadn't explicitly listed, and followed my approach perfectly. In this post, I want to break down the specific factors that made these efforts successful, even as others failed.</p>
<h1 id="heading-metadata-the-ais-rosetta-stone">Metadata: The AI’s Rosetta Stone</h1>
<p>The first key to this success wasn't the prompt; it was the database <strong>schema</strong>.</p>
<ul>
<li><p>Clear, unambiguous <strong>column names</strong>.</p>
</li>
<li><p>Tables and columns have clear, <strong>plain English comments</strong>.</p>
</li>
<li><p><strong>Foreign Key Constraints</strong> that clearly define the table relationships.</p>
</li>
<li><p><strong>Check Constraints</strong> to define valid values for columns, where possible.</p>
</li>
<li><p><strong>NOT NULL Constraints</strong> to identify which columns must be populated.</p>
</li>
<li><p><strong>Unique constraints / natural keys.</strong></p>
</li>
</ul>
<p>Because I provided the AI with the DDL (including this extra metadata), it didn't just see a table; it inferred far more of the business rules with fewer guesses. It knew that a column named <code>STATUS_CODE</code> wasn't just a string, but a state-machine driver. When the metadata is clean, the model makes fewer incorrect assumptions.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Let's face it, as APEX developers, this is something we should be doing anyway.</div>
</div>

<h1 id="heading-the-spec-is-the-work">The Spec is the Work</h1>
<blockquote>
<p><em>If I spend all this time writing a detailed spec for the AI, I could have just written the code myself.</em></p>
</blockquote>
<p>This is something that I used to think until I realized I was wrong on two counts:</p>
<ol>
<li><p>In most cases, I have to write a spec anyway, so the client can review it and I can be sure I am on the right path.</p>
</li>
<li><p>A well-thought-out spec can make the difference between average and near-perfect results when an LLM is generating code.</p>
</li>
</ol>
<p>I did change the way I write my specs. I now write them in Markdown (using <a target="_blank" href="https://obsidian.md/">Obsidian</a>) and export to Word using an Obsidian plugin that runs Pandoc if the client needs a Word copy. I also annotate the spec with hints for the AI, such as table names and references to procedures that perform similar logic. I make these annotations using HTML comments, which Pandoc excludes when exporting to Word.</p>
<p>Here is an example excerpt using HTML comments for annotations:</p>
<pre><code class="lang-markdown">The App should then create a child RFQ and RFQ lines for each supplier.
<span class="xml"><span class="hljs-comment">&lt;!-- AI &gt;</span></span> Use tables: SPTL<span class="hljs-emphasis">_RFQ_</span>SPLR<span class="hljs-emphasis">_HEADER and SPTL_</span>RFQ<span class="hljs-emphasis">_SPLR_</span>LINE --&gt;
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Writing specs isn’t extra work; it is the work you should have been doing anyway. It’s just that the audience has changed, and you need to adapt to it.</div>
</div>

<h1 id="heading-leveraging-existing-patterns">Leveraging Existing Patterns</h1>
<p>In my case, AI wasn't starting from a blank slate. I was adding code to an existing package, and I also had other packages in the repository that the AI could reference for context.</p>
<p>In one instance, I had a specific pattern established for processing file uploads:</p>
<ol>
<li><p>Upload records from Excel into an <code>APEX_COLLECTION</code> using <code>APEX_DATA_PARSER</code>.</p>
</li>
<li><p>Run validations to check the uploaded records for errors.</p>
</li>
<li><p>Allow the user to review the validated records before final processing.</p>
</li>
<li><p>Perform the final import into the base tables.</p>
</li>
</ol>
<p>I pointed the AI to two existing procedures that followed this pattern and said, "Follow the pattern in procedures X and Y, but apply the logic from the specification below…".</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Because it had a "template" of my coding style, the AI-generated code felt like I had written it myself.</div>
</div>

<h1 id="heading-the-power-of-agentsmd">The Power of AGENTS.md</h1>
<p>The final piece of the puzzle was the use of <a target="_blank" href="http://agents.md"><code>AGENTS.md</code></a>. <code>AGENTS.md</code> is a file containing instructions that many coding agents, such as Codex, Cursor, and Claude, pass to the LLM along with your prompt. My <code>AGENTS.md</code> files are constantly evolving, but they typically include instructions like:</p>
<ul>
<li><p>Use APEX PL/SQL APIs where possible <code>APEX_STRING</code>, <code>APEX_JSON</code>, and <code>APEX_DEBUG</code> over custom logic.</p>
</li>
<li><p>Use set-based logic where possible instead of FOR loops.</p>
</li>
<li><p>Avoid Dynamic SQL wherever possible; if unavoidable, always use bind variables and validate identifiers (e.g., DBMS_ASSERT) to reduce SQL injection risk.</p>
</li>
<li><p>The folder structure of the codebase.</p>
</li>
<li><p>Prefer <code>%TYPE</code> and <code>%ROWTYPE</code></p>
</li>
<li><p>No hard-coded schema names.</p>
</li>
<li><p>Always include <code>APEX_DEBUG</code> calls in exception handlers and major logic branches.</p>
</li>
<li><p>Code formatting rules.</p>
</li>
<li><p>etc.</p>
</li>
</ul>
<p>Without this file, the AI defaults to "generic" PL/SQL. With it, the AI becomes an expert in my specific preferences and standards.</p>
<h1 id="heading-warning">Warning!</h1>
<p>As I’ve said before, AI is a tool, not a crutch. The code you build is your responsibility (not the AI’s). For now, at least!</p>
<ul>
<li><p><strong>Understand the Output:</strong> Before committing the code, you should understand what it does and that it is doing what it is supposed to do.</p>
</li>
<li><p><strong>Security is Your Job:</strong> I still manually check security settings at the end of every project. AI can find vulnerabilities, but it shouldn't be the only one looking.</p>
</li>
<li><p><strong>Test, test, and test again</strong>: AI does not replace testing, though it can help with it.</p>
</li>
</ul>
<h1 id="heading-the-ai-generated-code-checklist">The AI-Generated Code Checklist</h1>
<ul>
<li><p>Clean DDL + constraints + comments included</p>
</li>
<li><p>Markdown spec with rules + edge cases</p>
</li>
<li><p>Reference 1–2 existing “golden” procedures</p>
</li>
<li><p>Repo instructions (AGENTS.md)</p>
</li>
<li><p>Run tests + security review + performance sanity check</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This week proved that we are moving toward a world where the APEX Developer acts more like a conductor than a member of the orchestra. I think I am OK with this, but it does take some getting used to.</p>
<p>If your database design is solid, your patterns are consistent, and your requirements are clear, the actual coding becomes a commodity. The AI didn't just save me time; it allowed me to stay in the "flow state" of designing the solution rather than getting bogged down in the syntax of a 300-line package body.</p>
<p>If you haven't reached this inflection point yet, stop focusing solely on the "prompt" and start considering the <strong>context</strong> you provide to the AI.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🚀</div>
<div data-node-type="callout-text">When <strong>APEXlang</strong> lands, the context story will matter even more, because the unit of generation will shift from PL/SQL functions and procedures to larger app-level artifacts. Either way, the lesson holds: invest in metadata, patterns, and specs, and the AI stops guessing.</div>
</div>

<p>Exciting times ahead!</p>
]]></content:encoded></item><item><title><![CDATA[Understanding APEX Open Door Credentials]]></title><description><![CDATA[Introduction
If you, like me, have wondered why you would ever use Open Door Credentials, then this post is for you.
Open Door Credentials are a powerful tool when used for the right reasons and in the correct instance. They make testing and troubles...]]></description><link>https://blog.cloudnueva.com/understanding-apex-open-door-credentials</link><guid isPermaLink="true">https://blog.cloudnueva.com/understanding-apex-open-door-credentials</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 08 Jan 2026 15:45:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765687779151/ba31d2a3-aa72-4e2f-9f97-b05468c10cc7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>If you, like me, have wondered why you would ever use Open Door Credentials, then this post is for you.</p>
<p>Open Door Credentials are a powerful tool when used for the right reasons and in the correct instance. They make testing and troubleshooting easier, especially for applications using Authentication Schemes like Social Sign-On, but they require discipline and restraint.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Open Door Credentials should only be used as a temporary tool for testing and troubleshooting. Once you have completed your testing, they should be removed and should never (ever) be deployed to production.</div>
</div>

<h1 id="heading-what-are-open-door-credentials">What are Open Door Credentials?</h1>
<p>Open Door Credentials allow you to sign on to an APEX application using a username and a password. Nothing unusual there. The difference is that you can sign in using any username and password, even if they do not exist in your application. You can enter Username: ABC and Password: XYZ, and APEX will still let you in. This applies only to Authentication. Authorization, application logic, and security checks still apply and should be used to control what the authenticated user can actually do.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😱</div>
<div data-node-type="callout-text">Although this sounds super scary, it does have its uses in certain limited situations.</div>
</div>

<p>Once the user is signed in with Open Door Credentials, as far as your APEX App is concerned, there is nothing different:</p>
<ul>
<li><p>Once authenticated, the username behaves like any other value in <code>:APP_USER</code>. This means all authorization schemes, row-level security logic, VPD policies, and application-specific checks will execute exactly as they would for a real user, assuming you validate the username correctly.</p>
</li>
<li><p>Any code you have in the Login Processing section of the Authentication Scheme (Pre-Authentication Procedure Name, Post-Authentication Procedure Name) is still executed.</p>
</li>
</ul>
<h1 id="heading-why-are-open-door-credentials-useful">Why are Open Door Credentials Useful?</h1>
<p>Open Door Credentials are particularly useful during the testing phase of a project and for troubleshooting issues. They allow you to experience exactly what that user is experiencing.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I find Open Door Credentials most helpful for Applications that rely on Social Sign-on (e.g., Okta, Active Directory, Google) for authentication because these apps do not have a login page, and you cannot spoof other users.</div>
</div>

<p>An example of where Open Door Credentials are useful is when testing APEX Workflow. With APEX Workflow, you often need to log in as multiple users to test the process end-to-end. Getting all the participants on a call to test is impractical, and having their passwords is insecure. Open Door Credentials allow you to test the end-to-end process yourself.</p>
<h1 id="heading-words-of-caution">Words of Caution</h1>
<ul>
<li><p>It may seem obvious, but I will say it anyway. <strong>Never use Open Door Credentials in Production.</strong></p>
</li>
<li><p>Open Door Credentials should not be used (even in DEV) for applications that store personally identifiable information or any other data that requires security. That is, unless you mask this data when you clone from PROD to DEV/TEST.</p>
</li>
<li><p>If your DEV APEX App is open to the internet, then you should either add an IP restriction to your load balancer or restrict access to the APEX App you are testing by IP Address while testing with Open Door Credentials.</p>
</li>
<li><p>Validate the username. Open Door credentials will accept any username and password. You should validate that the username is a valid user of your application in the Application Level Authorization Scheme (hopefully, you were already doing this).</p>
</li>
</ul>
<h1 id="heading-how-to-create-open-door-credentials">How to Create Open Door Credentials</h1>
<p>Navigate to Shared Components &gt; Authentication Schemes</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765666731888/6a5e0067-68f3-4c1a-878a-b08767ee4862.png" alt="Navigate to Shared Components &gt; Authentication Schemes" class="image--center mx-auto" /></p>
<p>Click Create and select ‘Based on a pre-configured scheme from the gallery’.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765666767137/6a0063c0-c546-4410-86bb-25680e2ab586.png" alt="Select what the new Authentication Scheme is based on." class="image--center mx-auto" /></p>
<p>Select ‘Scheme Type’ as ‘Open Door Credentials’, and click ‘Create Authentication Scheme’.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765666991250/cf3f6e74-c555-44c5-adcf-a7240a4c60c4.png" alt="Select Open Door Credentials  as the Scheme Type" class="image--center mx-auto" /></p>
<p>Add your usual Pre and Post-Authentication logic:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765667051754/260d14fc-3edf-41b2-bb84-21f139847ca4.png" alt="Add Pre and Post-Authentication logic." class="image--center mx-auto" /></p>
<h2 id="heading-logging-in">Logging In</h2>
<p>When you make the new Authentication Scheme, the current scheme and login, you will be presented with an APEX-generated login page like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765686383706/714810ee-850b-489e-b09a-9326c0c4f97f.png" alt="APEX Generated Login Page" class="image--center mx-auto" /></p>
<p><strong>Note</strong>: When using Open Door Credentials, the password value is ignored by the Authentication Scheme. Any password will be accepted.</p>
<p>Interestingly, some environments, e.g., <a target="_blank" href="https://oracleapex.com/">https://oracleapex.com/</a>, only show the username:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765686519950/b55c4b92-f693-4dcd-b02e-bf13e9a8e69b.png" alt="Alternate APEX Generated Login Page" class="image--center mx-auto" /></p>
<h2 id="heading-deploying-the-app-from-dev-to-prod">Deploying the App from DEV to PROD</h2>
<p>Unfortunately, Authentication Schemes do not currently have Build Options. With a build option, you could set it so that APEX automatically excludes the Authentication Scheme whenever you export it for deployment to another instance.</p>
<p>Not having Build Options means you must manually delete the Open Door Credential Authentication Scheme before you deploy to PROD. There is an <a target="_blank" href="https://apex.oracle.com/ideas/FR-2152">idea in the APEX Ideas App</a> to add Build Options to Authentication Schemes. Even though it is flagged as on the Roadmap, it is over 4 years old, so I encourage you to vote for it.</p>
<h1 id="heading-alternatives-to-open-door-credentials">Alternatives to Open Door Credentials</h1>
<p>Another option is to use the <strong>Oracle APEX Accounts</strong> Authentication Scheme and create dedicated test users. This approach uses real credentials, but it requires you to build a login page and ongoing user management, which may be impractical for short-lived testing scenarios.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Open Door Credentials are a powerful tool when used for the right reasons. They make testing and troubleshooting simpler, especially for applications using external authentication, but they require discipline and restraint. Use them responsibly, lock them down properly, and remove them before deploying to production.</p>
]]></content:encoded></item><item><title><![CDATA[Build Dynamic Excel Upload Templates with APEX_DATA_EXPORT]]></title><description><![CDATA[Introduction
I have lost count of how many times I have developed an Excel Upload using APEX_DATA_PARSER. It provides users with a convenient way to mass-upload or update data using a tool they are familiar with. I usually include a link to a static ...]]></description><link>https://blog.cloudnueva.com/dynamic-excel-upload-templates</link><guid isPermaLink="true">https://blog.cloudnueva.com/dynamic-excel-upload-templates</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 18 Dec 2025 13:18:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763306636657/45d2007d-9c87-4d11-971e-f3b26498be83.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>I have lost count of how many times I have developed an Excel Upload using <a target="_blank" href="https://blog.cloudnueva.com/apexdataparser"><code>APEX_DATA_PARSER</code></a>. It provides users with a convenient way to mass-upload or update data using a tool they are familiar with. I usually include a link to a static Excel Template File that users can download to understand the structure they need to include in their upload.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In this post, I will show you how to dynamically generate Excel Upload Template Files, including sample data and custom instructions.</div>
</div>

<h1 id="heading-use-case">Use Case</h1>
<p>In a recent project, I was tasked with building a Hierarchy Management Application. This involved loading nodes (and their attributes), then loading hierarchies composed of those nodes. I’ll be focusing on the node (and attribute) upload.</p>
<p>The Nodes APEX UI looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763217245839/82576296-016b-43b5-9796-d82f478a90ed.png" alt="APEX Page Showing Nodes Page" class="image--center mx-auto" /></p>
<ul>
<li><p>A Segment Type is a grouping of like nodes.</p>
</li>
<li><p>The Bonus Eligible column is a custom node attribute. Users can specify up to 20 custom attributes for each Segment Type.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Given that the attributes differ across Segment Types, it is not realistic to have separate Static Excel templates for each Segment Type.</div>
</div>

<p>👉 Enter <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT.html">apex_data_export</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💁</div>
<div data-node-type="callout-text">The apex_data_export package allows you to export data from Oracle APEX. It supports several file types, including PDF, XLSX, HTML, CSV, XML, and JSON.</div>
</div>

<h1 id="heading-goal">Goal</h1>
<p>The goal is to download an Excel file containing the existing rows and columns for a Segment Type, make updates, and re-import it to apply those updates.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763236621617/92247494-0c08-40d3-b85c-1ed24a536a96.png" alt="Example Excel Template Generated by APEX_DATA_EXPORT" class="image--center mx-auto" /></p>
<h1 id="heading-approach">Approach</h1>
<h2 id="heading-build-sql">Build SQL</h2>
<p>My approach was first to build a function to generate dynamic SQL for both the APEX page shown above and for <code>apex_data_export</code> to use. The function below is intended to show an approximation of the function (it is not the actual code).</p>
<pre><code class="lang-sql">FUNCTION get_node_sql 
  (p_segment_type_id IN apx_rdm_segment_type.segment_type_id%TYPE) RETURN CLOB IS

  CURSOR cr_segment_attributes 
   (cp_segment_type_id IN apx_rdm_segment_type.segment_type_id%TYPE) IS
    <span class="hljs-keyword">SELECT</span> attr.attribute_code
    ,      attr.data_type_code
    ,      attr.attribute_name <span class="hljs-keyword">AS</span> display_value
    <span class="hljs-keyword">FROM</span>   apx_rdm_segment_type_attr sta
    ,      apx_rdm_attribute         <span class="hljs-keyword">attr</span>
    <span class="hljs-keyword">WHERE</span>  attr.attribute_id   = sta.attribute_id
    <span class="hljs-keyword">AND</span>    sta.segment_type_id = cp_segment_type_id
    <span class="hljs-keyword">ORDER</span>  <span class="hljs-keyword">BY</span> sta.sort_order;

  (p_segment_type_id IN apx_rdm_segment_type.segment_type_id%TYPE) RETURN CLOB IS
  l_sql               CLOB;
  l_col_count         PLS_INTEGER := 0;
  lc_lf               CONSTANT VARCHAR2(1) := CHR(10);
<span class="hljs-keyword">BEGIN</span>

  <span class="hljs-comment">-- Build the base columns for the hierarchy node.</span>
  l_sql := <span class="hljs-string">'SELECT hnode.node_code, hnode.node_name, hnode.system_name, hnode.postable_flag, hnode.active_flag'</span> || lc_lf;

  <span class="hljs-comment">-- Loop through custom attributes and Append a MAX(...) expression per </span>
  <span class="hljs-comment">--   attribute assigned to the segment type.</span>
  FOR rec IN cr_segment_attributes (cp_segment_type_id =&gt; p_segment_type_id) LOOP
    l_col_count := l_col_count + 1;
    EXIT WHEN l_col_count &gt; 20;
      l_sql := l_sql
        || ',      MAX(CASE WHEN attrval.attribute_code = '''
        || <span class="hljs-keyword">REPLACE</span>(rec.attribute_code, <span class="hljs-string">''''</span>, <span class="hljs-string">''''''</span>)
        || <span class="hljs-string">''' THEN attrval.attr_value_varchar END) AS "'</span>
        || <span class="hljs-keyword">REPLACE</span>(rec.attribute_code, <span class="hljs-string">'"'</span>, <span class="hljs-string">'""'</span>)
        || <span class="hljs-string">'"'</span> || lc_lf;
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;

  <span class="hljs-comment">-- Add FROM and WHERE clauses.</span>
  l_sql := l_sql ||
  'FROM   apx_rdm_hierarchy_node_svw hnode
  LEFT JOIN apx_rdm_node_attribute_value_vw attrval 
    ON     attrval.node_id = hnode.node_id
    WHERE hnode.segment_type_id = :SEGMENT_TYPE_ID' || lc_lf;

  <span class="hljs-comment">-- Add Group By Clause.</span>
  <span class="hljs-comment">-- Attribute values are aggregated with MAX so they do not belong in the GROUP BY.</span>
  l_sql := l_sql || 'GROUP BY hnode.node_code, hnode.node_name, hnode.system_name, hnode.postable_flag, hnode.active_flag';

  RETURN l_sql;

<span class="hljs-keyword">END</span> get_node_sql;
</code></pre>
<h2 id="heading-fetch-sql-format-output-generate-amp-store-excel">Fetch SQL, Format Output, Generate &amp; Store Excel</h2>
<p>Next, write a procedure to generate the Excel Upload Template File. Again, the code below is an extract of the highlights from the actual code and is not complete.</p>
<pre><code class="lang-sql">PROCEDURE generate_node_upload_template
 (p_segment_type_id   IN apx_rdm_segment_type.segment_type_id%TYPE,
  p_segment_type_code IN apx_rdm_segment_type.segment_type_code%TYPE) IS

  <span class="hljs-comment">-- apex_exec and apex_data_export types.</span>
  l_xlsx_context        apex_exec.t_context;
  l_sql_params          apex_exec.t_parameters;
  l_xlsx_export         apex_data_export.t_export;
  lt_columns            apex_data_export.t_columns;
  l_print_config        apex_data_export.t_print_config;
  l_sql                 CLOB;

<span class="hljs-keyword">BEGIN</span>

  <span class="hljs-comment">-- Generate the SQL to retrieve the nodes and their attributes.</span>
  l_sql := get_node_sql (p_segment_type_id =&gt; p_segment_type_id);

  <span class="hljs-comment">-- Add Static Colummn Definitions and Headings.</span>
  <span class="hljs-comment">-- Note the use of p_is_frozen to the first three columns. This will freeze </span>
  <span class="hljs-comment">--  these three columns in the Excel output. </span>
  <span class="hljs-comment">-- The frozen columns must be the first contiguous columns.</span>
  <span class="hljs-comment">-- p_heading is the value used in the output Excel.</span>
  apex_data_export.add_column
   (p_columns =&gt; lt_columns,
    p_name    =&gt; 'NODE_CODE',
    p_heading =&gt; 'Node Code',
    p_is_frozen =&gt; TRUE);
  apex_data_export.add_column
   (p_columns =&gt; lt_columns,
    p_name    =&gt; 'NODE_NAME',
    p_heading =&gt; 'Node Name',
    p_is_frozen =&gt; TRUE);
  apex_data_export.add_column
   (p_columns =&gt; lt_columns,
    p_name    =&gt; 'SYSTEM_NAME',
    p_heading =&gt; 'Source System Name',
    p_is_frozen =&gt; TRUE);
  apex_data_export.add_column
   (p_columns =&gt; lt_columns,
    p_name    =&gt; 'POSTABLE_FLAG',
    p_heading =&gt; 'Postable Flag');
  apex_data_export.add_column
   (p_columns =&gt; lt_columns,
    p_name    =&gt; 'ACTIVE_FLAG',
    p_heading =&gt; 'Active Flag');

  <span class="hljs-comment">-- Loop through attributes for the Segment Type adding dynamic attribute columns.</span>
  FOR lr_segment_attrs IN cr_segment_attributes (cp_segment_type_id =&gt; p_segment_type_id) LOOP
    apex_data_export.add_column
     (p_columns =&gt; lt_columns,
      p_name    =&gt; lr_segment_attrs.attribute_code,
      p_heading =&gt; lr_segment_attrs.display_value);
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;

  <span class="hljs-comment">-- Add SQL Parameters / Bind Variable Values</span>
  apex_exec.add_parameter(l_sql_params, 'SEGMENT_TYPE_ID', p_segment_type_id);

  <span class="hljs-comment">-- Add Branding Colors to the Heading and make Heading Font Size Larger.</span>
  l_print_config := apex_data_export.get_print_config
                     (p_header_bg_color   =&gt; '<span class="hljs-comment">#020381',</span>
                      p_header_font_color =&gt; '<span class="hljs-comment">#FFFFFF',</span>
                      p_header_font_size  =&gt; 11);

  <span class="hljs-comment">-- Open the Query Context, bind the parameters and execute the query.</span>
  l_xlsx_context := apex_exec.open_query_context
                     (p_location       =&gt; apex_exec.c_location_local_db,
                      p_sql_parameters =&gt; l_sql_params,
                      p_sql_query      =&gt; l_sql);

  <span class="hljs-comment">-- Export to XLSX format</span>
  l_xlsx_export := apex_data_export.export
                     (p_context           =&gt; l_xlsx_context,
                      <span class="hljs-comment">-- ⬇️ Determines the Export File Format</span>
                      p_format            =&gt; apex_data_export.c_format_xlsx,
                      <span class="hljs-comment">-- ⬇️ Defines the Columns and Headings in the Export</span>
                      p_columns           =&gt; lt_columns,
                      <span class="hljs-comment">-- ⬇️ Used for the Excel Tab Name</span>
                      p_page_header       =&gt; p_segment_type_code,  
                      <span class="hljs-comment">-- ⬇️ Text Appears at the top of the Excel Sheet</span>
                      p_supplemental_text =&gt; 'This template is to be used for uploading Nodes into the [' || 
                                              p_segment_type_code || '] Segment Type. ',  
                      <span class="hljs-comment">-- ⬇️ Name of the Exported File</span>
                      p_file_name         =&gt; 'Node_Upload_Template',
                      <span class="hljs-comment">-- ⬇️ Print Configuration to use for the Exported File</span>
                      p_print_config      =&gt; l_print_config);
  apex_exec.close(l_xlsx_context);

  <span class="hljs-comment">-- Store the Exported File in an APEX Collection for Later Use.</span>
  apex_collection.create_or_truncate_collection(p_collection_name =&gt; 'NODES_TEMPLATE_COLLN');
  apex_collection.add_member
   (p_collection_name =&gt; 'NODES_TEMPLATE_COLLN',
    p_blob001         =&gt; l_xlsx_export.content_blob,
    p_c001            =&gt; l_xlsx_export.file_name);
<span class="hljs-keyword">END</span> generate_node_upload_template;
</code></pre>
<ul>
<li><p>You don’t have to use <code>apex_data_export.add_column</code>. If you do not, then APEX will use the column names (or aliases) from your SQL for the column headers.</p>
</li>
<li><p>The <code>p_is_frozen</code> parameter <code>apex_data_export.add_column</code> has to be applied to all of the columns you want frozen.</p>
</li>
<li><p>For my use case, I call the generate procedure above from a Dynamic Action, temporarily store the Excel file in a Collection, and then download it using the Download Dynamic Action (check out this <a target="_blank" href="https://lmoreaux.hashnode.dev/how-to-easily-download-files-in-oracle-apex-241#heading-new-download-dynamic-action">post</a> from <a class="user-mention" href="https://hashnode.com/@lmoreaux">Louis Moreaux</a> for more).</p>
</li>
</ul>
<h1 id="heading-more-on-apexdataexport">More on apex_data_export</h1>
<p>In this post, I focused on a specific use case and some specific features of apex_data_export. In this section, I will cover additional key features and user cases.</p>
<h2 id="heading-other-key-features">Other Key Features</h2>
<h3 id="heading-column-grouping"><strong>Column Grouping</strong></h3>
<p>The <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT-ADD_COLUMN_GROUP-Procedure.html"><code>apex_data_export.add_column_group</code></a> API allows you to organize columns into groups with group headings to generate something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763261031980/0cd689ac-d194-4048-90ab-c5c0bffbca60.png" alt="APEX_DATA_EXPORT Excel Export with Column Groups" class="image--center mx-auto" /></p>
<h3 id="heading-aggregating">Aggregating</h3>
<p>The <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT-ADD_AGGREGATE-Procedure.html"><code>apex_data_export.add_aggregate</code></a> API allows you to calculate totals for numeric values. This can be used in conjunction with the <code>p_is_column_break</code> parameter of <code>apex_data_export.add_column</code> to generate group totals.</p>
<h3 id="heading-highlights">Highlights</h3>
<p>The <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT-ADD_HIGHLIGHT-Procedure.html"><code>apex_data_export.add_highlight</code></a> API allows you to highlight cells in your output based on columns in your data source. The way this works is a little different.</p>
<p>In your SQL, return the highlight ID based on the criteria you want to use. In the example below, I have two highlights with IDs 1 and 2.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> order_number
,      order_total
,      due_date
,      <span class="hljs-keyword">CASE</span> <span class="hljs-keyword">WHEN</span> order_total &gt; <span class="hljs-number">1000</span> <span class="hljs-keyword">THEN</span> <span class="hljs-number">1</span> <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> order_total_highlight
,      <span class="hljs-keyword">CASE</span> <span class="hljs-keyword">WHEN</span> due_date &gt; <span class="hljs-keyword">SYSDATE</span> <span class="hljs-keyword">THEN</span> <span class="hljs-number">2</span> <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> due_date_highlight
<span class="hljs-keyword">FROM</span>   orders
</code></pre>
<pre><code class="lang-sql"><span class="hljs-comment">-- Define Highlight for Order Total</span>
apex_data_export.add_highlight(
        p_highlights          =&gt; l_highlights,
        p_id                  =&gt; 1,         <span class="hljs-comment">-- Order Total Highlight from SQL</span>
        p_value_column        =&gt; 'ORDER_TOTAL_HIGHLIGHT',
        p_display_column      =&gt; 'ORDER_TOTAL',  <span class="hljs-comment">-- Where to put the highlight</span>
        p_text_color          =&gt; '<span class="hljs-comment">#FF0000' );</span>

<span class="hljs-comment">-- Define Highlight for Due Date</span>
apex_data_export.add_highlight(
        p_highlights          =&gt; l_highlights,
        p_id                  =&gt; 2,         <span class="hljs-comment">-- Due Date Highlight from SQL</span>
        p_value_column        =&gt; 'DUE_DATE_HIGHLIGHT',
        p_display_column      =&gt; 'DUE_DATE',  <span class="hljs-comment">-- Where to put the highlight</span>
        p_text_color          =&gt; '<span class="hljs-comment">#FF0000' );</span>
</code></pre>
<ul>
<li>If the SQL returns a 1 for the <code>order_total_highlight</code> column, then the highlight is applied; otherwise, it is not. The same goes for <code>due_date_highlight</code>, except it needs to return a 2 for the highlight to be applied.</li>
</ul>
<h3 id="heading-download">Download</h3>
<p>The <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT-DOWNLOAD-Procedure.html"><code>apex_data_export.download</code></a> API allows you to initiate a download of the file generated by apex_data_export (or any other BLOB for that matter). Check out this <a target="_blank" href="https://haniel.hashnode.dev/easy-file-downloads-with-apexdataexportdownload">post</a> from <a class="user-mention" href="https://hashnode.com/@Haniel">Haniel Burton</a> for more.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💪</div>
<div data-node-type="callout-text">We are in a golden age of APEX file downloads. We have the <a target="_self" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_HTTP.html">apex_http</a> API, the apex_data_export.export method, and native Dynamic Actions!</div>
</div>

<h3 id="heading-print-config">Print Config</h3>
<p>The <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_DATA_EXPORT-GET_PRINT_CONFIG-Procedure.html"><code>apex_data_export.get_print_config</code></a> API, when used in conjunction with the <code>p_print_config</code> parameter of <code>apex_data_export.export</code>, is used to drive print formatting for your export. Here are the settings applicable to Excel exports:</p>
<ul>
<li><p>Page Header (p_page_header) is used for the tab name, unless overridden in the parameter with the same name in the call to <code>apex_data_export.export</code>.</p>
</li>
<li><p>Page footer-related parameters (p_page_footer_…): add and format text below the last cell in the Excel output.</p>
</li>
<li><p>Heading-related parameters (p_header_…): apply formatting to the Excel column headings.</p>
</li>
<li><p>Body-related parameters (p_body_…): apply formatting to the rows in the Excel output.</p>
</li>
<li><p>Border-related parameters (p_border_…): apply formatting to the cell borders in the Excel output.</p>
</li>
</ul>
<h2 id="heading-other-use-cases-for-apexdataexport">Other Use Cases for APEX_DATA_EXPORT</h2>
<p>In this post, I focused on a specific use case, but there are many others for <code>apex_data_export</code>.</p>
<ul>
<li><p>Exporting Setup Data in JSON for import into other systems or moving setups from DEV &gt; TEST &gt; PROD.</p>
</li>
<li><p>Generate downloads of data triggered from a button press, where you don’t have an Interactive Report or Grid to handle the download.</p>
</li>
<li><p>Generate a report from an APEX Automation and attach it to an email for distribution.</p>
</li>
<li><p>G<strong>enerate specialized “operational snapshot” files</strong> for downstream teams or auditors: Use the API to produce point-in-time exports (with highlights, aggregates, or formatting) from PL/SQL jobs, ideal for monthly financial freezes, audit cycles, or HR snapshots.</p>
</li>
</ul>
<h2 id="heading-apexexec">APEX_EXEC</h2>
<p>Part of the flexibility of apex_data_export stems from its use of the powerful <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_EXEC.html">apex_exec</a> API. This allows you to generate exports from REST APIs just as easily as from database tables.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Dynamic Excel template generation is one of those APEX features that solves a real problem: keeping users aligned with data structures that aren’t static. By pairing apex_data_export with apex_exec, you eliminate the maintenance headache of versioning static spreadsheets and give users templates that consistently reflect the current configuration; columns, attributes, sample values, instructions, everything.</p>
]]></content:encoded></item><item><title><![CDATA[How I Use AI to Make Me a Better APEX Developer]]></title><description><![CDATA[Introduction
I have been using AI to help me build APEX Apps for more than a year, and the pace of change during that time has been amazing. In this post I will review the tools and workflows that that have helped me to improve as an APEX Developer.
...]]></description><link>https://blog.cloudnueva.com/how-i-use-ai-for-apex-development</link><guid isPermaLink="true">https://blog.cloudnueva.com/how-i-use-ai-for-apex-development</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 27 Nov 2025 08:36:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763914219563/e26bdd19-ec8a-4b5f-9862-786f7369189f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>I have been using AI to help me build APEX Apps for more than a year, and the pace of change during that time has been amazing. In this post I will review the tools and workflows that that have helped me to improve as an APEX Developer.</p>
<p>I won’t be getting into how to set up these tools, just how I use them.</p>
<h1 id="heading-ai-subscriptions">AI Subscriptions</h1>
<p>I currently have the following AI Subscriptions:</p>
<ul>
<li><p>Open AI Chat GPT Business ($300/year)</p>
<ul>
<li>I use this subscription for ChatGPT, APIs, and Codex</li>
</ul>
</li>
<li><p>GitHub Co-Pilot Pro ($100/year)</p>
</li>
</ul>
<h1 id="heading-ai-tools">AI Tools</h1>
<p>These are the AI tools I am using today:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>Usage</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://code.visualstudio.com/docs/copilot/overview">GitHub Copilot in VS Code</a></td><td>Ghost Text / Autocomplete, Changes to PL/SQL code in the IDE.</td></tr>
<tr>
<td><a target="_blank" href="https://developers.openai.com/codex/ide/">Codex Plugin</a> in VS Code</td><td>Agent mode changes made from VS Code. Working with SQLcl MCP Server</td></tr>
<tr>
<td><a target="_blank" href="https://chatgpt.com/features/codex">Codex CLI</a></td><td>Tasks I can let run in the background, e.g., Code Reviews, Generating Repository Documentation, Creating Draft Data Models, Refactoring Code, etc.</td></tr>
<tr>
<td>GitHub Copilot in GitHub Desktop</td><td>Auto-Generate Commit Message</td></tr>
<tr>
<td><a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/using-oracle-sqlcl-mcp-server.html">Oracle SQLcl MCP Server</a></td><td>Talking to the Database. Tuning SQL, Understanding Data Models, etc.</td></tr>
</tbody>
</table>
</div><h1 id="heading-auto-complete-ghost-text">Auto-Complete / Ghost Text</h1>
<p>I estimate that <strong>Ghost Text</strong> accounts for about 50% of my overall productivity gains with AI. This is the feature where the AI predicts your next step based on the surrounding code and provides a suggestion you can accept with the Tab key. It is especially useful for repetitive tasks, such as:</p>
<ul>
<li><p>Writing <code>MERGE</code> and <code>INSERT INTO</code> statements. The AI leverages the <strong>schema and table structure</strong> currently open in the IDE (e.g., the columns and data types) to accurately auto-complete the statement after just a few initial lines.</p>
</li>
<li><p>Automatically adding comments and apex_debug entries.</p>
</li>
<li><p>Autocompleting the <code>EXCEPTION WHEN OTHERS</code> block of a procedure or function.</p>
</li>
<li><p>Applying a required change across multiple related code blocks.</p>
</li>
</ul>
<p>Here is a short video showing me creating a new procedure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763439879194/cf899552-58ba-40f3-ba31-3d1478ce564c.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-code-reviews">Code Reviews</h1>
<p>I use AI to do three different kinds of code reviews (of my own work):</p>
<ul>
<li><p>End of Day Review of all changes made during the day</p>
</li>
<li><p>Ad-Hoc review of a PL/SQL package</p>
</li>
<li><p>Full Code Review</p>
</li>
</ul>
<h2 id="heading-end-of-day-review">End of Day Review</h2>
<p>At the end of every day, I like to run an automated code review of the changes I made that day. The Codex CLI makes this easy with the <code>/review</code> command.</p>
<p>Change to the root folder of the GitHub repository and start Codex, then type <code>/review</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763856199041/2875242c-efef-4f3f-9e6f-f90f2af78e16.png" alt="Codex Review 1" class="image--center mx-auto" /></p>
<p>Then choose what kind of review you want to do; option 2 is what I do.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763856301554/5853d236-1e1a-43e2-a81e-8c861fb86561.png" alt="Codex Review 2" class="image--center mx-auto" /></p>
<p>Codex will then compare the last committed version with the changes and perform a review.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I can work on something else while it is running.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763856339376/3c1a54aa-636c-4aa8-a73b-b11c23ee8110.png" alt="Codex Review 3" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It is surprising how many issues this daily review catches.</div>
</div>

<h2 id="heading-ad-hoc-review-plsql-package">Ad-Hoc Review (PL/SQL Package)</h2>
<p>Every so often, I like to do a complete review of a package. I use the Codex CLI for this so that I can run it in the background. The key here is to develop a prompt that reviews the code the way you would like it reviewed.</p>
<p>Here is the prompt I currently use:</p>
<pre><code class="lang-markdown"><span class="hljs-section"># Code Review Objective</span>
Perform a code review for the following files:
<span class="hljs-bullet">-</span> <span class="hljs-code">`PLSQL/XXFN_RFQ_UTL_PKB.sql`</span>
<span class="hljs-bullet">-</span> <span class="hljs-code">`PLSQL/XXFN_RFQ_UTL_PKS.sql`</span>

Begin with a concise checklist (3-7 bullets) of the sub-tasks required to complete this review; keep items conceptual, not implementation-level.
<span class="hljs-section"># Tasks</span>
Review each file for:
<span class="hljs-bullet">-</span> Logic errors
<span class="hljs-bullet">-</span> Unused variables
<span class="hljs-bullet">-</span> Lack of code reuse
<span class="hljs-bullet">-</span> Security Concerns
<span class="hljs-bullet">-</span> Poorly performing code

For each identified issue, state your assumptions and verify the potential impact before assigning severity.
<span class="hljs-section"># Output Instructions</span>
<span class="hljs-bullet">-</span> Document all findings in <span class="hljs-code">`RFQ_CODE_REVIEW.md`</span>.
<span class="hljs-bullet">-</span> Use a Markdown table with the columns:
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**File**</span>
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**Line(s)**</span>
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**Issue Type**</span>
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**Description**</span>
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**Severity**</span> (Low/Medium/High)
<span class="hljs-bullet">    -</span> <span class="hljs-strong">**Suggested Fix**</span>
<span class="hljs-bullet">-</span> Group issues by file and list findings sequentially within each file.
<span class="hljs-bullet">-</span> If a file has no issues, include a row stating "No issues found" in the <span class="hljs-strong">**Description**</span> column, leaving the other columns blank for that row.
<span class="hljs-bullet">-</span> Assign <span class="hljs-strong">**Severity**</span> based on the potential impact of the issue; if uncertain about severity or affected lines, specify your assumption and reasoning.
<span class="hljs-bullet">-</span> For each finding, specify the affected line(s) precisely. For multi-line or file-wide issues, indicate the corresponding range or "entire file" as needed.

After reviewing and documenting, validate that all findings are clearly described and all required fields are filled in; if any instructions are ambiguous or criteria unmet, highlight them at the end of the output for clarification.
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Keep iterating on the prompt until you get the results you want. I am constantly fine-tuning my prompts. Furthermore, when a new AI model is released, you should review your prompts and update them according to the latest prompting guide.</div>
</div>

<h2 id="heading-complete-code-review">Complete Code Review</h2>
<p>I run a code review of the complete codebase about once a week. Once again, I turn to the Codex CLI to run this so I can work on something else while it runs.</p>
<p>Here is the prompt I currently use:</p>
<pre><code class="lang-markdown">You are acting as a senior Oracle APEX / PL/SQL architect and code reviewer.

Context:
<span class="hljs-bullet">-</span> The repository contains one or more Oracle APEX applications, shared components, PL/SQL packages, functions, procedures, triggers, views, and other database objects.
<span class="hljs-bullet">-</span> Assume it is used in a production or near-production environment.
<span class="hljs-bullet">-</span> Focus heavily on correctness, security, performance, maintainability, and APEX best practices.

Your task:
Perform a <span class="hljs-strong">**comprehensive code review**</span> of the entire repo (APEX export files, PL/SQL packages, views, triggers, utility scripts, etc.) and then produce a <span class="hljs-strong">**single Markdown report**</span> as your only output.

General review focus:
<span class="hljs-bullet">1.</span> <span class="hljs-strong">**Correctness &amp; robustness**</span>
<span class="hljs-bullet">   -</span> Logic errors, edge cases, null/empty handling, and error handling.
<span class="hljs-bullet">   -</span> Transaction handling and commit/rollback discipline.
<span class="hljs-bullet">   -</span> Concurrency issues (locks, race conditions, serialization).
<span class="hljs-bullet">2.</span> <span class="hljs-strong">**Security**</span>
<span class="hljs-bullet">   -</span> SQL injection risks (dynamic SQL, concatenated where clauses, using NVL with parameters, etc.).
<span class="hljs-bullet">   -</span> XSS / output escaping issues in APEX (unescaped substitutions, htp.p/owa<span class="hljs-emphasis">_util.showpage usage, missing server-side validation).
   - Session and authorization handling (APEX authorization schemes, access control logic).
   - Sensitive data handling (logging PII, passwords, tokens, or secrets).
   - Use of APEX substitution strings, bind variables, and item values in queries and PL/SQL.
3. <span class="hljs-strong">**Performance &amp; scalability**</span>
   - N+1 query patterns, repeated queries in loops.
   - Missing or misused indexes, non-selective predicates, full table scans where dangerous.
   - Inefficient PL/SQL patterns (unnecessary row-by-row processing, missing BULK COLLECT / FORALL where appropriate).
   - Heavy computations in views vs. materialized views or caching strategies.
   - APEX-specific performance concerns (expensive queries in report regions, interactive report/grid filters, LOVs with slow queries).
4. <span class="hljs-strong">**APEX application design**</span>
   - Page process and branch logic clarity.
   - Proper use of shared components (lists, LOVs, templates, authorization schemes).
   - Hard-coded values vs. configuration tables/application items.
   - Use of APEX APIs (APEX_</span>APPLICATION, APEX<span class="hljs-emphasis">_UTIL, APEX_</span>PAGE, APEX<span class="hljs-emphasis">_SESSION, APEX_</span>DEBUG, etc.) and any risky or deprecated calls.
<span class="hljs-bullet">5.</span> <span class="hljs-strong">**Code quality &amp; maintainability**</span>
<span class="hljs-bullet">   -</span> Naming conventions for packages, procedures, variables, constants, and views.
<span class="hljs-bullet">   -</span> Comment quality and accuracy (misleading or outdated comments).
<span class="hljs-bullet">   -</span> Module boundaries and cohesion (what should be refactored into separate packages).
<span class="hljs-bullet">   -</span> Reusability and DRY violations (duplicate logic that should be centralized).
<span class="hljs-bullet">   -</span> Error logging and instrumentation (APEX debug, custom logging tables, structured error messages).
<span class="hljs-bullet">6.</span> <span class="hljs-strong">**Database design &amp; views**</span>
<span class="hljs-bullet">   -</span> View complexity and readability.
<span class="hljs-bullet">   -</span> Security of views (exposing too much data, missing filters, relying on client-side filters).
<span class="hljs-bullet">   -</span> Use of synonyms, grants, and schema separation.

Output requirements:
<span class="hljs-bullet">-</span> Only output a single Markdown document called CodeReview.md
<span class="hljs-bullet">-</span> No explanation or actions outside of the Markdown.
<span class="hljs-bullet">-</span> Structure the report so that issues are easy to scan and the related code is easy to find.

Markdown structure:
<span class="hljs-bullet">1.</span> # APEX Repo Code Review
<span class="hljs-bullet">   -</span> Short overview of the overall health of the codebase.
<span class="hljs-bullet">   -</span> 2–3 key strengths.
<span class="hljs-bullet">   -</span> 3–5 top-priority concerns.

<span class="hljs-bullet">2.</span> ## Summary by Severity
<span class="hljs-bullet">   -</span> A bullet list showing counts:
<span class="hljs-bullet">     -</span> <span class="hljs-code">`Severe issues: N`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`Moderate issues: N`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`Low issues: N`</span>

<span class="hljs-bullet">3.</span> ## Detailed Findings
<span class="hljs-bullet">   -</span> Group by logical area, for example:
<span class="hljs-bullet">     -</span> <span class="hljs-code">`### PL/SQL Packages`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`### APEX Pages &amp; Processes`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`### Views &amp; SQL`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`### Security`</span>
<span class="hljs-bullet">     -</span> <span class="hljs-code">`### Performance`</span>
<span class="hljs-bullet">   -</span> Under each area, list issues as subsections:

   Example structure for each issue:
   #### [Severity] Short issue title
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Severity:**</span> Severe | Moderate | Low
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Location:**</span> <span class="hljs-code">`path/to/file`</span> (and object name, page number, or line range if available)
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Description:**</span>
<span class="hljs-bullet">     -</span> Clear explanation of what is wrong and why it matters.
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Impact:**</span>
<span class="hljs-bullet">     -</span> Security / correctness / performance / maintainability impact in 1–3 lines.
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Recommendation:**</span>
<span class="hljs-bullet">     -</span> Specific and actionable guidance on how to fix it.
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Code snippet (before):**</span>
<span class="hljs-bullet">     -</span> Show only the relevant lines, minimal but sufficient context
<span class="hljs-bullet">   -</span> <span class="hljs-strong">**Suggested fix (after or pseudo-code):**</span>
<span class="hljs-bullet">     -</span> Show improved version or a clear pseudo-code template

<span class="hljs-bullet">4.</span> ## Pattern-Level Recommendations
<span class="hljs-bullet">    -</span> Describe any recurring patterns that should be globally fixed (e.g., unsafe dynamic SQL patterns, repeated logic for auditing, repeated branching logic in APEX).
<span class="hljs-bullet">    -</span> Provide 2 to 5 concrete "refactoring themes" that would significantly improve the codebase.

<span class="hljs-bullet">5.</span> ## APEX-Specific Recommendations
<span class="hljs-bullet">    -</span> Suggestions for:
<span class="hljs-bullet">        -</span> Better use of shared components.
<span class="hljs-bullet">        -</span> Improved authorization and authentication handling.
<span class="hljs-bullet">        -</span> Performance tuning for heavy pages, reports, and LOVs.
<span class="hljs-bullet">        -</span> Hardening against XSS and misuse of substitution strings.
<span class="hljs-bullet">6.</span> ## Quick-Win Checklist
<span class="hljs-bullet">    -</span> A concise bullet list of the most important fixes (in priority order) that the team should implement next.

Severity rubric:
Use the following rubric consistently:
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Severe:**</span>
<span class="hljs-bullet">    -</span> Can cause data corruption, security vulnerabilities, major logical errors, or severe performance degradation in realistic conditions.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Moderate:**</span>
<span class="hljs-bullet">    -</span> Risky or inefficient patterns that can cause noticeable issues under load or over time, but not immediately catastrophic.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Low:**</span>
<span class="hljs-bullet">    -</span> Style, readability, minor performance improvements, or best-practice alignment that helps maintainability but is not urgent.

Additional rules:
<span class="hljs-bullet">-</span> Prefer <span class="hljs-strong">**precision over volume**</span>: do not list 100 trivial issues; focus on the most impactful ones, while still giving enough detail to be useful.
<span class="hljs-bullet">-</span> When in doubt, <span class="hljs-strong">**show code**</span>: include short but focused code snippets in fenced blocks so developers can quickly locate and fix the problem.
<span class="hljs-bullet">-</span> If something looks dangerous but you are not fully sure (based on the visible context), call it out as a <span class="hljs-strong">**potential issue**</span> and clearly say what assumptions you are making.

Now, perform this review and output only the Markdown report described above.
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Notice that I am asking the AI to output its results to a Markdown file. This makes it easy for me to read the results and could serve as a requirement for feeding back into AI to fix the issues.</div>
</div>

<h1 id="heading-oracle-sqlcl-mcp-server">Oracle SQLcl MCP Server</h1>
<p>Please see <a target="_blank" href="https://blog.cloudnueva.com/oracle-sqlcl-mcp-server-with-codex-and-copilot-joelkallmanday">this post</a> for more on how I use the Oracle SQLcl MCP Server.</p>
<h1 id="heading-writing-new-code">Writing New Code</h1>
<p>You will have noticed that, so far, I have not discussed writing brand-new code based solely on a prompt. This is where I start to get nervous about AI.</p>
<p>Lately, I have started having AI write individual PL/SQL functions and procedures. I have noticed a significant improvement here with the latest models (OpenAI 5.1). I see even better results when I am adding procedures and functions to existing packages. I am sure this is because the additional context helps AI write better code.</p>
<p>This, along with writing SQL statements (using the MCP server), is the sweet spot when it comes to generating new code with AI.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I don’t think we are there yet when it comes to building new APEX Apps from scratch with AI. But let’s see what happens when APEXlang comes out!</div>
</div>

<h1 id="heading-what-about-apex">What About APEX</h1>
<p>At the moment (before the release of APEX <s>25.2,</s> 26.1), AI is quite capable of reviewing and providing feedback on APEX Apps. This can be done via SQL using the SQLcl MCP server (and the right prompts), or based on AI extracting information from an APEX export file.</p>
<p>When I run the code reviews (mentioned above), AI checks the exported APEX Apps, and I get just as many findings from APEX as from PL/SQL.</p>
<p>I find AI especially useful for:</p>
<ul>
<li><p>Checking APEX security settings (SSP, Authorization Schemes Applied to Buttons, etc.).</p>
</li>
<li><p>Building custom UI Components (e.g., template components).</p>
</li>
</ul>
<h2 id="heading-apex-ai-wizard">APEX AI Wizard</h2>
<p>APEX 24.2 does have a Wizard that allows you to create basic Apps from a prompt (and or a spreadsheet). This is OK for one-off Apps, but I don’t think it is the long-term answer for building APEX Apps with AI (nor does Oracle).</p>
<h2 id="heading-apexlang">APEXlang</h2>
<p>APEXlang will be the future of building APEX Apps with AI. It is not out yet, but I saw a demo at this year’s Oracle AI World, and it looks very promising. Having a formal syntax (that an AI can learn) will unlock the ability to build brand new Apps and perform major refactors on your APEX Apps.</p>
<h2 id="heading-ai-controlled-browsers">AI Controlled Browsers</h2>
<p>When <a target="_blank" href="https://openai.com/index/introducing-chatgpt-atlas/">ChatGPT Atlas</a> was released, I immediately logged into my <a target="_blank" href="https://oracleapex.com/ords">https://oracleapex.com/ords</a> instance and asked AI to write an App to track the service history for my car. I was pretty impressed, it used QuickSQL to generate a data model and then built a fully functional App (albeit relatively simple).</p>
<p>I think the future of AI-controlled browsers and APEX lies in automated testing. You can turn your test scripts into prompts and have the browser run them. Google’s <a target="_blank" href="https://antigravity.google/">AntiGravity</a> IDE takes this a step further and can capture screenshots of the testing along the way.</p>
<h1 id="heading-how-do-i">How do I?</h1>
<p>The final use of AI for me is asking ad hoc questions. I work for myself, so I don’t have colleagues to ask questions of around the watercooler.</p>
<h1 id="heading-agentsmd">AGENTS.MD</h1>
<p>I encourage you to spend an afternoon (or two) developing a robust <a target="_blank" href="https://agents.md/">AGENTS.md</a> file for each of your code repositories. AGENTS.md gives you the ability to tell the AI what your naming conventions are, how you like to structure SQL statements, and general guidelines for how you want it to behave when generating code. Without it, it relies on your existing code base, and you will often end up spending as much time formatting your code as the time the AI saves you to start with.</p>
<p>I am not sure if <code>AGENTS.md</code> will become the de facto standard, but it is a better alternative than having AI coding agents use their own files to instruct the AI (.github/copilot-instructions.md, cursor.json, etc).</p>
<h1 id="heading-warning">Warning!</h1>
<p>There are a few things that I think you must keep in mind when using AI to help you code:</p>
<ul>
<li><p><strong>AI is a Tool, not a Crutch</strong> - There is no substitute for knowing what the code is supposed to look like. We all write the odd JavaScript snippet using AI without really understanding the output, but this should not be the default. If you are building code and getting paid for it, you'd better understand what it does!</p>
</li>
<li><p><strong>Never Leave Security Entirely Up to AI</strong> - I mentioned above that AI does a good job of checking security settings in APEX. Even though it does, I always manually check security settings at the end of a project (using my own SQL statements or tools like <a target="_blank" href="https://github.com/oracle-samples/apex-sert">APEX-SERT</a>, <a target="_blank" href="https://apexsec.recx.co.uk/">ApexSec,</a> or <a target="_blank" href="https://www.united-codes.com/products/apexprojecteye/">APEX Project Eye</a>).</p>
</li>
<li><p><strong>AI is Making me Dumber</strong> - There is no doubt in my mind that AI is making me dumber. When you rely on any tool for a period of time, your body adapts. When you buy a snow blower, the muscles you had from shoveling snow atrophy. The same can be said of AI. Does it really matter as long as you are getting the job done? It wasn’t a big deal when we adopted the calculator over mental math, but this may be different. Only time will tell.</p>
</li>
</ul>
<h2 id="heading-ai-overloadfatigue"><strong>AI Overload/Fatigue</strong></h2>
<p>Phillipp Hartenfeller recently posted the below comment in a LinkedIn thread, which really struck a chord with me.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763906584974/94985089-8969-4a71-ae79-834cb6fe8bf1.png" alt="LinkedIn Post AI Fatigue" class="image--center mx-auto" /></p>
<p>I realized that I, too, am about 20% more efficient with AI than without. This productivity improvement, however, is both a blessing and a curse. I am now able to work on more projects at the same time, which means dealing with more people, more project constraints, more meetings, etc. There is also increased context switching (not just between projects, but also between my coding and checking the code that the AI is generating for me). This leaves aside the time it takes to learn and keep up with the latest AI developments.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🧠</div>
<div data-node-type="callout-text">I don’t yet know the answer to this overload/fatigue, but I have challenged myself to be aware of it and try to mitigate it.</div>
</div>

<h1 id="heading-conclusion">Conclusion</h1>
<p>Much is being said about the hype surrounding AI and the AI bubble. For developers, however, I believe the promise/threat (depending on which way you look at it) of AI is real. I see AI as an adapt or die situation for programmers. Having said that, we are still IT professionals, and it is incumbent on us to use AI professionally.</p>
<p>In a few months, most of this article will be out of date (not least because <strong>APEX 26.1</strong> will likely have been released along with APEXlang).</p>
]]></content:encoded></item><item><title><![CDATA[The Importance of Using Views in APEX]]></title><description><![CDATA[Introduction
With so many emerging technologies inside and outside the Oracle database ecosystem, it might seem trivial, even old-fashioned, to talk about database views in APEX.
A recent project reminded me of the importance of getting the basics ri...]]></description><link>https://blog.cloudnueva.com/importance-of-using-views-in-apex</link><guid isPermaLink="true">https://blog.cloudnueva.com/importance-of-using-views-in-apex</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 20 Nov 2025 12:38:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763642268503/0a045f39-e160-4e21-a426-ae068dbb8719.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>With so many emerging technologies inside and outside the Oracle database ecosystem, it might seem trivial, even old-fashioned, to talk about database views in APEX.</p>
<p>A recent project reminded me of the importance of getting the basics right, so I thought I would share this insight in a brief post.</p>
<h1 id="heading-use-case">Use Case</h1>
<p>Recently, a project reminded me why views are still critical. I was building an inventory count application with three core tables: items, count_sheets, and item_categories. Several pages in the APEX app displayed similar data combinations, initial count, verified count, administrative adjustments, and extended values.</p>
<p>Here’s a simplified version of one of those queries:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> ctgy.category_name
,      ctlg.part_number
,      ctlg.part_description
,      ctlg.uom
,      ctlg.price
,      <span class="hljs-keyword">ROUND</span>(ctlg.price / <span class="hljs-keyword">NULLIF</span>(ctlg.qty_conversion, <span class="hljs-number">0</span>), <span class="hljs-number">2</span>) <span class="hljs-keyword">AS</span> unit_price
,      csh.counter_count                                     <span class="hljs-keyword">AS</span> counter_count
,      csh.reconcile_count                                   <span class="hljs-keyword">AS</span> reconcile_count
,      csh.adjustment_count                                  <span class="hljs-keyword">AS</span> adjustment_count
,      <span class="hljs-keyword">COALESCE</span>(csh.adjustment_count, 
                csh.reconcile_count, 
                csh.counter_count)                           <span class="hljs-keyword">AS</span> final_count
<span class="hljs-keyword">FROM</span>   inv_count_sheet csh
,      inv_catalog     ctlg
,      inv_category    ctgy
<span class="hljs-keyword">WHERE</span>  csh.item_id      = ctlg.item_id
<span class="hljs-keyword">AND</span>    ctlg.category_id = ctgy.category_id;
</code></pre>
<p>There is nothing overly complicated about the SQL. If I use it in the four or five pages, you may not think it is a big deal to replicate the SQL.</p>
<blockquote>
<p>💡 <em>What if the calculation for final_count needs to change?</em></p>
<p>If you’ve repeated this SQL across several APEX pages, even a small change means hunting down and updating each instance, then testing every affected page.</p>
<p>By encapsulating the query in a <strong>database view</strong>, that same change takes seconds to apply and test.</p>
</blockquote>
<p>Of course, this approach is not a panacea. If I needed to add columns or remove columns, I still need to make some code changes. Also, be mindful of performance implications. While views simplify code maintenance, they can sometimes hide expensive joins or aggregations.</p>
<h1 id="heading-other-benefits">Other Benefits</h1>
<p>Re-use and encapsulation of complex logic is the primary benefit of using views, but there are others:</p>
<ul>
<li><p><strong>Elegant APEX Apps</strong> - Instead of embedding complex joins or aggregations in multiple APEX pages, you define them once in a view. This centralized logic makes your APEX pages much simpler and easier to maintain.</p>
</li>
<li><p><strong>Security</strong> - You can limit the scope of views and expose the views instead of the underlying tables to consumers. When combined with <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/SET_TENANT_ID.html">APEX_SESSION.SET_TENANT_ID</a>, and <code>SYS_CONTEXT('APEX$SESSION', 'APP_TENANT_ID')</code>, you can use views to limit access in <a target="_blank" href="https://blog.cloudnueva.com/multi-tenant-apex-apps">Multi-Tenant Applications</a>.</p>
</li>
<li><p><strong>Consistent Data Model for APEX Components</strong> - Interactive grids, reports, and charts all query the same logical layer. This ensures uniform results across the application and reduces data inconsistency issues.</p>
</li>
<li><p><strong>Reusability Across Consumers</strong> - The same view can serve multiple APEX apps, RESTful services, AOP Reports, and Document Generator Reports, promoting modular design and avoiding duplicated SQL definitions.</p>
</li>
</ul>
<h1 id="heading-sql-macros">SQL Macros</h1>
<blockquote>
<p>For even more flexibility, Oracle provides <strong>SQL Macros</strong> (introduced in Oracle Database 19c). SQL Macros let you encapsulate reusable SQL logic, either as scalar or table macros, that expand at parse time. Unlike static views, they can accept parameters. This makes them a powerful complement to traditional views when you need more adaptable query definitions.</p>
</blockquote>
<p>In the table SQL Macro example below, I can pass in the Business Unit ID (p_bu_id) as a paremeter:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> example_sql_macro (p_bu_id <span class="hljs-keyword">IN</span> <span class="hljs-built_in">NUMBER</span>) <span class="hljs-keyword">RETURN</span> <span class="hljs-keyword">CLOB</span> SQL_MACRO <span class="hljs-keyword">AS</span>
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">RETURN</span> q<span class="hljs-string">'{
SELECT ctgy.category_name
,      ctlg.part_number
FROM   inv_count_sheet csh
,      inv_catalog     ctlg
,      inv_category    ctgy
WHERE  csh.item_id      = ctlg.item_id
AND    ctlg.category_id = ctgy.category_id
AND    csh.bu_id        = p_bu_id
  }'</span>;
<span class="hljs-keyword">END</span> example_sql_macro;
</code></pre>
<p>You use table SQL Macros in a SELECT statement just like a regular view:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> *
<span class="hljs-keyword">FROM</span>   example_sql_macro (p_bu_id =&gt; <span class="hljs-number">1</span>);
</code></pre>
<p>Parameters are substituted at parse time, and then the SQL is executed.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This post is a reminder to myself and others that views are an essential part of thoughtful APEX Application Development. Even though there are plenty of shiny new tech tools out there, there is no excuse not to do the basics right.</p>
]]></content:encoded></item><item><title><![CDATA[Color Icons in APEX Tree Regions]]></title><description><![CDATA[Introduction
This is a quick post to show you how to display different-colored icons in the nodes of an APEX Tree Region. This is one of those things that should be easier than it is!
Goal
I am building an App to maintain corporate hierarchies. These...]]></description><link>https://blog.cloudnueva.com/color-icons-in-apex-tree-regions</link><guid isPermaLink="true">https://blog.cloudnueva.com/color-icons-in-apex-tree-regions</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 06 Nov 2025 13:57:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762126954918/14a497d4-7e70-4f7d-8c15-5020b09ecd43.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>This is a quick post to show you how to display different-colored icons in the nodes of an APEX Tree Region. This is one of those things that should be easier than it is!</p>
<h1 id="heading-goal">Goal</h1>
<p>I am building an App to maintain corporate hierarchies. These could be cost center hierarchies, job hierarchies, etc. A key piece of functionality is the ability to upload changes to the hierarchies via Excel and provide a Diff between the original hierarchy and the newly uploaded hierarchy. The diff should show:</p>
<ul>
<li><p>Added Nodes (node added to the hierarchy)</p>
</li>
<li><p>Removed Nodes (node removed from the hierarchy)</p>
</li>
<li><p>Moved Nodes (node moved from one parent to another)</p>
</li>
</ul>
<p>The UI needs to show changes in the hierarchy and clearly distinguish between the above changes.</p>
<h1 id="heading-the-solution-part-1">The Solution Part 1</h1>
<p>Clearly, an APEX Tree region would be a great choice to illustrate changes to the hierarchy. Using the Oracle CONNECT BY clause, we can construct a hierarchical query that compares the hierarchy in the main table to the hierarchy being uploaded. We will skip the diff logic, but you can see the <a target="_blank" href="https://gist.github.com/jon-dixon/be35be00523935e8c9adec5607491bf3">SQL here</a> if you are interested.</p>
<p>In APEX, we set up a Tree Region and reference the columns in the outer SQL:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762124926847/c51d0913-dd5d-4ebe-ad1f-9f873d9a77d3.png" alt="APEX Tree Region Settings" class="image--center mx-auto" /></p>
<p>We end up with something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762124570181/b3b19b5c-d0e9-43b4-9fa0-07045dfc5e34.png" alt="Oracle APEX Tree Region No Color" class="image--center mx-auto" /></p>
<p>This looks great, but for larger hierarchies, it will be challenging for users to spot changes to the newly uploaded hierarchy.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Now to the point of this blog post, how to change the colors of the icons in the tree?</div>
</div>

<p>Ideally, we could do something like this in the CASE statement, which determines the icon:</p>
<pre><code class="lang-sql">  ,      CASE status
           WHEN 'Added'   THEN 'fa-plus-circle u-success-text'
           WHEN 'Removed' THEN 'fa-times-circle u-danger-text'
           WHEN 'Moved'   THEN 'fa-arrows-alt u-info-text'
           WHEN 'Changed' THEN 'fa-exchange u-info-text'
           ELSE 'fa-circle-o'
         <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> icon_css
</code></pre>
<ul>
<li>The <code>icon_css</code> column above is referenced in the APEX Tree Region Attribute called ‘Icon CSS Class Column’. Navigation: Tree → Attributes → Appearance → Icon CSS Class Column.</li>
</ul>
<p>Unfortunately, this does not work. If we inspect the HTML and CSS in the page, we can see why:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762125533022/65471fd3-109c-4541-bf97-a569a50e4775.png" alt="APEX Tree Region CSS" class="image--center mx-auto" /></p>
<ul>
<li>Even though we see the u-success-text class that we added, APEX is using a CSS variable <code>--a-treeview-node-icon-color</code> to determine the icon color. We could, of course, override that color, but the override would apply to all nodes, and we are back where we started.</li>
</ul>
<h1 id="heading-the-solution-part-2">The Solution Part 2</h1>
<p>Ideally, APEX would allow you to pass your own color class in the SQL like this:</p>
<pre><code class="lang-sql">  ,      CASE status
           WHEN 'Added'   THEN 'fa-plus-circle icon-green'
           WHEN 'Removed' THEN 'fa-times-circle icon-red'
           WHEN 'Moved'   THEN 'fa-arrows-alt icon-blue'
           WHEN 'Changed' THEN 'fa-exchange icon-blue'
           ELSE 'fa-circle-o icon-grey'
         <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> icon_css
</code></pre>
<p>On the page level, Inline CSS, we would style the above color class.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762125853294/84330afc-9d60-4cf2-ba92-4315e734080c.png" alt="Page Level CSS" class="image--center mx-auto" /></p>
<p>Unfortunately, this does not work either, as the APEX class overrides ours.</p>
<h1 id="heading-the-solution-part-3">The Solution - Part 3</h1>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😠</div>
<div data-node-type="callout-text">OK, enough about what won’t work… What will work?</div>
</div>

<p>We can work with APEX and set the <code>--a-treeview-node-icon-color</code> CSS variables based on our own custom CSS classes. Add the following to the page level Inline CSS attribute. <em>Page → CSS → Inline</em> (or <em>Theme → Custom CSS</em> if you want it app-wide).</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.fa</span><span class="hljs-selector-class">.icon-green</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.a-Icon</span><span class="hljs-selector-class">.icon-green</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.t-Icon</span><span class="hljs-selector-class">.icon-green</span> { <span class="hljs-attribute">--a-treeview-node-icon-color</span>: <span class="hljs-built_in">var</span>(--ut-palette-success); }

<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.fa</span><span class="hljs-selector-class">.icon-red</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.a-Icon</span><span class="hljs-selector-class">.icon-red</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.t-Icon</span><span class="hljs-selector-class">.icon-red</span> { <span class="hljs-attribute">--a-treeview-node-icon-color</span>: <span class="hljs-built_in">var</span>(--ut-palette-danger); }

<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.fa</span><span class="hljs-selector-class">.icon-blue</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.a-Icon</span><span class="hljs-selector-class">.icon-blue</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.t-Icon</span><span class="hljs-selector-class">.icon-blue</span> { <span class="hljs-attribute">--a-treeview-node-icon-color</span>: <span class="hljs-built_in">var</span>(--ut-palette-info); }

<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.fa</span><span class="hljs-selector-class">.icon-grey</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.a-Icon</span><span class="hljs-selector-class">.icon-grey</span>,
<span class="hljs-selector-class">.a-TreeView</span> <span class="hljs-selector-class">.t-Icon</span><span class="hljs-selector-class">.icon-grey</span> { <span class="hljs-attribute">--a-treeview-node-icon-color</span>: <span class="hljs-built_in">var</span>(--u-color-<span class="hljs-number">29</span>);}
</code></pre>
<p>Add the custom CSS classes icon-grey, icon-blue, icon-green, and icon-red to our SQL, along with the icon classes.</p>
<pre><code class="lang-sql">,      CASE status
           WHEN 'Added'   THEN 'fa-plus-circle icon-green'
           WHEN 'Removed' THEN 'fa-times-circle icon-red'
           WHEN 'Moved'   THEN 'fa-arrows-alt icon-blue'
           WHEN 'Changed' THEN 'fa-exchange icon-blue'
           ELSE 'fa-circle-o icon-grey'
         <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> icon_css
</code></pre>
<p>If we take a look at the HTML and CSS generated in the page now, we can see the UT <code>u-success-green</code> color class has been used in the <code>--a-treeview-node-icon-color</code> CSS variable for the specific node.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762126335824/57a0b39f-c21e-413f-bc6a-42b94e7ffbc7.png" alt class="image--center mx-auto" /></p>
<p>The result is colorized node icons, which make it easier to see what is being added, removed, or moved:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762126367536/33a681f3-f6f9-4550-98e3-453e613a2247.png" alt="APEX Tree Region with colored Node Icons" class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>I could have written this post with just the solution, but I thought it would be useful to see my thought process in solving this problem.</p>
<h2 id="heading-call-to-action">📣 Call to Action</h2>
<p>I have created an Idea in the <a target="_blank" href="https://apex.oracle.com/ideas/FR-4704">APEX Ideas App</a>, suggesting it would be easier to pass a color class along with the icon class. Unfortunately, the idea has been closed 😞</p>
]]></content:encoded></item><item><title><![CDATA[Oracle SQLcl MCP Server with Codex & Copilot #JoelKallmanDay]]></title><description><![CDATA[Introduction
I have been using GitHub Copilot with Oracle’s SQLcl MCP server since its release in July 2025. The combination of Generative AI and databases is a powerful pairing that can help APEX developers build better products in less time. This s...]]></description><link>https://blog.cloudnueva.com/oracle-sqlcl-mcp-server-with-codex-and-copilot-joelkallmanday</link><guid isPermaLink="true">https://blog.cloudnueva.com/oracle-sqlcl-mcp-server-with-codex-and-copilot-joelkallmanday</guid><category><![CDATA[sqlcl]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Wed, 15 Oct 2025 13:00:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760241018027/ac7c8104-e076-456c-82ce-51c2756fe21f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>I have been using GitHub Copilot with Oracle’s SQLcl MCP server since its release in July 2025. The combination of Generative AI and databases is a powerful pairing that can help APEX developers build better products in less time. This statement comes with some caveats, which I will cover in this post.</p>
<p>In this post, I will describe how to set up the SQLcl MCP server in SQL Developer for VS Code, utilizing both <a target="_blank" href="https://openai.com/codex/">OpenAI’s Codex</a> and <a target="_blank" href="https://github.com/features/copilot">GitHub Copilot</a>. I will also review several use cases and provide example prompts, which will help you get more out of this technology.</p>
<h1 id="heading-configuring-the-sqlcl-mcp-server">Configuring the SQLcl MCP Server</h1>
<p>First, you will need to install the latest version of the SQL Developer Extension for VS Code and the latest version of SQLcl on your machine. You will also need to know the path to your SQLcl install.</p>
<p>In the following sections, I will describe how to set up Codex and GitHub Copilot to utilize the SQLcl MCP Server. Which tool to use is up to you. I currently use both, but am leaning toward Codex as my go-to development assistant.</p>
<h2 id="heading-configuring-github-copilot">Configuring GitHub Copilot</h2>
<p>See the ‘Appendix 1 - Configure &amp; Test GitHub Copilot with SQLcl MCP Server’ for details on how to set up and test the SQLcl MCP Server with VS Code.</p>
<h2 id="heading-configuring-openai-codex">Configuring OpenAI Codex</h2>
<h3 id="heading-install-the-openai-codex-extension-for-vs-code">Install the OpenAI Codex Extension for VS Code</h3>
<ul>
<li>Install the Codex Extension for VS Code.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760206643572/5f06c8b5-e441-43c9-a5e7-82679ff58d7c.png" alt="Codex Extension for VS Code" class="image--center mx-auto" /></p>
<ul>
<li><p>Log in to your OpenAI / ChatGPT account</p>
<ul>
<li>Open the Codex extension &gt; Click the settings gear icon &gt; Log in</li>
</ul>
</li>
</ul>
<h2 id="heading-configure-codex-to-use-the-sqlcl-mcp-server">Configure Codex to use the SQLcl MCP Server</h2>
<ul>
<li>Open the Codex extension &gt; Click ⚙️ &gt; MCP Settings &gt; Open config.toml</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760206131975/c1656423-e2cf-42e6-8102-46b543ff6e2a.png" alt="Codes Settings" class="image--center mx-auto" /></p>
<ul>
<li>Once the file opens, copy and paste the below text into it (adjust your SQLcl path accordingly).</li>
</ul>
<pre><code class="lang-ini"><span class="hljs-section">[mcp_servers.sqlcl]</span>
<span class="hljs-attr">command</span> = <span class="hljs-string">"/opt/homebrew/Caskroom/sqlcl/25.3.0.274.1210/sqlcl/bin/sql"</span>
<span class="hljs-attr">args</span> = [<span class="hljs-string">"-mcp"</span>]
<span class="hljs-attr">startup_timeout_ms</span> = <span class="hljs-number">60000</span>
</code></pre>
<h2 id="heading-testing-the-setup">Testing the Setup</h2>
<p>For the rest of this post, I will be using a saved connection called ‘DEMO’ in SQLDeveloper for VS Code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760208293698/88509c97-81e4-42ad-88d1-65b24d7e7e09.png" alt="DEMO SQL Developer Connection" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">SQLcl and the SQL Developer Extension for VS Code share the same DB connections. This is how SQLcl connects to your database in the examples below.</div>
</div>

<h3 id="heading-test-from-codex-with-vs-code">Test from Codex with VS Code</h3>
<p>Open the Codex Extension and type the following in the chat Window:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Connect to DEMO using the SQLcl MCP Server</div>
</div>

<p>Once connected, ask:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">How many tables are these in the schema</div>
</div>

<p>You should end up with something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760208546997/520c055b-abed-4fd3-a3cc-556589054818.png" alt="Codex Connected to the DB" class="image--center mx-auto" /></p>
<h3 id="heading-test-from-the-codex-cli">Test from the Codex CLI</h3>
<p>The SQLcl MCP Server also works with the <a target="_blank" href="https://developers.openai.com/codex/cli/">OpenAI Codex CLI.</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760314788470/2af6882e-097e-416a-a5ce-0fb3832f1fc7.png" alt="Codex CLI and SQLcl MCP Server 1" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760314805992/f8b38895-0ae9-4b73-9139-a30843fdd548.png" alt="Codex CLI and SQLcl MCP Server 1" class="image--center mx-auto" /></p>
<h1 id="heading-how-it-works">How it Works</h1>
<p>Before reviewing the examples, it’s important to understand how the SQLcl MCP Server interacts with the Large Language Model (LLM).</p>
<p>When you type a prompt in Copilot Chat or Codex, VS Code sends that prompt, along with prior context and a list of available tools, to the LLM. One of those tools can be the SQLcl MCP Server.</p>
<p>The LLM analyzes the prompt and determines whether SQLcl is the appropriate tool to assist in fulfilling the request. If it is, the LLM instructs VS Code to run a command through SQLcl and return the output. The LLM then uses that output to decide the next step, continuing this exchange until the request is resolved.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In short, <strong>the LLM is the driver; SQLcl is just an assistant.</strong></div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">The reason I call this out is that if the LLM decides that dropping all of your tables will answer the question and that SQLcl is the right tool for the job, then SQLcl will happily drop all the tables!</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760212037485/ffe00adf-a5f9-493f-99ff-e6247f0d44f1.png" alt="It will drop tables!" class="image--center mx-auto" /></p>
<h1 id="heading-agentsmd">AGENTS.md</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760210292198/9b39df34-09cd-4e0f-9206-3c22b5a8d22f.png" alt="AGENTS.md" class="image--center mx-auto" /></p>
<p>Adding a file called <a target="_blank" href="https://agents.md/">AGENTS.md</a> to the root of your GitHub repositories allows you to pass on guidelines to the LLM. Both Codex and <a target="_blank" href="https://github.blog/changelog/2025-08-28-copilot-coding-agent-now-supports-agents-md-custom-instructions/">GitHub Copilot</a> recognize this file. You can include details such as:</p>
<ul>
<li><p>APEX and DB Versions</p>
</li>
<li><p>Folder Structures of your Repo</p>
</li>
<li><p>Coding Standards and Conventions</p>
</li>
<li><p>Formatting Rules</p>
</li>
<li><p>Agent behaviors and guidelines (don’t drop my tables)</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I created the initial version of my AGENTS.md by asking ChatGPT to create it for me.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">⚠️ <strong>Important:</strong> Do not rely on <a target="_self" href="http://AGENTS.md">AGENTS.md</a> to prevent destructive actions (DDL or DML). It only provides guidance — not enforcement.</div>
</div>

<h1 id="heading-sqlcl-mcp-use-cases">SQLcl MCP Use Cases</h1>
<p>Now that we are set up, it’s time to go through some use cases. I think this is the best way to understand why the SQLcl MCP server is so helpful.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">From now on, I will be using Codex for the demos. I will also assume that we are already connected to the DEMO database connection.</div>
</div>

<h2 id="heading-create-a-query">Create a Query</h2>
<p>Let’s start by having the MCP Server create a query for us to use.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Create a SQL query to get sales orders, customers, sales order lines, and products</div>
</div>

<p>Result:</p>
<hr />
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> so.order_number
     , so.order_date
     , so.total_amount         <span class="hljs-keyword">AS</span> order_total
     , so.status               <span class="hljs-keyword">AS</span> order_status
     , c.name                  <span class="hljs-keyword">AS</span> customer_name
     , c.email                 <span class="hljs-keyword">AS</span> customer_email
     , sol.line_number
     , sol.quantity
     , sol.unit_price
     , sol.line_total
     , p.sku
     , p.name                  <span class="hljs-keyword">AS</span> product_name
<span class="hljs-keyword">FROM</span>   sales_orders       so
     , customers          c
     , sales_order_lines  sol
     , products           p
<span class="hljs-keyword">WHERE</span>  so.customer_id    = c.customer_id
<span class="hljs-keyword">AND</span>    sol.order_id      = so.order_id
<span class="hljs-keyword">AND</span>    sol.product_id    = p.product_id;
</code></pre>
<hr />
<ul>
<li><p>The first thing you may notice is that the SQL is formatted. This is because of the formatting instructions in the AGENTS.md file. It also uses the old Oracle SQL syntax, again, because this is in my AGENTS.md (and because I am old 👴).</p>
</li>
<li><p>The LLM requested details of the tables in the database, SQLcl provided them, and the LLM generated the query. It correctly identified the tables based on my prompt, even though I did not use exact table names.</p>
</li>
</ul>
<h2 id="heading-tune-the-query">Tune the Query</h2>
<p>Now that we have a query, let’s tune it for optimal performance when we want to view orders with an order date within the past month.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Tune the SQL statement below. I want to improve performance when querying orders with an order date in the past month. Run the SQL query against the database, check the explain plan, and iterate through options until you find the best solution. SQL: SELECT so.order_number , so.order_date , <a target="_self" href="http://so.total">so.total</a>_amount AS order_total , so.status AS order_status , <a target="_self" href="http://c.name">c.name</a> AS customer_name , <a target="_self" href="http://c.email">c.email</a> AS customer_email , sol.line_number , sol.quantity , sol.unit_price , sol.line_total , p.sku , <a target="_self" href="http://p.name">p.name</a> AS product_name FROM sales_orders so , customers c , sales_order_lines sol , products p WHERE so.customer_id = c.customer_id AND sol.order_id = so.order_id AND sol.product_id = p.product_id AND so.order_date &gt;= ADD_MONTHS(TRUNC(SYSDATE),-1)</div>
</div>

<p>Here is an excerpt from the LLM’s “model reasoning trace” and its back and forth with SQLcl:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760211490603/8c2deb8f-526d-475f-9833-10c2e62b3c84.png" alt="LLM Chain of Thought" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As you can see, the LLM and SQLcl went through many iterations.</div>
</div>

<p><strong>Here is the final response from the LLM:</strong></p>
<hr />
<blockquote>
<p><strong>Tuning Results</strong></p>
<ul>
<li><p>Captured the original plan: full scans on SALES_ORDERS and SALES_ORDER_LINES, cost 12 (plan hash 1363380163), so the date filter wasn’t selective without supporting indexes.</p>
</li>
<li><p>Added index sales_orders_ord_dt_idx on (order_date, order_id) to allow the optimizer to apply the last-month filter via an index range scan.</p>
</li>
<li><p><strong><mark>Added index</mark></strong> sales_order_lines_ord_idx on (order_id, product_id) so line lookups now use range scans instead of scanning the entire detail table.</p>
</li>
<li><p><strong><mark>Regathered stats</mark></strong> on both tables (DBMS_STATS.GATHER_TABLE_STATS) to make the new access paths visible to the optimizer.</p>
</li>
<li><p>New plan hash 1931885458: both SALES_ORDERS and SALES_ORDER_LINES now use index range scans with batched rowid lookups; plan cost dropped to 9, and the query no longer performs storage full scans.</p>
</li>
<li><p>Executed the tuned SQL (ANSI JOIN form) and confirmed it returns no rows for the past month in this sample dataset, indicating the plan runs without error.</p>
</li>
</ul>
<p><strong>Next Steps</strong></p>
<ol>
<li><p>Validate the change against production-scale volumes (SQL Monitor/ASH) to quantify real runtime gains.</p>
</li>
<li><p>Keep table stats current after large data loads so the optimizer continues to choose the new indexes.</p>
</li>
</ol>
</blockquote>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As you can see, it went ahead and created a new index and gathered stats. This is great, and it did reduce the cost of the SQL from 14 to 9, but I did not ask it to do anything that changed the database. More proof (if any was needed) that you should not be running this against PROD.</div>
</div>

<h2 id="heading-combining-context-from-your-db-and-your-codebase">Combining Context from Your DB and Your Codebase</h2>
<p>Sometimes it is helpful to ask questions about your codebase in GitHub and what is actually in your database.</p>
<p>I have a folder open in VS Code with a GitHub Repo:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760212336403/14975e50-d061-4e48-b7b6-74c72d3c8623.png" alt="Folder Open with Github Repo" class="image--center mx-auto" /></p>
<p>Now, I can ask questions that span the DB and my file system.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Which tables in the database are referenced in the codebase? Use the SQLcl MCP server to inspect the DB.</div>
</div>

<p>Here is an excerpt from the “model reasoning trace”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760212653146/9d8ca341-ea8d-417a-bb72-7ff4cc18442c.png" alt="DB and File System Chain of Thought" class="image--center mx-auto" /></p>
<p><strong>Here is the Response:</strong></p>
<hr />
<blockquote>
<p>Connected to the DEMO database via SQLcl and cross-referenced every SQL/PL/SQL source in the repo. The query select distinct table_name from all_tables where table_name in (...) returned twelve matches, and each has live references in the codebase:</p>
<p><strong>GenAI Schema Tables</strong></p>
<ul>
<li><p>GEN_AI_CONFIGS – defined in cn_utils/ai/TABLES/GEN_AI_CONFIGS_CRTB.sql:1, used in cn_utils/ai/PLSQL/GEN_AI_UTL_PKB.sql:18.</p>
</li>
<li><p>GEN_AI_LOGS – defined in cn_utils/ai/TABLES/GEN_AI_LOGS_CRTB.sql:2, logged through cn_utils/ai/PLSQL/GEN_AI_UTL_PKB.sql:8.</p>
</li>
<li><p>GEN_AI_TOOLS – created in cn_utils/ai/TABLES/GEN_AI_TOOLS_CRTB.sql:1, referenced by cn_utils/ai/PLSQL/GEN_AI_TOOLS_UTL_PKB.sql:1.</p>
</li>
<li><p>GEN_AI_TOOL_PARAMS – documented in cn_utils/ai/TABLES/GEN_AI_TOOL_PARAMS_CRTB.sql:1, queried in cn_utils/ai/PLSQL/GEN_AI_TOOLS_UTL_PKB.sql:66.</p>
</li>
<li><p>SALES_ORDERS – DDL/comments in cn_utils/ai/SELECTAI/SELECT_AI_DEMO.sql:19, accessed in cn_utils/ai/PLSQL/GEN_AI_EXAMPLE_TOOLS_PKB.sql:26.</p>
</li>
<li><p>CUSTOMERS – table metadata in cn_utils/ai/SELECTAI/SELECT_AI_DEMO.sql:28, queried in conference_material/KSCOPE25/Built with LOV Using APEX/f777.sql:7176.</p>
</li>
</ul>
<p><strong>Content &amp; Reference Tables</strong></p>
<ul>
<li><p>CNBA_BLOG_POSTS – synchronized in cn_utils/ai_demo_app/APEX/f110.sql:334, selected in cn_utils/ai_demo_app/PLSQL/CN_AI_UTL_PKB.sql:36.</p>
</li>
<li><p>CNBA_POST_CHUNKS – inserted in cn_utils/ai_demo_app/APEX/f110.sql:2765, joined in cn_utils/ai_demo_app/PLSQL/CN_AI_UTL_PKB.sql:88.</p>
</li>
<li><p>CN_DOCUMENTS – queried in cn_utils/ai_demo_app/APEX/f110.sql:1580, retrieved in cn_utils/ai_demo_app/PLSQL/CN_AI_UTL_PKB.sql:176.</p>
</li>
<li><p>CN_DOCUMENT_CHUNKS – read in cn_utils/ai_demo_app/APEX/f110.sql:3860, maintained in cn_utils/ai_demo_app/PLSQL/CN_AI_UTL_PKB.sql:252.</p>
</li>
<li><p>PRODUCTS – LOV definition in cn_utils/ai_demo_app/APEX/f110.sql:1572, included in cn_utils/ai/SELECTAI/SELECT_AI_DEMO.sql:10.</p>
</li>
<li><p>COUNTRIES – created in conference_material/KSCOPE25/Built with LOV Using APEX/countries.sql:2, queried in conference_material/KSCOPE25/Built with LOV Using APEX/f777.sql:2462.</p>
</li>
</ul>
<p>Other names appeared in the source scan but are not tables in the DEMO schema (e.g., views like CNDEMO_ADDRESSES_V or package calls such as TABLE_FN_PK.EVS_SOLD_BY_YEAR). Let me know if you want to repeat the check against another connection or extend it to views/synonyms.</p>
</blockquote>
<hr />
<h2 id="heading-full-agent-mode">Full Agent Mode</h2>
<p>Q&amp;A questions are great, but can we create code in the DB? Let’s try!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Create a TAPI package for the customers, orders, order_lines, and products tables in the DEMO db. Use coding standards laid out in the AGENT.md file. Use the prefix AIDEMO for the package name. Create the package directly in the DEMO DB using the sqlcl mcp server.</div>
</div>

<p><strong>Notice</strong> the table names are not quite right. I have orders instead of sales_orders and order_lines instead of sales_order_lines.</p>
<p>Here is an excerpt from the “model reasoning trace”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760213083700/3832a37c-5c71-41bd-85f2-b9d602f6f453.png" alt="Chain of thought from Create Code Example." class="image--center mx-auto" /></p>
<p>Here is the response:</p>
<hr />
<blockquote>
<p>Created table APIs for customers, orders, order lines, and products in DEMO so each table now has consistent insert/update/delete/fetch routines with APEX debug logging and error propagation.</p>
<ul>
<li><p>AIDEMO_CUSTOMERS_PK uses customers%ROWTYPE for CRUD, returning generated IDs and logging apex_debug.error on failure.</p>
</li>
<li><p>AIDEMO_PRODUCTS_PK mirrors the same pattern for PRODUCTS, defaulting created_at to SYSDATE when absent.</p>
</li>
<li><p>AIDEMO_ORDERS_PK targets SALES_ORDERS (no plain ORDERS object exists) and fills defaults for order_date, total_amount, and status during inserts.</p>
</li>
<li><p>AIDEMO_ORDER_LINES_PK targets SALES_ORDER_LINES, recalculating line_total when not provided.</p>
</li>
<li><p>Verified all specs/bodies compile cleanly (USER_OBJECTS shows each package and body VALID).</p>
</li>
</ul>
<p>Next steps:</p>
<ol>
<li><p>Exercise the new TAPIs from your APEX flows or unit harness to confirm business logic/defaults behave as expected.</p>
</li>
<li><p>If you need deployment artifacts, export these CREATE OR REPLACE statements into your /SCRIPTS folder for source control.</p>
</li>
</ol>
</blockquote>
<hr />
<p>It figured out the correct tables and created a create/update/delete package for each table:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760213252702/bcd3ba72-35f7-497c-bf6d-d05565e142db.png" alt="Code Created" class="image--center mx-auto" /></p>
<p>A few notes:</p>
<ul>
<li>It included the <code>apex_debug</code> call in the WHEN OTHERS exception, which I had specified in my AGENTS.md</li>
</ul>
<h1 id="heading-what-about-apex">What about APEX?</h1>
<p>Because APEX lives in the database, it is an obvious candidate for interrogation by LLMs.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Look at the Oracle APEX applications in the WKSP_WKSPDEMO schema and list the top 5 security concerns.</div>
</div>

<p>Example response (not my apps 😊):</p>
<hr />
<blockquote>
<ul>
<li><p>Application 103 has 45 pages where PAGE_ACCESS_PROTECTION = 'Unrestricted' (apex_application_pages), leaving session state open to tampering; switch these pages to checksum-protected or require re-authentication. 🫢</p>
</li>
<li><p>Browser caching remains enabled for application 103 (BROWSER_CACHE = 'Enabled'), risking sensitive data leakage on shared machines; disable caching for authenticated apps.</p>
</li>
</ul>
</blockquote>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">➡</div>
<div data-node-type="callout-text">Which APEX Applications are not running the latest version of APEX and are not running the latest version of the universal theme?</div>
</div>

<p>Example response (again, not my apps 😊):</p>
<hr />
<blockquote>
<ul>
<li><p>apex_release.version_no reports 24.2.9 as the current APEX version; four applications (100, 101, 102, 110) already use compatibility_mode = '24.2' and Universal Theme 24.2 (apex_application_themes).</p>
</li>
<li><p>Application 103 is still on compatibility_mode = '4.2' 🫢 (apex_applications), so its runtime features are constrained to an obsolete APEX release.</p>
</li>
<li><p>The same application 103 runs Universal Theme version 1.2 with file_prefix = '#IMAGE_PREFIX#themes/theme_42/1.2/' (apex_application_themes), which predates the current 24.2 delivery.</p>
</li>
</ul>
</blockquote>
<hr />
<h1 id="heading-other-prompts">Other Prompts</h1>
<ul>
<li><p>Identify tables in the DEMO schema that are missing foreign keys. Add each missing foreign key to a script called MISSING_FOREIGN_KEYS.sql for my review.</p>
<ul>
<li>Not only did the LLM do a good job of identifying missing foreign keys and creating the respective ALTER TABLE scripts, but it also identified orphaned records in one table!</li>
</ul>
</li>
<li><p>Review database objects in the DEMO schema and files in my codebase to identify unused tables. Create a script called POTENTIAL_TABLE_DROPS.sql with DROP statements for each. Do not execute the drop statements.</p>
</li>
<li><p>Look at SQL queries that have run in the DEMO schema and list the top three poorly performing SQL statements. You may need to run this as a privileged user.</p>
</li>
<li><p>You are an expert technical author with specialist knowledge in Oracle APEX, Oracle Database, PL/SQL, and SQL. You have been assigned to the ‘XYZ project’. The goal of the project is to migrate from a legacy APEX version to the latest version (24.2) while enhancing security and adding the required business functionality. Connect to the 'XYZ' connection using the sqlcl mcp server. Review all of the database objects and APEX applications in the ‘XYX’ schema. Your goal is to create high-quality, easy-to-read, and concise technical documentation for the support team that will take over support for the application. Create a document called TECHICAL_DESIGN.md in the DOCS folder.</p>
</li>
</ul>
<h1 id="heading-other-considerations">Other Considerations</h1>
<ul>
<li><p>Ensure the LLM and SQLcl are connected to the correct instance and schema. This is especially important if, like me, you have numerous saved database connections in SQL Developer. Because the LLM is deciding what command to run in SQLcl, it can easily pick the wrong connection. It helps to have clear connection names that differ between clients and instances, e.g., ABCCORP-DEV and CLOUDNUEVA-DEV, as opposed to DEV1 and DEV2.</p>
</li>
<li><p>I recommend creating a read-only schema in non-development instances.</p>
</li>
<li><p>For complex questions, the number of iterations between the LLM and SQLcl can be large. Some questions can take multiple minutes to answer and require a significant number of tokens. 💲</p>
</li>
<li><p>I find it interesting to review both the plan that the LLM generates at the start of the process and the “model reasoning trace” it emits as it goes through the process of answering your question. This helps me to build better prompts.</p>
</li>
<li><p>As models evolve (GPT3 &gt; GPT4 &gt; GPT5), the prompts you enter today may not yield the same results tomorrow. As with any AI technology, it is important to build a list of <a target="_blank" href="https://blog.cloudnueva.com/why-evals-are-important-in-ai-development">Evals</a> that you can use to test new models and prompts against results from previous iterations.</p>
</li>
<li><p>If you do not already, add rich and informative table and column comments. I laid out why this is important in my <a target="_blank" href="https://blog.cloudnueva.com/select-ai-is-not-a-toy#heading-table-amp-column-comments">post about SELECT AI</a>.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In the end, the SQLcl MCP server isn’t magic 🪄; it’s just a bridge between your database and an LLM. It saves time, reduces context switching, and helps you think through SQL and APEX problems more efficiently. It won’t replace your judgment or stop you from making a bad call, but it can make routine work faster and more consistent.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Like any tool, it’s only as good as the care and thought that you put behind it.</div>
</div>

<h1 id="heading-appendix-1-configure-amp-test-github-copilot-with-sqlcl-mcp-server">Appendix 1 - Configure &amp; Test GitHub Copilot with SQLcl MCP Server</h1>
<p>Here is a guide to <a target="_blank" href="https://code.visualstudio.com/docs/copilot/getting-started">get started with GitHub Copilot in VS Code</a>.</p>
<h2 id="heading-install-github-copilot-extensions-for-vs-code">Install GitHub Copilot Extensions for VS Code</h2>
<ul>
<li>Install the GitHub Copilot and GitHub Copilot Chat Extensions</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760206602839/4fb22159-614b-4837-88a5-63811b65a24b.png" alt="GitHub Copilot and GitHub Copilot Chat Extensions" class="image--center mx-auto" /></p>
<ul>
<li>Log in using your GitHub account</li>
</ul>
<h2 id="heading-configure-copilot-to-use-the-sqlcl-mcp-server">Configure Copilot to use the SQLcl MCP Server</h2>
<ul>
<li><p>Open the <a target="_blank" href="https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette">VS Code Command Palette</a>.</p>
</li>
<li><p>Type MCP to see the MCP Options</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207088126/845efb6d-4e98-4f26-b857-5582a0274bac.png" alt="Copilot MCP Options" class="image--center mx-auto" /></p>
<ul>
<li>Select MCP: Add Server</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207168597/0f69819d-0633-4d1e-aaed-4a1d494cd7c9.png" alt="Copilot Add MCP Server" class="image--center mx-auto" /></p>
<ul>
<li>Select Command (stdio)</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207228168/02fcb70c-2283-4ae7-8e46-ae90c34416fc.png" alt="Copilot Enter Command" class="image--center mx-auto" /></p>
<ul>
<li>Enter a name for the MCP Server</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207254783/185c0b38-a441-4018-ab4b-70508fb6810b.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Select which scope you want it to run in:</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207290592/ac9b697a-6dfa-426e-b9c0-1838dd813f41.png" alt="Copilot MCP Server Scope" class="image--center mx-auto" /></p>
<ul>
<li>You should now see the mcp.json file</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207331142/dceb6b93-92ab-4f62-89ca-a57324db11dd.png" alt="Copilot mcp.json" class="image--center mx-auto" /></p>
<ul>
<li>Now that the SQLcl MCP Server is installed, you can start/stop/restart it by opening the menu and selecting &gt; MCP: List Servers</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207605605/84496a0a-f068-4680-82a0-8e153f2188d3.png" alt="Start MCP Server 1" class="image--center mx-auto" /></p>
<ul>
<li>Then select SQLcl</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207725912/2a5b6a0c-9a2f-4230-bb4e-c671eb2febe0.png" alt="Start MCP Server 2" class="image--center mx-auto" /></p>
<ul>
<li>Select ‘Start Server’ to start the MCP Server. The same navigation will allow you to stop an already running MCP Server.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207744614/1d8af4ce-e725-4669-82b9-51ff9c504a1d.png" alt class="image--center mx-auto" /></p>
<ul>
<li>Click ‘Show Output’ to see a log of what it is doing.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760207822059/aba2e6eb-70dd-4fad-a9d4-e5a2fbd9b318.png" alt="Start MCP Server 5" class="image--center mx-auto" /></p>
<p><strong>Note</strong>: VS Code will start the SQLcl MCP Server for you if it is not already started.</p>
<h2 id="heading-test-from-vs-code">Test from VS Code</h2>
<p>Open the CoPilot Chat Extension and type the following in the chat Window:</p>
<p>Connect to DEMO using the SQLcl MCP Server</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760208892836/8fe34b15-dfa7-45df-97fc-e4363561aff6.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🤨</div>
<div data-node-type="callout-text">Oops. What went wrong? Copilot did not route the prompt to the SQLcl MCP server, likely due to a VS Code workspace context or MCP scope issue. I have found that the VS Code extension becomes confused (regarding the SQLcl MCP Server) when a Workspace or any open files are in VS Code. If I close all folders and files and try again…</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760209177914/c5afcabf-49ff-4a95-8035-a6cafe3c6550.png" alt="SQLcl MCP Server from Copilot" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">While it’s great that I am now connected, not being able to ask questions about my database and codebase simultaneously is not ideal. This is the main reason that I now use Codex instead of CoPilot when using the SQLcl MCP Server. You may also have noticed that Copilot is a lot more ‘chatty’, which I don’t like.</div>
</div>]]></content:encoded></item><item><title><![CDATA[Handle Non-Standard API Authentication in APEX]]></title><description><![CDATA[Introduction
Oracle APEX Web Credentials supports many standard REST API authentication schemes. However, not all APIs play nicely - some use custom or unsupported authentication flows.
When that happens, securing and reusing credentials becomes hard...]]></description><link>https://blog.cloudnueva.com/handle-non-standard-api-authentication-in-apex</link><guid isPermaLink="true">https://blog.cloudnueva.com/handle-non-standard-api-authentication-in-apex</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 02 Oct 2025 12:27:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746986710472/c8bb3a56-a7dc-49bb-86ca-b1cbd83a82ad.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p><a target="_blank" href="https://blog.cloudnueva.com/apex-web-credentials">Oracle APEX Web Credentials</a> supports many standard REST API authentication schemes. However, not all APIs play nicely - some use custom or unsupported authentication flows.</p>
<p>When that happens, securing and reusing credentials becomes harder. You lose the built-in power of APEX Web Credentials and the ability to use features like REST Data Sources.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This post will focus on these authentication outliers. I will show you how, in many cases, we can still use APEX Web Credentials, even if APEX does not support the authentication type of the API you are trying to call.</div>
</div>

<h2 id="heading-goals">Goals</h2>
<ul>
<li><p>Built a reusable and secure authentication solution for non-standard APIs</p>
</li>
<li><p>Leveraged Oracle TDE and Data Redaction for at-rest and at-access credential protection</p>
</li>
<li><p>Enabled native APEX Web Credential reuse through persistent token injection</p>
</li>
</ul>
<h1 id="heading-use-case">Use Case</h1>
<p>I am working on a project to extract Quote information from <a target="_blank" href="https://servicepath.co/">servicePath</a>. servicePath is a SaaS based Configure, Price, Quote solution (CPQ) focused on technology sales.</p>
<p>The <a target="_blank" href="https://developer-hub.servicepath.io/reference/intro/getting-started">servicePath REST APIs</a> use an HTTP Header Bearer token for Authentication. A token can be obtained from the <a target="_blank" href="https://developer-hub.servicepath.io/reference/post_api-v3-security-tokens">Generate Access Token</a> endpoint. Here is where the fun starts!</p>
<h2 id="heading-getting-a-servicepath-access-token">Getting a servicePath Access Token</h2>
<p>The servicePath token service requires that you send a username and password in the body of a POST request:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746899891292/a3870a3b-f9c3-497f-b772-b89de8bf8930.png" alt="Postman screenshot showing Service Path Token Service" class="image--center mx-auto" /></p>
<p>After calling the token service, you receive a Bearer token (access token) in a JSON response:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Bearer"</span>: <span class="hljs-string">"eyJhbG..."</span>, 
  <span class="hljs-attr">"Refresh"</span>: <span class="hljs-string">"I4-8Cun..."</span>
}
</code></pre>
<p>The Bearer (access token) is used in the ‘Authorization’ HTTP Header variable to authenticate when calling servicePath APIs:</p>
<pre><code class="lang-bash">curl --location <span class="hljs-string">'https://example.servicepath.io/api/v3/Quotes?page_size=500&amp;last_modified_from=2025-04-23'</span> \
--header <span class="hljs-string">'Content-Type: application/json'</span> \
--header <span class="hljs-string">'Authorization: Bearer eyJhbG...'</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🤦‍♂</div>
<div data-node-type="callout-text">I don't understand why servicePath did not use basic authentication (or even better, OAuth2).</div>
</div>

<p>The servicePath approach rules out using an APEX Web Credential in the classical sense. But all is not lost; we can still use APEX Web Credentials and benefit from REST Data Sources, etc. Read on to find out how.</p>
<h1 id="heading-approach">Approach</h1>
<p>This diagram illustrates the idea I am trying to convey in this post. We use custom code to get and refresh the Bearer token, then store it persistently in an HTTP Header type APEX Web Credential to be available to other APEX sessions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746922023031/619908fa-2941-4892-adce-0ad7411a533c.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-storing-credentials-securely">Storing Credentials Securely</h1>
<p>As we cannot use APEX Web Credentials, we must first find a secure way to store the API credentials (in this case, a username and password).</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">For ultimate security, you may want to consider using DBMS_CRYPTO or an external keystore to secure these credentials.</div>
</div>

<p>The approach described in this section is much more straightforward and sufficiently secure for most use cases. It relies on <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/23/dbtde/changes-this-release-oracle-database-transparent-data-encryption-guide.html">Oracle Transparent Data Encryption</a> (TDE) to secure the data at rest, and <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/23/dbred/index.html">Oracle Data Redaction</a> to keep the credentials secure from access via SQL. This approach works seamlessly on OCI Autonomous Databases or on the OCI APEX Service, which uses TDE on all tablespaces without any setup required on your part.</p>
<h2 id="heading-step-1-create-a-new-schema">Step 1: Create a New Schema</h2>
<pre><code class="lang-sql"><span class="hljs-comment">-- For OCI ATP, run as ADMIN</span>
<span class="hljs-comment">-- Having a separate schema separates securing the API keys from your APEX Parsing Schema.</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> api_sec
  <span class="hljs-keyword">IDENTIFIED</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">"ComplexPassword"</span>
  <span class="hljs-keyword">QUOTA</span> <span class="hljs-number">10</span>M <span class="hljs-keyword">ON</span> <span class="hljs-keyword">users</span>
  <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">TABLESPACE</span> <span class="hljs-keyword">users</span>
  <span class="hljs-keyword">TEMPORARY</span> <span class="hljs-keyword">TABLESPACE</span> temp
  PROFILE <span class="hljs-keyword">default</span>;

<span class="hljs-comment">-- Create Limited Grants to the new user</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SESSION</span>                 <span class="hljs-keyword">TO</span> api_sec;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span>, <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">PROCEDURE</span> <span class="hljs-keyword">TO</span> api_sec;
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">USER</span> api_sec <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">ROLE</span> <span class="hljs-keyword">NONE</span>;
</code></pre>
<h2 id="heading-step-2-create-the-credentials-table-in-the-new-schema">Step 2: Create the Credentials Table in the New Schema</h2>
<pre><code class="lang-sql"><span class="hljs-comment">-- Run as user api_sec or admin</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> api_sec.api_credentials 
 (credential_code <span class="hljs-built_in">VARCHAR2</span>(<span class="hljs-number">30</span>)   PRIMARY <span class="hljs-keyword">KEY</span>,
  api_user        <span class="hljs-built_in">VARCHAR2</span>(<span class="hljs-number">100</span>)  <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  api_pwd         <span class="hljs-built_in">VARCHAR2</span>(<span class="hljs-number">4000</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>);
</code></pre>
<h2 id="heading-step-3-create-a-data-redaction-policy-for-the-new-table">Step 3: Create a Data Redaction Policy for the New Table</h2>
<pre><code class="lang-sql"><span class="hljs-comment">-- Run as ADMIN</span>
<span class="hljs-comment">-- Any user except for your parsing schema user will see NULL for the API_PWD column</span>
<span class="hljs-keyword">BEGIN</span>
  DBMS_REDACT.ADD_POLICY
   (object_schema   =&gt; <span class="hljs-string">'API_SEC'</span>,
    object_name     =&gt; <span class="hljs-string">'API_CREDENTIALS'</span>,
    column_name     =&gt; <span class="hljs-string">'API_PWD'</span>,
    policy_name     =&gt; <span class="hljs-string">'REDCT_API_CRED_PWD'</span>,
    function_type   =&gt; dbms_redact.full,
    expression      =&gt; <span class="hljs-string">'sys_context(''userenv'',''current_user'') &lt;&gt; ''PARSING_SCHEMA'''</span>,
    <span class="hljs-keyword">enable</span>          =&gt; <span class="hljs-literal">TRUE</span>);
<span class="hljs-keyword">END</span>;
<span class="hljs-comment">-- Where PARSING_SCHEMA is your APEX Parsing Schema</span>
</code></pre>
<h2 id="heading-step-4-create-a-plsql-package-in-the-new-schema">Step 4: Create a PL/SQL Package in the New Schema</h2>
<p>This package will handle all interactions with the <code>api_credentials</code> table.</p>
<p>Create the package spec in the new <code>API_SEC</code> schema.</p>
<h3 id="heading-package-spec">Package Spec</h3>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">PACKAGE</span> api_credentials_pk <span class="hljs-keyword">AUTHID</span> DEFINER <span class="hljs-keyword">AS</span>

<span class="hljs-comment">-- This procedure is specific to servicePath</span>
<span class="hljs-comment">-- You would create specific procedures similar to this for </span>
<span class="hljs-comment">--   every API that requires their own unique authentication approach.</span>
<span class="hljs-keyword">PROCEDURE</span> set_servicepath_credentials 
  (p_apex_credential <span class="hljs-keyword">IN</span> <span class="hljs-built_in">VARCHAR2</span>,
   p_token_api_url   <span class="hljs-keyword">IN</span> <span class="hljs-built_in">VARCHAR2</span>);

<span class="hljs-comment">-- Create a new record in api_credentials</span>
PROCEDURE create_credential
 (p_credential_code IN api_credentials.credential_code%TYPE,
  p_api_user        IN api_credentials.api_user%TYPE,
  p_new_password    IN api_credentials.api_pwd%TYPE);

<span class="hljs-comment">-- TBD Create APIs to change a password and delete a credential.</span>

<span class="hljs-keyword">END</span> api_credentials_pk;
</code></pre>
<ul>
<li>Create with <code>DEFINER</code> rights so that when we call the APIs from our APEX parsing schema, the APIs can access the un-redacted password in the <code>API_SEC</code> schema.</li>
</ul>
<h3 id="heading-package-body">Package Body</h3>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">PACKAGE</span> <span class="hljs-keyword">BODY</span> api_credentials_pk <span class="hljs-keyword">AS</span>

<span class="hljs-comment">----------------------------------------------------------</span>
<span class="hljs-comment">-- Notice that we never return the value for API_PWD.</span>
<span class="hljs-comment">-- This prevents a developer from every seeing the password.</span>
<span class="hljs-keyword">PROCEDURE</span> set_servicepath_credentials
  (p_apex_credential <span class="hljs-keyword">IN</span> <span class="hljs-built_in">VARCHAR2</span>,
   p_token_api_url   <span class="hljs-keyword">IN</span> <span class="hljs-built_in">VARCHAR2</span>) <span class="hljs-keyword">IS</span>

  <span class="hljs-keyword">CURSOR</span> cr_credentials <span class="hljs-keyword">IS</span>
    <span class="hljs-keyword">SELECT</span> api_user
    ,      api_pwd
    <span class="hljs-keyword">FROM</span>   api_credentials
    <span class="hljs-keyword">WHERE</span>  credential_code = <span class="hljs-string">'SERVICEPATH'</span>;

  lr_credentials      cr_credentials%ROWTYPE;
  l_body_json         VARCHAR2(1000);
  l_response          CLOB;
  l_access_token      VARCHAR2(4000);
  l_response_obj      json_object_t;

<span class="hljs-keyword">BEGIN</span>

  apex_automation.log_info (<span class="hljs-string">'** Start Refresh servicePath Bearer Token **'</span>);
  apex_automation.log_info ('APEX Credential: '|| p_apex_credential);

  <span class="hljs-comment">-- Get servicePath Credentials</span>
  OPEN cr_credentials;
  FETCH cr_credentials INTO lr_credentials;
  CLOSE cr_credentials;

  <span class="hljs-comment">-- Build JSON with Username and Password.</span>
  l_body_json := JSON_OBJECT('Username' VALUE lr_credentials.api_user,
                             'Password' VALUE lr_credentials.api_pwd);

  <span class="hljs-comment">-- Set HTTP Headers.</span>
  apex_web_service.set_request_headers 
   (p_name_01   =&gt; 'Content-Type', 
    p_value_01  =&gt; 'application/json',
    p_name_02   =&gt; 'Accept', 
    p_value_02  =&gt; 'application/json',
    p_name_03   =&gt; 'User-Agent', 
    p_value_03  =&gt; 'APEX-Integration',
    p_reset     =&gt; TRUE);

  <span class="hljs-comment">-- Call the servicePath Token Web Service.</span>
  l_response := apex_web_service.make_rest_request
                 (p_url         =&gt; p_token_api_url,
                  p_body        =&gt; l_body_json,
                  p_http_method =&gt; 'POST');
  apex_automation.log_info ('Token API HTTP Response: '|| apex_web_service.g_status_code);

  IF apex_web_service.g_status_code &lt;&gt; 201 THEN
    apex_automation.log_error ('Token API <span class="hljs-keyword">Call</span> Failed. Response: <span class="hljs-string">'|| l_response);
    raise_application_error(-20010, '</span><span class="hljs-keyword">Error</span> getting servicePath Token. <span class="hljs-keyword">HTTP</span> <span class="hljs-keyword">Status</span> Code: <span class="hljs-string">' || apex_web_service.g_status_code);
  ELSE
    -- Store the Bearer token persistently in an APEX HTTP Header Web Credential
    l_response_obj := json_object_t.parse(l_response);
    l_access_token := l_response_obj.get_String('</span>Bearer<span class="hljs-string">');
    apex_credential.set_persistent_credentials
     (p_credential_static_id =&gt; p_apex_credential,
      p_key                  =&gt; '</span>Authorization<span class="hljs-string">',
      p_value                =&gt; '</span>Bearer <span class="hljs-string">' || l_access_token);
    apex_automation.log_info ('</span>Token <span class="hljs-keyword">Set</span>: <span class="hljs-string">'|| SUBSTR(l_access_token,1,10)||'</span>...<span class="hljs-string">');
  END IF;

  apex_automation.log_info ('</span>** <span class="hljs-keyword">End</span> <span class="hljs-keyword">Refresh</span> servicePath Bearer Token **<span class="hljs-string">');

EXCEPTION WHEN OTHERS THEN 
  apex_automation.log_error ('</span>Unhandled <span class="hljs-keyword">Error</span> [<span class="hljs-string">'|| SQLERRM || '</span>]<span class="hljs-string">');
  RAISE; 
END set_servicepath_credentials;

----------------------------------------------------------
PROCEDURE create_credential 
 (p_credential_code IN api_credentials.credential_code%TYPE,
  p_api_user        IN api_credentials.api_user%TYPE,
  p_new_password    IN api_credentials.api_pwd%TYPE) IS

BEGIN
  INSERT INTO api_credentials
    (credential_code, api_user, api_pwd)
  VALUES 
    (p_credential_code, p_api_user, p_new_password);
END create_credential;

END api_credentials_pk;</span>
</code></pre>
<h2 id="heading-step-5-grant-execute-to-the-package">Step 5: Grant Execute to the Package</h2>
<p>Next, allow your APEX parsing schema to run APIs in the package:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Run as API_SEC or ADMIN</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> api_sec.api_credentials_pk <span class="hljs-keyword">TO</span> &lt;&lt;PARSING_SCHEMA&gt;&gt;;
</code></pre>
<h2 id="heading-step-6-add-a-credential">Step 6: Add a Credential</h2>
<p>Create a credential:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Run from APEX Parsing Schema.</span>
<span class="hljs-keyword">BEGIN</span>
  api_credentials_pk.create_credential
   (p_credential_code =&gt; <span class="hljs-string">'SERVICEPATH'</span>,
    p_api_user        =&gt; <span class="hljs-string">'myusername'</span>,
    p_new_password    =&gt; <span class="hljs-string">'mypassword'</span>);
<span class="hljs-keyword">END</span>;
</code></pre>
<h2 id="heading-step-7-lock-the-new-schema">Step 7: Lock the New Schema</h2>
<pre><code class="lang-sql"><span class="hljs-comment">-- Run as ADMIN</span>
<span class="hljs-comment">-- Prevents anyone logging into this schema</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">USER</span> api_sec <span class="hljs-keyword">ACCOUNT</span> <span class="hljs-keyword">LOCK</span>;
</code></pre>
<h2 id="heading-summary">Summary</h2>
<ul>
<li><p>No user can see the <code>API_PWD</code> column, except for the <code>ADMIN</code> user. Even the <code>API_SEC</code> user cannot see it because we locked the account.</p>
</li>
<li><p>There is no API to get the password. Instead, we have an API to set the APEX Web Credential with the Access token. This prevents developers from seeing the API password (or the token).</p>
</li>
<li><p>No schemas have access to the <code>api_credentials</code> table except for <code>API_SEC</code> (which is locked).</p>
</li>
<li><p>The only way to affect the <code>api_credentials</code> is via the PL/SQL package <code>api_credentials_pk</code> which can only be run from your APEX parsing schema.</p>
</li>
</ul>
<h1 id="heading-putting-it-all-together">Putting it all Together</h1>
<p>In this section, we will create an APEX Web Credential to store the Bearer Token and an APEX Automation to update the APEX Web Credential with a new Bearer token on a schedule.</p>
<h2 id="heading-apex-web-credential">APEX Web Credential</h2>
<p>Before creating the Automation, we must create an ‘HTTP Header’ type APEX Web Credential to store the Bearer Token:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746984086278/3443647b-eece-4407-a6e0-f8e6e832b633.png" alt="APEX Web Credential" class="image--center mx-auto" /></p>
<p>The <code>set_servicepath_credentials</code> procedure uses <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/APEX_CREDENTIAL.SET_PERSISTENT_CREDENTIALS-Procedure-Signature-3.html">APEX_CREDENTIAL.SET_PERSISTENT_CREDENTIALS</a> to store the Bearer token. <code>apex_credential.set_persistent_credentials</code> sets the Credential Name and Secret so all APEX sessions can utilize it.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>
  apex_credential.set_persistent_credentials
   (p_credential_static_id =&gt; p_apex_credential,
    p_key                  =&gt; <span class="hljs-string">'Authorization'</span>,
    p_value                =&gt; <span class="hljs-string">'Bearer '</span> || l_access_token);
<span class="hljs-keyword">END</span>;
</code></pre>
<ul>
<li><p>The <code>p_key</code> parameter is stored in the ‘Credential Name’ field of the Web Credential.</p>
</li>
<li><p>The <code>p_value</code> parameter is stored in the ‘Credential Secret’ field of the Web Credential.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: Unlike the ‘OAuth2 Client Credentials’ type Web Credential, APEX will not refresh the token for you. This is why we will use an APEX Automation.</div>
</div>

<p>There is an overloaded version of this procedure. It does the same thing with different parameter names.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>
  apex_credential.set_persistent_credentials
   (p_credential_static_id =&gt; p_apex_credential,
    p_username             =&gt; <span class="hljs-string">'YourUserName'</span>,
    p_password             =&gt; <span class="hljs-string">'YourPassword'</span>);
<span class="hljs-keyword">END</span>;
</code></pre>
<h2 id="heading-apex-automation">APEX Automation</h2>
<p>We can use an <a target="_blank" href="https://blog.cloudnueva.com/apex-automations">APEX Automation</a> to update the Web Credential with a new Bearer token on a schedule. In the case of servicePath, the Bearer token is valid for eight hours. Given this, we may want to run the automation every seven hours.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746984313611/cf0b7dae-5b3b-4b35-a795-7d78538f4457.png" alt="APEX Automation to Refresh the Token." class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746984357266/9dd79841-0a0a-4c6b-8790-368226348da0.png" alt="APEX Automation Action" class="image--center mx-auto" /></p>
<ul>
<li><p>In the Automation Action, we call the set_servicepath_credentials API to set the Bearer token in the APEX Credential with a static ID called ‘servicepath’.</p>
</li>
<li><p>We also pass in the token URL for the servicePath token endpoint.</p>
</li>
</ul>
<h1 id="heading-using-the-web-credential">Using the Web Credential</h1>
<p>Now that the Bearer token is stored persistently in an APEX Web Credential (and refreshed on a schedule), we can use the HTTP Header type APEX Web Credential the same way as any other APEX Web Credential.</p>
<p>Used in <code>APEX_WEB_SERVICE</code>:</p>
<pre><code class="lang-sql">  l_response := apex_web_service.make_rest_request
                 (p_url                   =&gt; l_api_url,
                  p_http_method           =&gt; 'GET',
                  p_credential_static_id  =&gt; 'servicepath');
</code></pre>
<p>Used in an APEX REST Data Source:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746985657367/eb3284a2-d406-48ee-af17-ae73fde1db24.png" alt="Using the Credential in an APEX REST Source" class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>While Oracle APEX Web Credentials don’t natively support every API authentication scheme, with the right architecture, we can work around those limitations without compromising on security or maintainability.</p>
<p>In this example, we:</p>
<p>✅ <strong>Handled non-standard API auth</strong> — Built a flow to retrieve and refresh Bearer tokens from a servicePath API that doesn’t follow standard auth protocols</p>
<p>✅ <strong>Secured credentials effectively</strong> — Used Oracle Transparent Data Encryption and Data Redaction to store credentials securely on OCI ATP without external key management</p>
<p>✅ <strong>Enabled full APEX integration</strong> — Persisted tokens in APEX Web Credentials so REST Data Sources and APEX_WEB_SERVICE calls can use them transparently</p>
<p>✅ <strong>Automated refresh securely</strong> — Leveraged APEX Automation to update tokens on schedule, preserving a fully native APEX experience</p>
]]></content:encoded></item><item><title><![CDATA[Wrapping APEX_MAIL & Using JSON_OBJECT_T for Placeholders]]></title><description><![CDATA[Introduction
In this post, I’ll share two practical tips to enhance email functionality in Oracle APEX applications:

Why you should wrap the APEX_MAIL PL/SQL procedure with your own API.

How to use JSON_OBJECT_T to manage email placeholders more fl...]]></description><link>https://blog.cloudnueva.com/wrapping-apexmail-and-placeholders</link><guid isPermaLink="true">https://blog.cloudnueva.com/wrapping-apexmail-and-placeholders</guid><category><![CDATA[orclapex]]></category><category><![CDATA[#oracle-apex]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Jon Dixon]]></dc:creator><pubDate>Thu, 28 Aug 2025 11:11:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745693849176/c8053cc2-7c97-40cf-8c57-f9847ed86af4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>In this post, I’ll share two practical tips to enhance email functionality in Oracle APEX applications:</p>
<ol>
<li><p>Why you should <strong>wrap the APEX_MAIL PL/SQL procedure</strong> with your own API.</p>
</li>
<li><p>How to use <strong>JSON_OBJECT_T</strong> to manage email placeholders more flexibly.</p>
</li>
</ol>
<p>The second technique is also useful for passing parameters between procedures, without needing to know all the parameters in advance.</p>
<h1 id="heading-using-jsonobjectt-for-placeholders">Using JSON_OBJECT_T for Placeholders</h1>
<h2 id="heading-background">Background</h2>
<p><a target="_blank" href="https://blog.cloudnueva.com/apex-email-templates-advanced-formatting">APEX mail templates</a> allow you to include placeholder variables that are substituted when you send an email using the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeapi/SEND-Procedure-Signature-2.html">APEX_MAIL</a> API. All you have to do is pass JSON in the <code>p_placeholders</code> parameter. This is a very flexible approach.</p>
<p>However, in many apps, you’ll encounter multiple templates that share common fields. Without a structured approach, this often leads to redundant, boilerplate code. If we utilize the <code>JSON_OBJECT_T</code> data type, we can build a more efficient and lower-code solution.</p>
<h2 id="heading-setting-placeholders">Setting Placeholders</h2>
<p>First, we define a reusable procedure to populate common placeholder fields related to quotes:</p>
<pre><code class="lang-sql">PROCEDURE common_email_placeholders
  (p_quote_id     IN quotes.quote_id%TYPE,
   x_placeholders IN OUT NOCOPY json_object_t) IS

  lr_quote_info        cr_quote_info%ROWTYPE;

<span class="hljs-keyword">BEGIN</span>

  <span class="hljs-comment">-- Get Details for the Quote.</span>
  <span class="hljs-keyword">OPEN</span>  cr_quote_info (cp_quote_id =&gt; p_quote_id);
  FETCH cr_quote_info INTO lr_quote_info;
  CLOSE cr_quote_info;

  <span class="hljs-comment">-- Set Quote related values.</span>
  x_placeholders.put ('CUSTOMER_NAME', lr_quote_info.account_name);
  x_placeholders.put ('QUOTE_TYPE', lr_quote_info.quote_type);
  x_placeholders.put ('QUOTE_NAME', lr_quote_info.quote_number);
  x_placeholders.put ('QUOTE_AMOUNT', lr_quote_info.quote_amount);

  <span class="hljs-comment">-- Set other common values e.g. links back to our app, instance, name, and language.</span>
  x_placeholders.put ('LANGUAGE_CODE', lr_quote_info.language_code);
  x_placeholders.put ('INSTANCE_NAME', get_instance_name());
  x_placeholders.put ('CUSTOMER_APP_LINK', get_app_url());
  x_placeholders.put ('PORTAL_APP_LINK', portal_url());

<span class="hljs-keyword">END</span> common_email_placeholders;
</code></pre>
<p>The common procedure can include all possible placeholders.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It’s OK if we have additional fields in the JSON that are not referenced in all the templates. APEX will only substitute the placeholders that appear in the currently referenced template.</div>
</div>

<h2 id="heading-procedure-to-send-the-email">Procedure to Send the Email</h2>
<p>Next, we create a procedure to send a specific type of email, such as a request for approval. It builds on the common placeholders and adds email-specific fields if needed:</p>
<pre><code class="lang-sql">PROCEDURE email_request_for_approval
  (p_document_id IN evl_documents.document_id%TYPE,
   p_rule_name   IN VARCHAR2,
   p_to_email    IN VARCHAR2,
   p_comments    IN VARCHAR2 DEFAULT NULL) IS

  l_placeholders_obj   json_object_t := json_object_t();

<span class="hljs-keyword">BEGIN</span>

  <span class="hljs-comment">-- Set common email placeholders.</span>
  common_email_placeholders
   (p_document_id  =&gt; p_document_id,
    x_placeholders =&gt; l_placeholders_obj);

  <span class="hljs-comment">-- Add placeholders specific to this particular email.</span>
  l_placeholders_obj.put ('APPROVAL_TYPE', p_rule_name);

  <span class="hljs-comment">-- Call wrapper API to send the email (see below).</span>
  send_email
   (p_to                 =&gt; p_to_email,
    p_template_static_id =&gt; 'APPROVAL_REQUIRED',
    p_placeholders       =&gt; l_placeholders_obj);

<span class="hljs-keyword">END</span> email_request_for_approval;
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">By leveraging <code>JSON_OBJECT_T</code>, we gain tremendous flexibility. You can pass additional data between procedures <strong>without changing the parameter lists</strong>, which reduces code maintenance overhead.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Of course, with great power comes great responsibility. I am not advocating that we start creating all procedures and functions with just a single <code>JSON_OBJECT_T</code> parameter.</div>
</div>

<h1 id="heading-why-wrap-apexmail">Why Wrap APEX_MAIL?</h1>
<p>Instead of calling <code>APEX_MAIL</code> directly, I recommend using a wrapper procedure. Here’s a basic example:</p>
<pre><code class="lang-sql">PROCEDURE send_email
  (p_to                 IN VARCHAR2,
   p_cc                 IN VARCHAR2 DEFAULT NULL,
   p_from               IN VARCHAR2 DEFAULT NULL,
   p_template_static_id IN VARCHAR2,
   p_placeholders       IN json_object_t) IS

  l_to_email       VARCHAR2(32000);
  l_cc_email       VARCHAR2(32000);
  l_placeholders   CLOB;

<span class="hljs-keyword">BEGIN</span>

  <span class="hljs-comment">-- Convert Placeholders json_object_t object to a CLOB required by APEX_MAIL.</span>
  l_placeholders := p_placeholders.to_Clob;

  <span class="hljs-comment">-- Whitelist To email dlist. </span>
  <span class="hljs-comment">-- Useful for testing, avoid sending customers emails from development or test instances.</span>
  l_to_email := apply_email_whitelist (p_email_address =&gt; p_to);
  IF p_cc IS NOT NULL THEN
    <span class="hljs-comment">-- Whitelist CC email dlist.</span>
    l_cc_email := apply_email_whitelist (p_email_address =&gt; p_cc);
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

  <span class="hljs-comment">-- TBD additional code that checks a setting and does not send the email at all.</span>

  <span class="hljs-comment">-- Send the Email.</span>
  apex_mail.send 
   (p_to                 =&gt; l_to_email,
    p_cc                 =&gt; l_cc_email,
    p_from               =&gt; NVL(p_from,  'Quoting &lt;quoting@example.com&gt;'),
    p_replyto            =&gt; 'no-reply@example.com',
    p_template_static_id =&gt; p_template_static_id,  
    p_placeholders       =&gt; l_placeholders);  

  <span class="hljs-comment">-- TBD additional code to log the email.</span>
  <span class="hljs-comment">-- insert into email_log (to_email, subject, body, sent_on) values (...);</span>

<span class="hljs-keyword">END</span> send_email;
</code></pre>
<h2 id="heading-key-benefits-of-wrapping-apexmail"><strong>Key Benefits of Wrapping APEX_MAIL</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Description</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Email Whitelisting</strong></td><td>In non-production instances, we mustn’t send emails to the intended recipients. Having a wrapper for <code>APEX_MAIL</code> allows us to apply whitelisting logic to all email addresses before sending the email. For example, if an email was intended for (ceo@example.com, cfo@example.com), we can intercept it in DEV and replace it with (dev@example.com, dev2@example.com). This allows us to test approval emails and other messages without risking sending them to actual recipients.</td></tr>
<tr>
<td><strong>Disable Sending Easily</strong></td><td>Centralized check to suppress any emails from being sent if needed.</td></tr>
<tr>
<td><strong>Custom Logging</strong></td><td>We may also want to log emails to a table, including the email body, which APEX does not store in its email logs. Custom logging can be expanded to have a scheduled process that checks with the email service provider to confirm that emails have been delivered.</td></tr>
<tr>
<td><strong>Switch Email Providers</strong></td><td>Having a wrapper API allows us to change the email provider with minimal impact on our code. For example, we may want to send emails using a service like <a target="_blank" href="https://sendgrid.com/">SendGrid</a> instead of <code>APEX_MAIL</code>.</td></tr>
<tr>
<td><strong>Standardized Error Handling</strong></td><td>Centralize exception management for all outgoing emails (optional enhancement).</td></tr>
</tbody>
</table>
</div><h1 id="heading-conclusion">Conclusion</h1>
<blockquote>
<p>Wrapping APEX_MAIL and using JSON_OBJECT_T for placeholders are simple but powerful techniques to improve your APEX application’s email functionality.</p>
<ul>
<li><p>Wrappers improve control, security, and maintainability.</p>
</li>
<li><p>JSON-based placeholders enable flexible and scalable email generation.</p>
</li>
</ul>
<p>These strategies reduce technical debt today and future-proof your application for tomorrow.</p>
</blockquote>
]]></content:encoded></item></channel></rss>