A client came to me with what sounded like a contained project: migrate their Salesforce field service app to Oracle APEX. Two months. No rush — until you looked at the scope.

The app was the operational backbone of their field service operation. Technicians used it daily to receive work orders, schedule visits to client sites, log crew hours, and record the materials consumed on each job. Preventive maintenance schedules ran through it. Inventory tracking ran through it. Dispatchers lived in it. It wasn’t a reporting tool or a nice-to-have — it was how the business ran.

The motivation was twofold. Salesforce licensing costs had become hard to justify for the scope of what the app actually did. And the client needed capabilities Oracle APEX handles better by design.

Their end users needed reports they could configure themselves — filtering, column selection, aggregations, export — without going back to a developer. APEX Interactive Grids do this out of the box. They also wanted a more open customization model: APEX lets you incorporate any JavaScript library directly, build custom plug-ins, and tap into a growing ecosystem of open-source community plug-ins ready to install and use. Salesforce allows third-party JavaScript libraries in Lightning Web Components, but they must pass Lightning Locker or Lightning Web Security validation — not all libraries are compatible, and those that aren’t require forking and patching. For a team that wanted to extend the application freely and keep ongoing costs predictable, APEX was the more practical foundation.

So the destination was clear. The path there was not. There was no Salesforce developer environment to work in, no handover documentation, no one from the original implementation available to answer questions. What I had was the source code repository and access to the running application.

Twenty-six pages. Seventeen tables. A business logic layer I had never seen before, written in Apex — Salesforce’s language, which shares only its name with Oracle APEX and nothing else.

What made this project survivable — honestly, what made it possible at the pace we needed — was using AI as a co-pilot throughout every stage. Claude Code was my primary tool, though the approach works with any capable coding AI like GitHub Copilot or Codex. This post walks through exactly how I structured that collaboration, what worked, what didn’t, and how you can adapt it for your own migration.

The setup

Before writing a single line of Oracle code, I configured a workspace that would let the AI and I work effectively together:

  • VS Code as the primary IDE
  • Claude Code extension for VS Code (the AI layer)
  • Oracle SQL Developer extension for VS Code (database connectivity and project management)
  • SQLcl with Oracle SQL Projects (version-controlled database scripts)
  • A dedicated GitHub repository for the migration project

The SQLcl project structure is worth highlighting: it gives you a folder-based layout for your database objects (tables, packages, sequences, triggers) that deploys in dependency order. When the AI generates scripts, they land in the right place automatically.

Step 1: Understanding the Salesforce source code

My first problem was that I don’t speak Salesforce. The repository had files and folders I had never navigated before, and without understanding the structure, I couldn’t know what to migrate or where to find it.

How a Salesforce repository is organized

A standard Salesforce DX project follows a predictable layout under force-app/main/default/. Here’s what each folder contains and why it matters for migration:

  • objects/ — The data layer. Each subfolder is a Salesforce object (equivalent to a database table). Inside you’ll find the object’s fields, validation rules, and relationships defined in Custom objects end with __c (e.g. WorkOrder__c). This is where you extract your ER model.
  • classes/ — The business logic layer. These .cls files are Apex classes — Salesforce’s Java-like Controllers, services, utility classes, and trigger handlers all live here. Each class has a companion .cls-meta.xml file with deployment metadata.
  • triggers/ — Database-level event handlers (before/after insert, update, delete). Similar in concept to Oracle triggers, but written in Apex.
  • lwc/ — The UI layer. Lightning Web Components are the modern Salesforce frontend framework. Each component is a folder containing an .html template, a .js controller, and a .js-meta.xml config file. One component typically maps to one screen or reusable UI element.
  • aura/ — An older UI framework you may still encounter alongside LWC in legacy Same concept, different syntax.
  • permissionsets/ and profiles/ — Security and access control Useful context for understanding what roles the app supports.
  • flows/ — Salesforce Flow These are visual process builders that encode business logic outside of Apex — easy to miss if you’re only looking at code files.

The naming convention is your navigation system: a component called workOrderDetail in lwc/ will typically call a class called WorkOrderDetailController.cls in classes/, which in turn queries the WorkOrder__c object defined in objects/. Once you see the pattern, the whole codebase becomes readable.

Asking Claude to map it

With this structure in mind, my first Claude Code prompt was:

“I have a Salesforce repository in front of me. I’ve never worked with Salesforce before. Please analyze the folder structure and give me a map of what’s in here — what each folder contains, what file types I’ll encounter, and how to read the relationship between the UI components, the business logic, and the data layer.”

What came back confirmed the structure above and applied it specifically to our codebase — naming which LWC components mapped to which Apex classes, which objects they read from, and where the trigger logic lived. This took about ten minutes. Without AI, I’d have spent two days reading documentation.

One important thing I learned immediately: AI reads Salesforce code well, but it doesn’t always read it correctly. Claude mapped our WorkOrder__c object relationships slightly wrong on the first pass — it missed a lookup that ran through a junction object. I caught it by comparing the map against the running app. That validation step — always checking AI output against the live application — became a discipline throughout the project.

Step 2: DB migration — understand the data before you touch anything else

Even if your Oracle schema already exists, do not skip this step.

The Mermaid diagram exercise is not just about generating DDL scripts. It forces you to understand the data model at a level that every subsequent decision depends on — how entities relate, where the business rules live in the schema, and what architectural choices Salesforce made that you may want to do differently in Oracle. This is also the moment to make deliberate decisions about primary keys, constraint handling, naming conventions, and check constraints. Decisions made here echo through every package, every APEX page, and every query in the project.

If your Oracle schema is already defined, use this step to document it as a Mermaid diagram anyway. You’ll refer to it constantly.

Extract the data model from the source code

“Read all the .object files in force-app/main/default/objects/. List every custom object, its fields with data types, and any lookup or master-detail relationships between objects.”

Salesforce .object files are essentially XML schema definitions. They list every field, its type, and its relationships to other objects. Ask Claude to read them all and identify the entities:

For our field service app, this surfaced entities like WorkOrder, ServiceAppointment, Asset, ServiceTerritory, and Technician — along with the relationships between them.

Generate a Mermaid ER diagram

Once you have the entity list, ask Claude to turn it into a Mermaid erDiagram. This gives you something visual you can reason about and share with a client or team member:

erDiagram
    WORK_ORDERS ||--o{ SERVICE_APPOINTMENTS : schedules
    WORK_ORDERS }o--|| ASSETS : covers 
    SERVICE_APPOINTMENTS }o--|| TECHNICIANS : assigned_to 
    TECHNICIANS }o--|| SERVICE_TERRITORIES : belongs_to

WORK_ORDERS {
    number work_order_id PK 
    varchar2 wo_number 
    varchar2 status
    date scheduled_date 
    number asset_id FK
}

This is the moment to review the model critically and apply your Oracle standards before any scripts are generated. Key decisions to make:

Primary keys — use GENERATED ALWAYS AS IDENTITY (Oracle 12c+) rather than sequences with triggers. It’s cleaner, requires less code, and is the modern Oracle standard.

Foreign key constraints — define them as DEFERRABLE INITIALLY DEFERRED. This allows bulk inserts and data migrations without fighting constraint ordering, while still enforcing referential integrity when transactions commit.

Check constraints — any column that Salesforce implemented as a picklist (status, priority, type) should become a CHECK constraint in Oracle. Don’t rely on the application layer alone to enforce valid values.

Naming conventions — apply them consistently before generating scripts, not after:

  • Tables: plural, uppercase (WORK_ORDERS, TECHNICIANS)
  • Primary key constraint: <table_name>_pk (e.g. work_orders_pk)
  • Foreign key constraints: <table_name>_fk01, <table_name>_fk02 (numbered sequentially)
  • Indexes: <table_name>_<column>_ix

Feed these decisions back into Claude before script generation:

“Update the Mermaid diagram to apply these standards: [list your decisions]. Once confirmed, generate the Oracle DDL scripts following the CLAUDE.md conventions in this project.”

Generate the DDL scripts

With a clean, reviewed diagram and clear standards, ask Claude to generate the Oracle DDL. The scripts go straight into your SQLcl project, organized by object type, ready to deploy in dependency order.

This step transforms a Salesforce data model into an Oracle schema that you actually understand and own — not one that was generated blindly and accepted without scrutiny.

Step 3: Map the application pages

Now that I understood the data model, I needed to understand the application itself — what pages existed, what each one did, and how they connected.

“Analyze the Lightning Web Components in this repository. Give me a list of every distinct page or screen in the application, what its purpose is, what data it reads or writes, and how users navigate between pages.”

Claude produced a list of 18 pages for our app — work order lists, detail views, scheduling calendars, technician dashboards, asset history screens. Each entry included the Salesforce components involved and the Apex classes they called.

This is where the live app becomes essential. I opened the running Salesforce application alongside Claude’s map and walked through every page. Claude had missed one screen entirely (an admin configuration page buried in a settings menu) and had merged two separate list views into one. I fed those corrections back:

“The map is mostly correct but needs two adjustments: [details]. Please update the map.”

The corrected map became the migration spec. Every subsequent step referenced it.

Step 4: Generate PL/SQL CRUD packages

Before touching business logic, I built the data access layer. For each of the 17 tables, I asked Claude to generate a PL/SQL package with standard CRUD procedures:

“Generate a PL/SQL package for the WORK_ORDERS table. Include procedures for: insert_row, update_row, delete_row, get_by_id, and a function get_list that accepts optional filter parameters and returns a SYS_REFCURSOR. Follow Oracle naming conventions. The package spec and body should be in separate files.”

This produced consistent, predictable data access across all tables. APEX pages would call these packages rather than writing inline SQL — a pattern that makes the application easier to maintain and the APEX development cleaner.

Claude’s first attempt at the get_list function used a dynamic SQL approach I wasn’t happy with. I corrected it:

“Rewrite get_list using static SQL with conditional WHERE clauses instead of EXECUTE IMMEDIATE. Here’s the pattern I want: [example].”

This back-and-forth is normal and expected. The AI gets you 80% of the way there; you close the gap with your standards and judgment.

Step 5: Migrate the business logic

With the CRUD layer in place, I tackled the business logic — the Apex classes that contained field validation, status transitions, scheduling rules, and notification triggers.

Map the logic first

Before writing a single line of PL/SQL, ask Claude to map what logic belongs where:

“For each page in our application map, list the Apex classes and methods it calls. I want to know exactly what business logic each screen depends on.”

This gives you a dependency picture — which classes are shared across multiple pages, which are page-specific, and which are utility layers called from everywhere.

Design the package architecture in Plan Mode

Don’t ask Claude to start generating packages immediately. Instead, use Plan Mode to design the architecture first, review it, and lock it down before any code is written.

“Based on the business logic map and the Salesforce Apex classes in this project, propose a PL/SQL package architecture. For each package: define its responsibility, list its public procedures and functions, identify any private helpers, specify its dependencies on other packages, and explain the error handling approach. Do not generate any code yet — present this as a design proposal.”

The proposal will typically suggest grouping by functional area rather than one package per table — a PKG_WORK_ORDER_SVC that handles all work order business logic, a PKG_SCHEDULING_SVC for appointment logic, a PKG_NOTIFICATION_SVC for alerts. Review this critically. Things to evaluate:

Public vs private split — procedures called by APEX pages go in the spec. Internal helpers, validation sub-routines, and shared logic go in the body only. If something is in the spec without a clear external caller, push back.

Error handling strategy — establish one approach and hold to it. My preference: RAISE_APPLICATION_ERROR with a consistent error number range for business rule violations, and unhandled exceptions propagating naturally for unexpected errors. Ask Claude to confirm the proposal follows this before approving.

Dependency order — packages that depend on other packages must be noted explicitly. The SQLcl project needs to deploy them in the right sequence, and circular dependencies need to be caught at design time, not after scripts are generated.

Once you’re satisfied with the design, approve it and then ask Claude to generate:

“The architecture looks good with these adjustments: [your changes]. Now generate the package specs and bodies following the CLAUDE.md conventions in this project.”

Translate the Salesforce logic faithfully

When Claude generates the packages, instruct it to preserve the original logic rather than simplify it:

“Here is the Salesforce Apex class WorkOrderService.cls. Translate the business logic into PKG_WORK_ORDER_SVC following the approved architecture. Preserve the logic faithfully — don’t simplify. Where Salesforce-specific features have no Oracle equivalent, add a comment explaining what the original code did and flag it for manual review.”

That last instruction — flag Salesforce-specific behavior — was important. Things like Salesforce’s platform events, governor limits patterns, and trigger framework conventions don’t translate directly. Claude would mark them with — TODO: SALESFORCE-SPECIFIC — so I could review them rather than silently getting something wrong.

Step 6: Generate the APEX application

With the database objects and PL/SQL packages in the SQLcl project, I had Claude plan the APEX application before generating any scripts:

“Based on our 26-page application map, create a plan for an Oracle APEX application that replicates this functionality. For each page: suggest the APEX page type (interactive report, form, calendar, etc.), the page template, the data source (which PL/SQL package procedure), and any APEX components needed. Present this as a structured plan before generating any code.”

I reviewed the plan, pushed back on two page type choices (Claude suggested a classic report where I wanted an interactive grid), and then approved it:

“The plan looks good with these adjustments: [changes]. Now generate the APEX application export script.”

What actually happened

This is the step where I want to be honest: Claude struggled here more than anywhere else in the project.

The APEX export script format is complex — it’s a dense, highly structured SQL file that APEX’s import engine is unforgiving about. Claude’s first attempt produced a script that failed on import. The second attempt imported but had broken page references and missing region source queries. By the third iteration, after feeding Claude the specific import errors and correcting the structure incrementally, I had a script that loaded cleanly.

Even then, what I got was not an application ready to deploy. It was a solid first version — navigation structure intact, pages present, regions wired to the right package procedures, basic form items in place. But the layouts needed work, validations needed tuning, and several dynamic actions had to be built manually. I’d estimate it represented about 60% of the work done, which on a 26-page application is still a meaningful head start. Building the same skeleton by hand would have taken considerably longer.

The iteration process taught me something useful: when Claude produces a broken APEX script, don’t ask it to “fix it” in the abstract. Copy the exact import error, paste it back, and ask Claude to correct that specific issue. Targeted corrections work far better than open-ended retries.

Where this is heading

This is the area where I expect the most meaningful improvement in the near future. Oracle APEX 26.1 introduces APEXLang — a new human-readable, text-based format for representing APEX applications. The previous export format is a dense SQL script that AI models have to reverse-engineer and reconstruct blind. APEXLang replaces that with structured, diffable, Git-friendly text that is far easier for both humans and AI to read, generate, and validate.

More importantly, APEXLang is designed as the foundation for what Oracle is calling GenDev — generative development, where AI can understand and produce APEX application logic from natural language. If you’re reading this after APEX 26.1 ships, the iteration cycles described above may already be a thing of the past. For now, go in expecting to work the script through a few cycles before it imports cleanly, and budget time for manual polish after import.

Step 7: Deploy

Deployment was the straightforward part, which is the point of having the SQLcl project structure:

sql user/password@connection @deploy.sql

The SQLcl project deploys tables first, then sequences, then packages, then any seed data — in dependency order. Once the database objects were in place, I imported the APEX application export through the APEX builder, made the final adjustments, and had a running first version to test against the Salesforce original.

What I’d do differently

Invest more time in the page map validation. The hours I spent walking through the live Salesforce app alongside Claude’s map paid back many times over. Every correction at that stage prevented rework later.

Push back on AI-generated PL/SQL earlier. I accepted some of Claude’s first-pass packages without scrutinizing them closely enough, and found inconsistencies when the APEX pages started exercising them. Establish your standards upfront and give Claude a reference package to follow from the beginning.

Don’t expect the APEX generation to be hands-off. The generated application script is a head start, not a finish line. Budget time for manual APEX work — roughly 30–40% of what a full manual build would take, in my experience.

The honest summary

Claude Code (or any capable coding AI) doesn’t replace your Oracle and APEX expertise. What it does is eliminate the tax of unfamiliarity — reading a codebase in a language you don’t know, translating patterns between platforms, generating boilerplate you’d otherwise write by hand. On a 26-page, 17-table application, I estimate the AI collaboration cut the project timeline roughly in half.

The discipline that made it work: treat every AI output as a draft, not a deliverable. Validate against the running application. Establish standards early and hold the AI to them. Correct mistakes explicitly so they don’t propagate.

If you’re sitting in front of a Salesforce repository wondering where to start — start by asking Claude (or your AI of choice) to explain it to you. You’ll be writing Oracle packages within the hour.

About the author

Gerardo Meoño is a Senior APEX Developer specializing in Oracle technologies — Oracle Database, Oracle APEX, and PL/SQL — with additional experience in Python and Java. He works on application modernization projects, helping teams migrate legacy and third-party platforms onto the Oracle stack.