Back to blog
Migration Dec 19, 2025 9 min read

From WHEN-VALIDATE-ITEM to TypeScript Validators: Preserving 25 Years of Business Rules

Last updated Apr 9, 2026

TL;DR

WHEN-VALIDATE-ITEM triggers hold 38% of all Oracle Forms business logic. Translating them to TypeScript requires splitting pure validation from data access and mirroring Forms' execution model, not just converting syntax.

Where the rules actually live

What fraction of an Oracle Forms application’s business logic actually lives in a single trigger type? Across 2,400 .fmb files we’ve analyzed, the answer is 38%. WHEN-VALIDATE-ITEM triggers are the single largest bucket of embedded PL/SQL in most Forms applications — more than all form-level triggers, package bodies, and library units combined. We’ve written separately about what’s actually inside a .fmb file; this post focuses on the triggers that carry the bulk of the logic. An insurance carrier we worked with had 9,700 of them in a policy administration system first deployed in 2001.

Migrations live or die on how these triggers get translated.

One of my first Oracle projects in 2004 was a Polish telecoms billing front-end with a WHEN-VALIDATE-ITEM on the subscriber plan code that ran a cross-table lookup plus a credit-check call. It worked perfectly for four years, until someone added a trailing space to a reference value and the trigger started silently rejecting every new contract on Monday morning. I drove 200 kilometers to the customer site to watch them reproduce the bug in person, because nobody believed it wasn’t the network. The root cause was a missing RTRIM. I’ve never written a string comparison without normalization since.

What WHEN-VALIDATE-ITEM is supposed to do

The trigger fires when an item loses focus and its value has changed. Its job is to decide whether the new value is acceptable. If not, it raises FORM_TRIGGER_FAILURE, which returns focus to the item and displays a message. The rules vary — range checks, cross-field dependencies, database lookups, regulatory limits.

-- Typical WHEN-VALIDATE-ITEM
IF :POLICY.COVERAGE_AMOUNT > 1000000
   AND :POLICY.UNDERWRITER_LEVEL < 3 THEN
  MESSAGE('Coverage above 1M requires senior underwriter');
  RAISE FORM_TRIGGER_FAILURE;
END IF;

That four-line example touches two form items, a hardcoded threshold, and a display message. It’s representative. The median WHEN-VALIDATE-ITEM trigger in our sample is 11 lines. The 95th percentile is 84.

The translation target

Our target shape is a pure TypeScript validator function that takes the current form state and returns either null or an error object. No side effects, no DOM access, no direct database calls from the validator itself.

export const validateCoverageAmount: Validator<PolicyForm> = (state) => {
  if (state.coverageAmount > 1_000_000 && state.underwriterLevel < 3) {
    return { field: "coverageAmount",
             message: "Coverage above 1M requires senior underwriter" };
  }
  return null;
};

Database lookups move to async validators that call a generated REST endpoint. The validator stays pure; the endpoint owns the data access. This split is the single most important decision in the translation — it’s what makes the rules testable, cacheable, and auditable. The general shape of what changes and what stays the same when PL/SQL becomes TypeScript applies here too: the business logic survives mechanically, only the runtime changes.

Handling the messy cases

Not every trigger is four clean lines. We’ve catalogued five patterns that resist naive translation:

  • Implicit commits. Triggers that call COMMIT mid-validation. These get refactored into explicit save steps. The full list of invisible commit points and the way they interact with database triggers is covered in our piece on database triggers and package state.
  • Global variables. References to :GLOBAL.xyz that store session state across forms. These become a typed session store.
  • DO_KEY calls. Triggers that re-trigger navigation events. These become explicit state transitions.
  • Dynamic SQL. FORMS_DDL and EXEC_SQL calls. These get flagged for human review — about 6% of triggers land here.
  • Cross-form references. Reading items from another open form. These become session-scoped context objects.

Roughly 91% of WHEN-VALIDATE-ITEM triggers fall into clean patterns that translate automatically. The remaining 9% need review. Knowing which 9% before the project starts is the difference between a predictable timeline and a quarterly slip.

Preserving semantics the compiler can’t see

The hardest rules are the ones that depend on Oracle Forms’ execution model. A WHEN-VALIDATE-ITEM only fires when the item has changed — not on every save, not on query results, not on programmatic assignment. Getting this wrong means validators fire too often and break workflows that used to work.

We mirror the Forms semantics explicitly. The validator runtime tracks per-field dirty state, suppresses validation during query population, and honors the original trigger hierarchy (item, block, form). The translated rule is identical; the runtime that invokes it is what matches behavior.

Testing the translation

Every migrated validator gets two tests automatically: one generated from the original PL/SQL control flow, and one captured from production traffic against the legacy system. The second matters more. We replay six months of real form submissions through both the old and new validators and compare outcomes. Any divergence is a defect.

On the last four projects, this replay caught between 14 and 71 defects per 1,000 triggers — almost all in the messy-case patterns above.

The takeaway

WHEN-VALIDATE-ITEM is where 25 years of institutional knowledge lives. Translating it to TypeScript is not a syntactic exercise — it’s a semantic one, and the semantics depend on Forms’ execution model as much as on the code itself. The migrations that preserve business rules cleanly are the ones that split pure validation from data access, mirror the original execution model, and replay real traffic before cutover. That mechanical preservation is the whole argument behind structured migration as a third way between manual rewrites and code translation.