How to Migrate Classic Process JavaScript to the New UI
Overview
In Etendo Classic, a Defined Process (a process configured in the Application Dictionary, with or without parameters) can carry hand-written JavaScript that customizes its dialog: pre-filling fields when it opens, validating and transforming input as the user types, gating execution behind a confirmation, showing in-dialog banners, launching nested processes, or post-processing the server response. In Classic this JavaScript runs inside the SmartClient runtime.
The new Workspace UI reproduces that behavior without SmartClient. The custom JavaScript is no longer attached to a SmartClient module; instead it is stored as plain text in a set of metadata columns on the process and its parameters. At runtime the new UI compiles that text and runs it against a curated context that emulates the Classic APIs the scripts expect. This makes the capability fully metadata-driven: any process becomes scriptable simply by filling in its columns — no new code is deployed to the UI.
This guide explains how to migrate the JavaScript of a Defined Process from Classic to the new UI in your own environment. If you maintain a productive instance with custom Defined Processes whose Classic JavaScript personalizes their behavior, those scripts will not run in the new UI until they are migrated into the metadata columns. This document is the reference for that migration.
Scope and responsibilities
This guide targets developers who must migrate Classic process JavaScript without access to the new UI source code. The recommended, supported path is to follow this documentation and migrate by hand. Your tool is this documentation and the manual, self-service steps it describes. Concretely:
- Produce the migrated code by following this guide.
- Paste it into the correct metadata fields (see The metadata fields).
- Validate the result manually in the running dialog.
- If something does not work, report the problem — describe exactly what fails and where.
Background: the five Classic hook points
A Classic Defined Process can attach behavior at up to five points. The new UI maps each one to a metadata column:
| # | Classic hook | Fires when | Scope |
|---|---|---|---|
| 1 | onLoad |
The process dialog opens | Process (OBUIAPP_Process) |
| 2 | onProcess |
The user presses the execute / OK button | Process (OBUIAPP_Process) |
| 3 | onRefresh |
The dialog needs to re-pull its data (e.g. after a nested process closes) | Process (OBUIAPP_Process) |
| 4 | onChange |
A parameter's value is committed | Parameter (OBUIAPP_Parameter) |
| 5 | onGridLoad |
An embedded grid parameter finishes loading rows | Parameter (OBUIAPP_Parameter) |
Beyond these entry points, a Classic process file is a single JavaScript module: the entry points call
shared helpers, constants, and closure state declared in the same file. The new UI reproduces this with
a dedicated module-scope column (see em_etmeta_payscript_logic).
The goal of migration is behavioral parity: a migrated process must behave the same as its Classic counterpart, with the JavaScript living in metadata instead of in a SmartClient module.
What the new UI can do
This section describes the capabilities available to migrated scripts — the what, the why, and
how you use them — without going into how they are implemented internally. If a script touches
something the new UI does not implement, it throws a clear "<api> is not implemented yet" error rather
than failing silently, so gaps surface during migration instead of in production.
The execution-model contract you must respect
A few rules govern every field. A body that breaks them will not run:
- Each hook is a bare arrow-function expression. A field value must be a single arrow function such
as
async (process, view) => { … }. It must not be wrapped in an immediately-invoked function expression (IIFE, e.g.(async () => { … })()) and must not be an object literal. Anything that does not evaluate to a function is rejected at compile time. onLoad/onProcess/onChange/onGridLoadare arrow functions; module scope is different. The module-scope field (em_etmeta_payscript_logic) is a module body — a sequence of declarations ending inreturn { … };— not an arrow function. See its field description below.- Helpers resolve by bare name. Anything returned from the module-scope field is available, by its
bare name, inside all five hooks. Globals such as
OB,callAction,confirm,messageBar, andBigDecimalare likewise injected and callable directly. onChangedoes not fire during initial seeding. Just like Classic, parameteronChangeruns only on a genuine user change, never while the dialog seeds its initial/default values. Put load-time computation inem_etmeta_onload, not inem_etmeta_on_parameter_change.
Capability catalog
| Capability | What it gives migrated scripts |
|---|---|
The view object |
Mirrors the Classic OBStandardView. Read-only context (view.theForm, view.messageBar, view.getContextInfo(), view.selectedRecords, view.tabId, …) is always available; action methods (view.refresh(), view.openProcess(...), view.executeProcess(), the footer buttons, the OK button) are available inside the hooks that need them. |
form, item proxies |
view.theForm.getItem(name) and field methods: getValue() / setValue(v), show() / hide() / isVisible(), setRequired(bool) / setDisabled(bool), setTitle(text), setValueMap(map) / getValueMap(), setValueFromRecord({ id, _identifier }), and more. Numeric parameters return real numbers, so Classic comparisons keep working. |
grid proxy |
For embedded (Window-Reference / Pick&Execute) grids: selection (getSelectedRecords, selectRecord, …), row reads, edit values (setEditValue, getEditValues), visibility (show() / hide()), per-row action buttons, and runtime hooks registered from onGridLoad (onRecordChange, onSelectionToggle, setColumnOnChange, setColumnValidator, fireOnPause). Inside onGridLoad the grid is passed as the grid argument; from any other hook (onLoad, onProcess, onRefresh, a parameter's onChange) reach the same handle via view.theForm.getItem(name).canvas.viewGrid. |
Server execution — view.executeProcess() |
The new-UI equivalent of the Classic actionHandlerCall(). It builds the standard execution payload and submits it to the process's configured Java class. Use it in onProcess instead of hand-building a payload. |
| HTTP helpers | callAction(handler, payload), callDatasource(entity, payload), callServlet(path, payload) — POST to backend handlers/datasources with auth and CSRF handled automatically. |
| Modal dialogs | confirm / warn / say (and isc.confirm / ask / warn / say). Promise-based and also accept the Classic callback shape. |
| In-dialog message bar | view.messageBar.setMessage(severity, title, text, actions?) and view.messageBar.hide(), with OB.MessageBar.TYPE_* / isc.OBMessageBar.TYPE_* severities. Clickable links must be expressed as a structured actions array, not as inline <a onclick> HTML. |
| Nested processes | view.openProcess(params) (and view.standardWindow.openProcess(...)) layers another process dialog on top; the parent's onRefresh fires when the child closes. |
The OB.* shim |
A shared OB namespace mirroring the Classic globals: OB.I18N.getLabel, OB.Format.*, OB.Utilities.Number.JSToOBMasked, OB.Utilities.Action.set/execute/executeJSON, OB.PropertyStore, OB.RemoteCallManager.call (callback↔Promise adapter), OB.Datasource.create, OB.Constants, severity constants. |
Decimal arithmetic — BigDecimal |
The Classic BigDecimal is injected as a global, so money math (add, subtract, multiply, divide, compareTo, setScale, …) is migrated verbatim. Never rewrite money math with Number / parseFloat, which drifts and breaks parity with the server. |
| Action-JSON dispatcher | OB.Utilities.Action.executeJSON(actions) dispatches a backend responseActions array (message bar, toast, navigate to tab, refresh grid, download/browse report, …). |
| Custom UI component | When the process's custom-component flag is set, onLoad returns a UI schema and the dialog renders a bespoke component instead of the standard parameter form. |
What is not (yet) supported
The new UI is generic and metadata-driven, with no per-process special-casing. A handful of Classic mechanisms are either intentionally changed or not supported:
- Inline-onclick HTML in messages is not allowed. Message text is sanitized (formatting tags only);
use the structured
actionsarray for clickable affordances. - SmartClient UI primitives (
isc.ClassFactory.defineClass,isc.DynamicForm,isc.OBPopup, …) are not available. Their equivalents are the declarativegrid.setRowActions(...)andopenDynamicForm({ fields }). - Anything the platform does not implement throws
"<api> is not implemented yet". If you hit such an error during validation, that is a capability gap to report (see Reporting issues) — not something to work around in your environment.
The metadata fields (where the migrated code goes)
Seven custom columns carry the migrated code. Five belong to the process (four hooks plus one rendering flag), and two belong to each parameter. Enter the migrated JavaScript in the field corresponding to each column on the Process Definition window (and its Parameters tab) in the Application Dictionary.
| Field (column) | Entity | Hook | Code shape |
|---|---|---|---|
em_etmeta_onload |
Process | onLoad |
async (process, view) => { … } |
em_etmeta_onprocess |
Process | onProcess |
async (process, view) => { … } |
em_etmeta_on_refresh |
Process | onRefresh |
(view) => { … } |
em_etmeta_payscript_logic |
Process | shared module scope | module body ending in return { … }; |
em_etmeta_on_parameter_change |
Parameter | onChange |
(item, view, form, grid) => { … } |
em_etmeta_on_grid_load |
Parameter | onGridLoad |
(grid, view, parameters) => { … } |
em_etmeta_custom_component |
Process | rendering flag | boolean (Yes/No) |
Any field may be left empty
A process rarely uses all five hooks. Fill only the fields that correspond to hooks the Classic file actually implements; leave the rest empty. An empty field means "no script for this hook".
Below is the purpose, signature, and a simple example for each field.
1. em_etmeta_onload — process onLoad
- Entity: Process · Signature:
async (process, view) => { … } - Fires: once, when the dialog opens, after the defaults have been seeded and before the form is interactive.
- Purpose: seed or compute default values, hide or require fields, pre-select grid rows, gate the
open behind a
confirm, or (with the custom-component flag) return a UI schema. - Returns (optional): a map of
paramName: valuepairs to seed fields; a falsy return is a no-op.
async (process, view) => {
const form = view.theForm;
// Hide an advanced field unless the launching record is a sales transaction.
const ctx = view.getContextInfo();
if (ctx.inpissotrx !== 'Y') {
form.getItem('credit_to_use').hide();
}
// Seed a sensible default and make a field mandatory.
form.getItem('payment_date').setValue(ctx.inpdateordered);
form.getItem('reference_no').setRequired(true);
};
2. em_etmeta_onprocess — process onProcess
- Entity: Process · Signature:
async (process, view) => { … } - Fires: when the user presses the execute / OK button.
- Purpose: client-side validation before submit, calling the backend, and post-processing the response.
- Key rule: call
view.executeProcess()to run the process's own Java class (the equivalent of the ClassicactionHandlerCall()). To abort with an error, return{ severity: 'error', text }.
async (process, view) => {
const form = view.theForm;
const amount = form.getItem('actual_payment').getValue(); // numeric field returns a real number
if (amount <= 0) {
const text = OB.I18N.getLabel('ETP_AmountMustBePositive');
view.messageBar.setMessage(isc.OBMessageBar.TYPE_ERROR, null, text);
return { severity: 'error', text }; // aborts; the modal stays open
}
const response = await view.executeProcess(); // equivalent to actionHandlerCall()
return response && response.message;
};
Pick&Execute / Window-Reference processes behave differently
For a process whose pattern is Pick&Execute, or that has a Window-Reference grid parameter, the
platform submits the grid selection itself. There, onProcess runs as a pre-submit validation
hook: return { severity: 'error', text } to abort, or undefined to let the submit proceed —
and do not call view.executeProcess() (it would double-submit).
3. em_etmeta_on_refresh — process onRefresh
- Entity: Process · Signature:
(view) => { … } - Fires: when the dialog needs to re-pull its data — for example, when a nested process closes, the
parent's
onRefreshis invoked automatically. - Purpose: refresh grid/form data after an external change.
(view) => {
// Re-pull the embedded grid after a nested process modified the data.
view.theForm.getItem('order_invoice').canvas.viewGrid.invalidateCache();
};
4. em_etmeta_payscript_logic — shared module scope
- Entity: Process · Shape: a module body (declarations and helper functions) that ends with
return { … };— not an arrow function. - Purpose: hold everything the Classic file declared at module level — shared helpers, constants, and closure state — that the entry points reference by bare name. It is evaluated once per dialog open, and whatever it returns is made available inside all five hooks.
const PAID_FULLY = 'PPM';
function remaining(form) {
const total = new BigDecimal(String(form.getItem('total').getValue()));
const paid = new BigDecimal(String(form.getItem('actual_payment').getValue()));
return total.subtract(paid);
}
return { PAID_FULLY, remaining };
Namespaced self-registration
If the Classic file registers a namespace such as OB.APRM.AddPayment = { … }, do the same
inside this module body; the OB shim tolerates OB.<Module>.<Process> writes.
5. em_etmeta_on_parameter_change — parameter onChange
- Entity: Parameter (one per parameter that has an onChange) · Signature:
(item, view, form, grid) => { … }—itemis the changed field;gridisnullfor scalar parameters. - Fires: when the parameter's value is committed by the user (never during initial seeding).
- Purpose: react to a value change — recompute dependent fields, toggle required/disabled, refresh a value map, show a banner.
(item, view, form) => {
// When the document type changes, recompute the suggested amount.
const isCredit = item.getValue() === 'CR';
form.getItem('credit_amount').setDisabled(!isCredit);
if (!isCredit) {
form.getItem('credit_amount').setValue(0);
}
};
6. em_etmeta_on_grid_load — parameter onGridLoad
- Entity: Parameter (the grid parameter) · Signature:
(grid, view, parameters) => { … } - Fires: each time the embedded grid receives a delivered datasource result (including an empty result, exactly once).
- Purpose: post-process loaded rows (default selection, per-row components, derived columns), register per-column edit hooks/validators, or react to an empty grid (e.g. an info banner when no rows match).
(grid, view) => {
if (grid.getData().getLength() === 0) {
view.messageBar.setMessage(isc.OBMessageBar.TYPE_INFO, null,
OB.I18N.getLabel('ETP_NoOutstandingDocuments'));
return;
}
// Recompute a total whenever an editable amount cell changes.
grid.onRecordChange((record, changes) => {
// …re-sum settlement amounts over the selected rows…
});
};
7. em_etmeta_custom_component — custom-component flag
- Entity: Process · Type: boolean (Yes/No).
- Purpose: selects how the process dialog is rendered:
- No (false) — the normal case. The dialog renders the standard metadata-driven form: the
process's parameters, their metadata, and the migrated JavaScript hooks (
onLoad,onProcess,onChange, …) described in this guide. This is what you use for virtually every migration. - Yes (true) — a bespoke UI. The dialog does not render the standard parameter form.
Instead, the new UI must contain a dedicated custom component built specifically for that
process, and
onLoadreturns the schema that drives it. This mirrors Classic, where such a process also has its own hand-built UI rather than a generic parameter dialog. It is reserved for the rare process whose dialog is not a flat list of fields (e.g. a specialized picker).
- No (false) — the normal case. The dialog renders the standard metadata-driven form: the
process's parameters, their metadata, and the migrated JavaScript hooks (
A custom component requires platform team involvement
Setting this flag to Yes is not a migration you can complete by filling in fields. A custom component is real code that must be added to the new UI for that specific process — and you do not have access to the new UI source code. If a process genuinely needs a custom component, contact the platform team to have it built. For all other processes, leave this flag as No and migrate using the standard fields above.
Migrating with the "New UI Migrations" Team
You can migrate every process by hand following this guide. The "New UI Migrations" Team is an optional tool that the Etendo team built and used internally to accelerate the work; its use is not supported. It translates a Classic Defined-Process JavaScript file into the metadata columns described above, using its migration agents. Download it from Downloads and load its files into your AI assistant. It was used with Claude Code, but the agents and skill are plain Markdown that work with any AI assistant. Interact with it in Spanish (the first message at least) as shown in the steps below.
Downloads
The migration agents and the architecture & migration skill are available as a single download:
Download the migration agents and skill (.zip)
The zip contains four Markdown files:
faro.md— the coordinator agent, which validates the request and dispatches the work.babel.md— the migrator/developer agent, which analyzes the Classic JavaScript and generates the migrated code per metadata column.sello.md— the QA agent, which guides manual validation against a test checklist.new-ui-js-migration-guide.md— the skill: the authoritative architecture & migration reference. It is the source of truth for every supported API, signature, and migration rule.
Load these Markdown files into your AI assistant. Internally the Etendo team used Claude Code but the files are tool-agnostic, so they work with any AI assistant.
The agent tooling is not supported
Use of the "New UI Migrations" Team is optional and not supported. No support is given for problems arising from using the agents themselves. Platform capability gaps and substrate bugs are still reported and supported as usual (see Reporting issues).
Migrating by hand
Steps 4 to 6 apply to you too: substitute your own migrated bodies for the team's labeled code blocks, and derive your own manual test plan in place of the team's checklist.
What the team does — and does not do
The team does: read the Classic .js file and the process metadata; check feasibility against the
architecture & migration skill (see Downloads); generate the migrated code for each column
as a bare arrow function; self-check
that each body compiles; and hand you a per-column output ready to paste, along with a manual-test
checklist.
The team never: pastes the code into the UI for you (you do that); changes application source code; commits or pushes anything; or invents platform support that does not exist. If a process uses something the new UI cannot do, the team stops and reports the gap instead of producing code that looks migrated but breaks.
Validate before considering the migration done
The team's output is a draft to verify, not a finished result. It can misread an edge case or a Classic idiom. Always run the manual checklist before considering the migration done, and report anything that fails.
Step 1 — Identify the inputs
Gather two inputs:
- The path to the Classic
.jsfile of the Defined Process (the file that holds its custom JavaScript). - The process id — the 32-character
obuiapp_process_idof the Defined Process. Read it from the Process Definition window's record, or from the URL/help of that record.
Step 2 — Engage the team
The team's migration agent only acts when your message matches its activation template exactly (the quotes may be single or double). Send it, in Spanish, the file path and the process id:
Quiero migrar el javascript del proceso definido. El path del archivo original es '<path>' y el id del proceso es '<process-id>'.
For example:
Quiero migrar el javascript del proceso definido. El path del archivo original es 'modules/com.example.payments/web/js/ob-myprocess.js' y el id del proceso es '9BED7889E1034FE68BD85D5D16857320'.
If the message does not match the template, the team will not act — it will restate the template you must use.
Step 3 — Review the team's output
The team returns, in Spanish for the deliverables:
- A coverage report: a table classifying every Classic API the file uses as supported, best-effort, or unsupported. If anything is unsupported, the team stops here and explains exactly what the new UI lacks — that is a gap to report, not to work around.
- The migrated code per column, each in its own code block, clearly labeled with the column it goes into. Columns with no code are marked "LEAVE EMPTY".
- A list of advisories (dead code dropped, cloning notes, semantic differences).
- A manual-test checklist — the concrete steps you must run to confirm parity.
Read the coverage report and advisories before pasting anything. They tell you what to expect and what to test.
Steps 4 to 6 apply either way
Whether you used the team or migrated by hand, follow steps 4 to 6 as written: paste your migrated code, validate it manually, and report any problem.
Step 4 — Paste the code into the fields
For each labeled block, copy the body into the matching field on the Process Definition window (or its Parameters tab), following The metadata fields. Paste only the migrated code — the team's output or your own — exactly as intended, into the indicated column. Save the record.
Match the field to the column
Pasting an onProcess body into the onLoad field (or a parameter hook into the wrong parameter)
will produce wrong behavior that is hard to diagnose. Double-check each block's label against the
field you are editing.
Step 5 — Validate manually
Open the process dialog in the new UI and run every item in your manual-test plan (the team's checklist,
or your own plan if you migrated by hand): confirm the dialog opens correctly, that onLoad seeding/hiding
happened, that each onChange reacts as in Classic, that grids load and validate as expected, and that
execution returns the same result as Classic. Compare side-by-side with the Classic dialog where possible.
Step 6 — Report problems (do not fix)
If a test fails — a hook does not fire, a value is wrong, or you see a "… is not implemented yet"
error — report it. Describe the symptom precisely (which process, which hook, what you did, what you
expected, what happened) and include any error text. If you engaged the "New UI Migrations" Team, tell it
"el test X falló: <síntoma>" and it will re-analyze and re-emit the corrected field(s) for that
process.
Do not attempt to patch the new UI in your environment. Capability gaps and substrate bugs are resolved by the platform team; your contribution is a clear, reproducible report.
Reporting issues
Because you do not have access to the new UI source code, a good report is the most valuable thing you can produce when a migration does not behave like Classic. Include:
- The process — name and
obuiapp_process_id. - The hook / field involved (
onLoad,onProcess, a specific parameter'sonChange, etc.). - Steps to reproduce — exactly what you did in the dialog.
- Expected vs actual — what Classic does vs what the new UI does.
- Any error text shown in the dialog or the browser console (especially
"<api> is not implemented yet", which names the missing capability). - The relevant migrated body you pasted, and the coverage report if you engaged the "New UI Migrations" Team.
This work is licensed under CC BY-SA 2.5 ES by Futit Services S.L.