Why I built this

Universities run scholarship programs with messy, rule-heavy eligibility logic: minimum CGPA, attendance, family income, branch (STEM versus non-STEM), backlogs, scholarship caps per student, and so on. The portals students see are usually static lists and PDF forms. A student rarely knows, without a counselor, which schemes they actually qualify for, what documents they need, or where their application is stuck.

ScholarPath is our answer: a conversational student portal where a single AI Agent finds scholarships, explains eligibility against the student’s real record, and submits the application with the student’s confirmation. The model never invents a verdict. Every eligibility outcome is computed by PL/SQL in the database and surfaced to the agent through declarative tools.

The whole thing runs on Oracle APEX 26.1 with OCI Generative AI as the backing LLM, and is authored using the APEXLang .apx-first workflow so the application is reproducible from source.

Live deployment at a glance

ThingValue
PlatformOracle APEX 26.1.0
Workspace / parsing schemaKOVDMO
Applicationid 100, alias SCHOLARSHIP-AGENT
LLMOCI Generative AI, cohere.command-r-plus-08-2024, region uk-london-1
AuthCustom auth scheme (student credentials), separate from APEX workspace admin
Source layoutAPEXLang .apx files under applications/scholarship-agent/, deployed via apex import

Three demo students (samiksha, arjun, neha) exercise the happy paths and the edge cases (a backlog, a non-STEM branch, a high-CGPA STEM student).

What’s new in APEX 26.1 that I leaned on

APEX 26.1 turns “AI features” from a bolt-on into a first-class app component. The pieces I used in ScholarPath:

1. Native AI Agents

An AI Agent is a real shared component now, with its own metadata table (WWV_FLOW_AI_AGENTS), a system prompt, a welcome message, a temperature and response-format pair, and a set of tools. It is no longer a pattern you have to assemble out of dynamic actions and process callbacks. You define it once, the runtime wires it to the chat surface, and the LLM call, tool dispatch, and conversation state are handled for you.

2. Declarative tools (retrieveData and executeServersideCode)

Tools attach to an agent and tell the runtime exactly what the LLM is allowed to call. There are two flavours I used:

  • retrieveData with a sqlQuery block. The runtime executes the SQL with the student’s session context, materializes the rows, and feeds them back to the LLM as the tool result. No PL/SQL needed.
  • executeServersideCode with a plsql block. The runtime calls your PL/SQL, and your code is expected to return data via apex_ai.set_tool_result(p_result, p_notification_message, p_notification_type, p_early_exit, p_is_safe). The procedure also supports p_early_exit (stop the LLM loop and show the result to the user) and p_is_safe (signal whether the side effect is reversible).

Tool parameters bind cleanly into the SQL or PL/SQL via colon binds (:SCHOLARSHIP_CODE), so the LLM-supplied arguments never become string-concatenated into a query.

3. Two execution points: augmentSystemPrompt vs onDemand

A tool with executionPoint: augmentSystemPrompt is fired automatically at the start of every turn and its result is folded into the system prompt. I use this to inject the signed-in student’s profile (CGPA, branch, attendance, family income, year of study) so the model always reasons from real data instead of asking the student to re-type it. A tool with executionPoint: onDemand is only invoked when the LLM decides to call it.

4. Human-in-the-loop approval gate (userApproval)

Any tool that performs a real, write-side action can declare a userApproval block. Before the runtime actually invokes your PL/SQL, the chat surface renders a confirmation dialog with the title, message, confirm-label and cancel-label you specify. The LLM cannot bypass it, and the user can always cancel. I use this on submit_application so the agent can never submit a scholarship on the student’s behalf without an explicit click.

5. Generative AI Services as workspace components

A Generative AI Service is now a workspace-level configuration that points at OCI, OpenAI, Cohere, Google Gemini, or any OpenAI-compatible endpoint. It carries the model name, endpoint URL, the credential reference, and a usedByAppBuilder flag. Multiple agents (and multiple apps) in the workspace can share one service, which keeps credential management central. I bound ScholarPath to a single OCI service called OCI Generative AI running cohere.command-r-plus-08-2024.

6. The inline native AI Assistant region

APEX 26.1 ships a dynamic action and rendering primitive (showAiAssistant) that draws the agent’s chat panel into a container of your choosing. I use the inline variant with appearance.displayAs: inline and containerSelector: #ai-chat, so the chat lives inside our custom region (a green-gradient agent shell with avatar and “online” indicator) and feels like part of the page, not a floating widget.

7. APEXLang .apx-first authoring

Every page, region, item, dynamic action, authentication scheme, agent and tool in ScholarPath is defined in .apx source files. The toolchain is apex validate (a local linter), then apex import -input applications/scholarship-agent -id 100 against the target workspace. The result is the same as if you had clicked it together in App Builder, but it is reviewable in git and reproducible across environments.

The local APEXLang compiler also ships templates for AI Agents (templates/shared-components/ai-agents/), so the agent and tool grammar has a canonical reference, not just a tribal-knowledge one.

The agent: five tools, one grounding contract

The whole behaviour of ScholarPath lives in one agent definition and five tools. Here is the shape of each tool, why it exists, and what it grounds against.

Tool 1: get_student_context — retrieveData, augmentSystemPrompt

SQL select schol_agent_api.student_context as student_profile_json from dual

Fires at the start of every turn. Returns the signed-in student’s profile as a single JSON document (name, branch, year, CGPA, attendance, backlogs, family income, etc.). The system prompt tells the model: “the signed-in student’s profile is provided to you as JSON by the get_student_context augment. Always reason from that profile and from the tool results below.”

This is how I keep the LLM from asking the student “what’s your CGPA?” or worse, hallucinating one.

Tool 2: retrieve_scholarships — retrieveData, onDemand

SQL select code, name, amount_label, frequency, description, eligible_yn, match_pct   from schol_v_my_scholarships  order by match_pct desc

A session-scoped view, schol_v_my_scholarships, joins the active scholarships with the signed-in student and asks the eligibility engine to compute, for each scholarship, both an eligible_yn flag and a match_pct. The agent gets a ranked list it can read off, with the strongest matches first.

Tool 3: check_eligibility — executeServersideCode, onDemand

PL/SQL schol_agent_api.check_and_set_result(p_scholarship_code => :SCHOLARSHIP_CODE);

This is the thin-wrapper pattern that the APEX 26.1 template literature recommends. The tool’s PL/SQL is one line. All the work lives in the packaged procedure schol_agent_api.check_and_set_result, which:

  1. Runs schol_eligibility_api.evaluate against the SCHOL_CRITERIA table for that scholarship code.
  2. Builds a structured per-criterion result (requirement, the student’s value, met yes or no, an overall verdict).
  3. Calls apex_ai.set_tool_result with a clean JSON payload and an appropriate notification.

Two benefits: the LLM cannot see the implementation, only the verdict; and I can change the eligibility engine without re-importing the app.

Tool 4: submit_application — executeServersideCode, onDemand, with userApproval

PL/SQL schol_agent_api.submit_and_set_result(p_scholarship_code => :SCHOLARSHIP_CODE);

Same thin-wrapper pattern, but with a userApproval block:

APEXLang userApproval {     requiresConfirmation: true     title: Submit scholarship application?     message: The agent will submit your application to the scholarship              committee on your behalf. Do you want to proceed?     confirmLabel: Yes, submit it     cancelLabel: Not yet }

The runtime intercepts the LLM’s call, shows the dialog, and only proceeds on confirm. On submit, the package inserts the row, returns the new application number, and apex_ai.set_tool_result carries it back into the conversation with a success notification.

The agent’s system prompt reinforces the gate: “to submit, call submit_application. This is a real action: ALWAYS confirm with the student first (the tool also asks for explicit approval).”

Tool 5: track_applications — retrieveData, onDemand

SQL select app_no, scholarship, status, stage,        to_char(updated_on,’DD Mon YYYY’) as last_updated   from schol_v_my_applications

A simple session-scoped status feed for follow-up turns (“where is my STEM application?”).

The grounding contract, in one paragraph

The model is a router and a writer. It picks the right tool, fills in parameters, summarizes the result, and asks the student the next question. It does not compute eligibility, it does not invent CGPAs, it does not know which scholarship codes exist outside what the database returns. The five tools collectively define the entire universe of moves the agent can make, and each move is a database call the security model already trusts. That contract is what makes the agent boring (in the good way) in production.

System prompt: keeping the model in its lane

The system prompt does three jobs:

  • Sets the role and tone. Warm, concise, encouraging. Uses the student’s first name occasionally. Indian Rupees for amounts.
  • States the grounding rules explicitly. “Never guess CGPA, attendance, income, eligibility or application status.” It then maps each user intent to the right tool (“to recommend scholarships, call retrieve_scholarships…”).
  • Strips AI-signature style. The prompt includes: “Never use em dashes or double hyphens in your replies; use commas, periods, semicolons, or parentheses instead. Avoid emojis unless the student uses one first.” This is a small but important touch for an enterprise-facing product where students should not feel like they are talking to a generic chatbot.

I kept the prompt under the 4000-character limit of WWV_FLOW_AI_AGENTS.SYSTEM_PROMPT (drafted at around 3800 chars to leave headroom for indentation and encoding).

Two parameters round it out: temperature: 0.2 (I want predictability over creativity for eligibility explanations) and responseFormat.type: text (the agent’s output is conversational, not JSON).

The OCI Generative AI wiring

The agent needs an LLM. I bound ScholarPath to OCI Generative AI in the uk-london-1 region, using the cohere.command-r-plus-08-2024 model. The wiring has three pieces:

  • Workspace web credential. Created programmatically with apex_credential.create_credential plus apex_credential.set_persistent_credentials (the 4-argument OCI overload that takes client_id = user OCID, client_secret = PEM private key, namespace = tenancy OCID, fingerprint).
  • Generative AI service. A workspace component bound to that credential, pointed at https://inference.generativeai.uk-london-1.oci.oraclecloud.com, with the model name configured.
  • Network ACL. KOVDMO, APEX_PUBLIC_USER, and the runtime user APEX_260100 all need connect:443 to the OCI inference host, plus a separate resolve ACE (without a port, since mixing connect with resolve and a port in the same ACE throws ORA-24244).

The agent’s genAI { service: @<service-static-id> } line ties the agent to the service. One workspace, one service, many possible agents.

Architecture in one diagram (in words)

  • Front door. APEX page 100 renders a single dynamic content region that calls schol_html.render_chat_page(p_student_id). The region’s left pane is our custom agent shell; the right rail is a static set of cards (Conversation Summary, Application Progress 4-step tracker, “Try asking…” chips).
  • Chat surface. Inside the agent shell I drop an inline showAiAssistant dynamic action that mounts the native AI Assistant into #ai-chat. A small JS shim wired to the chip elements pushes their text into the assistant’s textarea so a click on “Show me what I qualify for” submits a real turn.
  • Agent runtime. APEX 26.1 calls the OCI Generative AI service, dispatches tool calls, materializes results, and handles the approval dialog. No custom code here.
  • Data and rules. A SCHOL_* schema holds scholarships, criteria, applications, and students. schol_eligibility_api evaluates criteria deterministically. schol_agent_api wraps each tool’s behaviour so the PL/SQL inside the agent is a single line.
  • Auth. Custom auth scheme calls schol_auth.authenticate(p_username, p_password). Post-auth populates session items the views and tools rely on.

Lessons from shipping it

A few things were not obvious from the docs and were worth the time to learn:

  1. The thin-wrapper pattern pays for itself. A one-line plsqlCode calling a packaged procedure is easier to read, easier to evolve, and survives apex import cleanly. The procedure owns the apex_ai.set_tool_result call.
  2. Generative AI services do not import inside an app bundle. A genAIService block dropped into applications/<app>/workspace-components/ will fail to resolve its credential reference at import time, because the bundle import resolves credential references against the bundle, not the workspace. I create the credential programmatically and the service in App Builder (or via the workspace-level .apx, kept outside the app tree), then bind it to the agent.
  3. Two things reset on every apex import and have to be re-wired:
  4. The custom auth scheme’s Authentication Function Name (APEXLang has no property for it). Re-run wwv_flow_imp_shared.create_authentication with p_attributes => ‘{“enable_legacy_attributes”:”Y”,”authentication_function”:”authenticate”}’. Without this, every login fails with “Invalid Login Credentials”.
  5. The agent’s remote_server_id (the foreign key to the GenAI service). Re-apply with a one-line UPDATE apex_260100.wwv_flow_ai_agents SET remote_server_id = <svc_id> WHERE flow_id = 100 AND static_id = ‘scholarship-agent’;.
  6. APEXLang block names are static ids. aiAgent scholarship-agent ( declares an agent whose static id is scholarship-agent. Trying to also set advanced.staticId will pass the local compiler and fail at live import.
  7. Tool parameter blocks use UPPER CASE labels. parameter SCHOLARSHIP_CODE ( matches the colon-bind :SCHOLARSHIP_CODE in the SQL or PL/SQL. The block only needs description:; dataType defaults to varchar2 and required defaults to true.
  8. Tool type values are camelCase semantic names, not the internal NATIVE_* constants. Use retrieveData, executeServersideCode, executeClientsideCode.

What we get out of it

The student experience: one chat window, three clicks to find the scholarships they qualify for, a clear explanation of any “no” answer with the missing criterion called out by name, and a confirmation dialog before anything is actually submitted. The administration experience: every business rule lives in a PL/SQL package the institution already controls, the LLM is a thin layer on top, and adding a new scholarship category is a SCHOL_CRITERIA row, not a model retraining job.

That is the bet APEX 26.1 makes: that the right place for the eligibility engine, the audit trail, and the security model is the database, and the right place for the LLM is whatever your workspace credential happens to point at. ScholarPath is what that bet looks like when you take it end to end.

Refer to this post for the working video of the agent: https://www.linkedin.com/posts/mohanapriyav_agentic-oracleapex-ugcPost-7465906249042186240-hVE4/?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAQkvaIBb_rtn3otxqK9NBFqpuZ8a86_GRM

Leave a comment