The Low Code Factory Discuss an Engagement

Home/Case Studies/Legacy ERP forms modernization

Manufacturing In production

187 enterprise forms, compiled out of a vendor platform and into React.

Hundreds of business-critical forms were locked inside an aging, proprietary ERP forms platform. Rebuilding them by hand was estimated by the client's own developers at two days each, which made the estate a project nobody would fund. We built the compiler instead.

187
forms migrated, plus a dashboard portal
faster than the hand-migration estimate
100%
of every form captured, nothing silently dropped
01 — Context

Forms outlive the platforms they were built on

The manufacturer ran hundreds of business-critical forms on a proprietary ERP forms platform: leave requests, security entries, purchase approvals, plant operations. The forms worked, which was precisely the difficulty. They ran the business every day, they were tied to a vendor runtime the company no longer wanted to license, the interface had aged visibly, and the people who understood the platform had moved on.

A form on a platform of this kind is not a layout. It is a declarative specification spanning four planes, and a faithful modernization has to preserve all four: the models it binds to and their types, the view of controls, grids and layout that users see, the behaviour scripts that run as people interact with it, and the lifecycle actions that fire on load, save and submit.

Multiply that by several hundred screens and the problem stops being one of effort. It becomes one of consistency, because no two developers reading the same legacy form will interpret it identically.

02 — Business challenge

Every obvious approach fails, and fails expensively

  • Hand-rewriting. The client's own developers estimated two days per form. Across the estate that is a headcount decision rather than a project, and the output drifts: fields are missed, validation is interpreted differently, and the inconsistency only surfaces in testing.
  • Wrapping the old runtime. An iframe around the legacy platform preserves the licence bill, the lock-in and the dated interface. It relocates the problem rather than solving it.
  • Naive conversion. A tool that translates what it recognises and skips what it does not will produce something that renders. The gap surfaces during user acceptance testing, or in production.
  • Black-box generation. Output no developer can read or extend is simply a new kind of lock-in, swapping a runtime the client could not change for a codebase they cannot either.

The economics are what actually decide this. At two days a form, the estate represents roughly 370 developer-days of work with no new functionality at the end of it. That is a business case no sponsor approves, which is why estates like this one sit on a platform long after everybody agrees they should not.

03 — Constraints

What the environment fixed before we started

  • Nothing could be silently dropped. A form that renders but has quietly lost a validation rule is more dangerous than one that fails to convert at all, because nobody will look for the missing rule.
  • The output had to be code the client's engineers would maintain. Generated code that no developer will willingly touch is a second legacy system with a shorter fuse.
  • No vendor frontend library could survive into the output. The whole point of the exercise was to leave the platform, not to depend on it from a distance.
  • Custom business logic could not be guessed. The behaviour scripts encoded rules that existed nowhere else, and a machine translation of them that was subtly wrong would be worse than no translation at all.
  • The migration had to be re-runnable. The source estate was still live and still changing during the programme.
04 — Solution

A compiler, with two intermediate representations

The engine does not translate a form line by line. It parses it, understands it, and re-expresses it — which is the difference between a converter and a compiler, and it is the reason the output is readable.

The first stage produces a Source IR: a lossless mirror of the legacy form, keyed on the original control type. Because it is keyed on the source vocabulary rather than the target's, the engine physically cannot drop a construct it does not yet understand. Anything unrecognised is still present in the intermediate representation, still counted, and still reported.

Canonicalization passes then fold that into a Semantic IR, an idiomatic tree shaped like the target rather than the source. The passes are named for what they do: mapping one construct onto another, unwrapping needless nesting, absorbing a wrapper into the thing it wraps, and replacing a legacy idiom with the modern construct that expresses the same intent. React is generated from the Semantic IR.

All of it is governed by a typed, cascading policy ruleset the client can override. A quirk specific to one estate is handled by adding a rule, not by forking the engine, which is what stops a second client from becoming a second product.

Deterministic by design

The same form definition produces the same React on every run. That property does more work than it appears to. It means the migration can be re-run as the source evolves rather than being a one-way door, and it means a reviewer can diff two runs instead of re-reading the whole output. When the policy ruleset changes, the effect of that change is visible as a diff across every affected form at once.

The line between structure and logic

Most of a form is structural: its fields, its validation, its grids, its layout. Structure can be translated deterministically, and the engine emits it as native React using MUI, React Hook Form and typed service hooks over TanStack Query.

The custom business logic is the genuinely hard part, and it is the one place where silent automation is dangerous. So the engine draws a hard line and never executes a custom script. Each one becomes a typed stub handler in the generated component, plus read-only reference documentation placed alongside the form it belongs to. A developer finishes it by hand, with the original in front of them and the context intact.

That division is the honest one. A tool claiming fully automated conversion of custom business logic is either wrong or producing code somebody will quietly rewrite later. Capturing everything, and being precise about which part still needs a person, is what makes the numbers trustworthy.

05 — Architecture

The two-stage pipeline

Policy ruleset — typed, cascading, client-overridable map · unwrap · absorb · replace-by-intent Legacy form vendor definition parse Source IR lossless mirror keyed on control type canonicalize Semantic IR idiomatic tree shaped like the target emit MUI · React Hook Form React the client owns zero vendor runtime custom scripts legacy-reference/ typed stubs + reference docs · never executed, never guessed Migration report, per form every construct: translated to React, or explicitly recorded as preserved
The same form definition compiled twice produces the same React twice. A reviewer diffs two runs rather than re-reading the output, and a change to the policy ruleset shows its effect across every affected form at once.

The migration report

Every construct in a source form is accounted for. It is either translated into React or explicitly recorded as preserved for a developer to complete, and the report states which, per construct, per form. The count is exact.

The migration report for a single form, showing 13 of 13 constructs captured and 0 deferred, with a per-construct log of each custom handler preserved into legacy-reference as documentation for a developer to finish.
The migration report for one form: every construct captured, none deferred, and a per-construct log of exactly what became React and what was preserved as a documented handler for a developer to complete.
A React screen generated from a legacy form definition, with fields, a Material-styled layout, validation and a bound results grid.
A generated React screen, rendered from a migrated form definition — produced deterministically and identically on every run. Sample form; no customer data is shown.

This is the artifact that makes the programme governable. A programme owner can take a number to a steering committee before cutover and know it is not an estimate. Nothing disappears between the old platform and the new one without appearing in that report.

06 — Delivery approach

Built as a tool the client operates, not a service we perform

The engine could have been run by us, form by form, with the output delivered as a series of pull requests. That would have been faster to start and would have left the client dependent on us for every subsequent form, every ruleset change and every correction as the source estate continued to evolve.

Instead the engine ships as a tool their developers operate. A form is selected, compiled, and its migration report reviewed. The generated React is inspected in a live preview beside the original. The policy ruleset is theirs to extend as the estate reveals new quirks, and it is versioned and exportable rather than embedded in the engine.

The remaining custom logic is completed by a developer against the reference documentation the engine produced, which is what the second half of a day per form is actually spent on. That work is real, it is skilled, and it is deliberately left to a person.

07 — Outcome

What changed

187 forms and a dashboard portal have been migrated for the first client. Hand-migration had been estimated by their own developers at two days per form. With the engine, a developer completes two forms per day: the structural majority arrives generated, and the remaining custom logic is finished by hand in roughly half a day with the reference documentation alongside it.

Across 187 forms that is the difference between roughly 370 developer-days and roughly 95. The estate stopped being a rewrite nobody would fund and became a schedule somebody could actually run — which is the outcome that mattered, because the alternative was not a slower migration but no migration at all.

The generated application depends on React, MUI and the client's own conventions, with no vendor frontend library anywhere in the output and no runtime shim underneath it. The client owns the engine, the policy ruleset, the generated code, and the route off the platform they were leaving.

08 — Technologies

What it is built on

TypeScript React MUI React Hook Form TanStack Query Two-IR compiler pipeline

The target stack was the client's, not ours. The output had to look like code their team already writes, because the measure of the engine is whether they will maintain what comes out of it.

09 — What this demonstrates

Lessons

The decisive design choice was keying the first intermediate representation on the source vocabulary rather than the target's. It is a small decision with a large consequence: it makes silent loss structurally impossible, because a construct the engine does not understand still has somewhere to live. Every converter that quietly drops what it does not recognise made the opposite choice, usually without noticing.

The second lesson is that admitting the limit is what makes the tool trustworthy. The engine could have attempted the custom business logic and produced something plausible. It would have been wrong occasionally, in ways nobody could easily detect, and the entire output would then have been suspect. Drawing a hard line and reporting exactly what fell on the other side of it is why a programme owner can take the number to a steering committee.

The third is economic rather than technical. The engineering here did not make a possible project faster. It made an impossible project possible, by moving it from 370 developer-days to 95 and turning a business case that no sponsor would approve into one that was straightforward. That is usually where the value of a tool lies, and it is why building the compiler was cheaper than doing the work.

Stuck on a platform you have outgrown?

Tell us what is trapped inside it, how large the estate is, and where it needs to end up.

Discuss an Engagement All case studies