Skip to main content

Change packages

The Change packages page captures every tracked data write into a grouped, reviewable, promotable set. The use case the feature was built around is promoting JD Edwards security from a development environment to production — copy a role, attach a few menus, save → the writes land in production tables on dev, AND in a change package the operator can review, approve and ship to prod as a JSON bundle.

The same pattern fits every other "make changes here, replay them there" workflow: a curated UDC catalogue refresh, a batch of new permissions on a custom screen, a routing-rule update on a configuration table. As soon as a screen is change_tracked, its inserts / updates / deletes land in the active draft package alongside the production write.

Behind the page, the framework keeps every tracked write in its own database — the framework's, separate from the audit mirror —, and the apply path replays them on the target with pre-image drift detection.

The Package page carries configuration slices (screens, menu items, dashboards…); Change packages carry data. The two sit side by side under Liberty Next → Deploy.


Opening the page​

  • Liberty Next → Deploy → Changes.
  • From the Liberty Next → Build → Connectors page, Track changes in the screen card opens the page filtered on the matching application.

At a glance​

Nomaflow · ChangesChangesApply🔍 Filter packages…JDE-PROD · Security 2026-06-09submitted · franck · 09/06 09:42 · 12 entriesPENDINGJDE-PROD · UDC refreshdraft · franck · 09/06 08:15 · 4 entriesDRAFTJDE-PROD · Permissions Q2approved · valeria · 06/06 17:10 · 47 entriesAPPROVEDJDE-PROD · Permissions Q1exported · valeria · 04/06 12:01 · 23 entriesEXPORTEDSecurity 2026-06-09application = JDE-PROD · 12 entriesApproveRejectExport▾ ROLES (3)PRJM_NEW · UDC_ADM · BUYER_R2PRJM_NEW · INSERT · src: import securityINSERTRLDF · ∅ → PRJM_NEW | RLDC · ∅ → 1 | +6 more▾ RELATIONSHIPS (5)F95921 · F00926RLDF=PRJM_NEW · YRLE=A03 · INSERTINSERT▾ SECURITY (4)F00950RLUSER=PRJM_NEW · RLAT1=8 · UPDATEUPDATE▾ ACTION REPLAYS (1)call_plugin · nomajde.security:j_remerge_securityCALL_PLUGIN · child_role=PRJM_NEW · replay = trueCALL_PLUGINPost-apply: 1 step · nomajde-remerge-security (runs once on the target after every captured row lands)

How a package fills up​

Three things have to be in place for a write to land in a package.

RequiredWhat
Screen is change_tracked = trueA flag in the screen's General tab (or its TOML). Without it, writes go to the underlying table only — no capture.
An active draft exists for the applicationThe screen's connector → its application. The framework keeps at most one DRAFT per application (enforced by a partial unique index). The first tracked write on that application creates one if none exists.
The user is signed inThe package records captured_by from the session. Anonymous writes are not tracked.

Once those are in place, every tracked write produces one change entry:

OperationWhat's captured
INSERTnew_values — the bound write columns.
UPDATEold_values (the pre-image fetched through the screen's read_query before the write) + new_values. The pre-image is what drives drift detection on apply.
DELETEold_values only.
CALL_APIThe resolved call parameters + the configured change_replay flag — re-issued verbatim on apply when replay = true.
CALL_PLUGINSame — the plugin id + resolved kwargs, gated by change_replay.

Each entry also records:

  • connector + query — the framework can route the replay to the matching connector on the target.
  • entity — the optional change_entity from the screen (role, user, relationship, security, …). The UI groups entries by this label on the detail page; a screen with no change_entity still captures, just grouped by query.
  • entity_key — the natural key of the row (resolved from the screen's key_columns). Drives compaction on export — INSERT then UPDATE then UPDATE on the same key collapses to one effective net entry; INSERT then DELETE cancels out entirely.
  • source_action — the label of the action that triggered the write, threaded from the firing context. Lets the detail view group a batch as "imported by import security" instead of an unattributed wall of inserts.
  • read_query — the screen's read query, used by the apply path to fetch the current target row and compare to old_values for drift detection.
  • seq — monotonic per package, so replay preserves the capture order. A role has to exist before it can be referenced; the seq guarantees that.

What change_tracked does NOT track​

Not trackedWhy
Writes from a screen with change_tracked = false.Default. Audit-only tables (notification log, retry stats) shouldn't pollute promotion bundles.
Writes from a Nomaflow job, a bulk import, the CLI.Tracking is per-screen and runs through the screen's write path. Jobs use j_* callables directly against the connector.
Reads — no SELECT is ever captured.The package is a change log.
The framework's own metadata writes (jobs, users, the change packages themselves).The capture loop checks change_tracked on the connector's screen, not on the table.

The lifecycle​

A package walks through these states:

DRAFT ─── submit ───▶ PENDING ─── approve ──▶ APPROVED ─── export ──▶ EXPORTED
│
└── reject ──▶ REJECTED
StateMeaning
DRAFTThe active package on its application. New tracked writes attach here. Entries can be excluded / re-included.
PENDINGSubmitted for approval. No new writes attach (a fresh DRAFT opens for the application on the next capture). Reviewer can approve or reject.
APPROVEDFrozen, ready to export. Entries can no longer be excluded.
EXPORTEDA JSON bundle has been downloaded. The package is now the source-side record of what was shipped; the bundle is what reaches the target.
REJECTEDThe reviewer turned it down. It stays rejected; the next tracked write on the application opens a new draft.

The four lifecycle buttons on the detail header — Submit, Approve, Reject, Export — fire POST /admin/changesets/<id>/submit (or …/approve, …/reject, …/export). Change packages are reserved to superusers.

Per-entry: exclude / re-include​

While the package is DRAFT, every captured entry has an Exclude button. Excluded entries are not removed (no audit loss); they're flagged status = excluded and skipped on compaction + export. Re-including is one click. Useful for cherry-picking: capture a wide set of changes during the day, exclude the experiment that didn't pan out, ship the rest.

Compaction is computed at export time, not on capture — exclusions take effect immediately the moment the bundle is built.


The Changes tab — review and act​

The page has two tabs: Changes (the package catalogue) and Apply (covered below).

Left column — the package list​

ElementWhat
Filter boxMatches the package name or status.
Active packagePre-selected on open (the draft of the application the operator last touched). Highlighted blue.
Package rowName + meta line (status · captured_by · timestamp · entry_count) + a coloured status badge.
Status coloursDRAFT blue, PENDING orange, APPROVED green, EXPORTED grey, REJECTED red.
Trash iconDeletes the package log — the rows already written to the underlying tables are NOT affected; only the change-package record is removed. Confirmed via the themed dialog.

Right column — package detail​

ElementWhat
HeaderPackage name + application + entry count + lifecycle buttons (Submit / Approve / Reject / Export) + the resolved post-apply step ids (when set on contributing screens).
Entries grouped by entityDefault groups: ROLES, USERS, RELATIONSHIPS, SECURITY, MENUS, ACTION REPLAYS (the CALL_API / CALL_PLUGIN entries) — the group label comes from each entry's change_entity. Click the group head to fold / unfold.
Group headThe label + the entry count + a short sample of the natural keys (e.g. PRJM_NEW · UDC_ADM · BUYER_R2).
Entry rowOperation badge + the natural key + the firing action label. Click the chevron to expand the old → new diff.
Diff viewTwo-column field / value table with three tones: red strikethrough = old, green = new, secondary = unchanged. A small Show unchanged toggle reveals the noise of untouched columns when reviewing a renamed-but-not-changed entry.
Per-entry buttonsExclude (only on DRAFT packages) flips the entry to EXCLUDED and dims it. Click again to re-include.

Post-apply steps on the header​

A screen can name post-apply steps (Screen.post_apply): actions to run once on the target after the package's rows have landed — re-merge a security model, refresh a materialised view. They come from a library of post-apply steps shared by the application, each step being a plugin call (call_plugin), an API endpoint (call_api) or a query (run_query). Their ids accumulate on the package header, and on export they are appended to the bundle as final operations.


The Apply tab — bring a bundle in​

Run this on the target install (typically prod) to bring a bundle exported from the source (typically dev).

The apply flow is two-pass: dry-run first to see the per-op drift report, then real apply, optionally with force ticked on rows you want to override.

StepWhat
1. Pick the JSONThe exported bundle. The header shows its name, application and op count once parsed.
2. Dry-runPOST /admin/changesets/apply { bundle, dry_run: true }. The server walks every op, fetches the current target row via the captured read_query, compares to the captured old_values, and tags the op with a status. No write happens.
3. Read the reportEach op shows its status badge: would_apply (clean), conflict (the target differs from the captured state — see below), would_force (a conflict the operator chose to apply), unverified (no drift check possible), skipped (an action captured for review only), error. The summary line totals each status.
4. Tick what to apply anywayEach conflict op — and each unverified op, whose drift cannot be checked because the screen has no read query — has a force checkbox. Ticking it applies the op anyway (it then reads would_force); an unticked one is not applied.
5. ApplyPOST /admin/changesets/apply { bundle, dry_run: false, force: [<indexes>] }. Each op runs in its own transaction; per-op rowcounts + status come back in the report.
6. ConfirmationA row lands in the target's import log (top of the Apply tab), with the bundle's checksum. The source package stays EXPORTED.

Promote to another instance​

An approved — or already exported — package can go straight to another instance, without handling the JSON: Promote to… in the package's header opens the promotion window. Pick the Target and an optional Comment: the target runs the dry-run at once and returns its drift report, with the same force checkboxes; Apply on <target> applies it there and marks the package exported. The targets are declared under Package → Promotion — see Packages → Promotion; the promotions applied are listed in its history.

Drift detection — what it compares​

For every row op, the apply path reads the target's current row with the captured read_query and key, then:

OpTarget rowStatus
INSERTabsentwould_apply
INSERTalready existsconflict
UPDATE / DELETEequal to the captured pre-imagewould_apply
UPDATE / DELETEdifferent, or missingconflict
anyno read query or key captured, or unreadableunverified — applied without check
CALL_API / CALL_PLUGIN—unverified — re-run on apply; skipped when the action was captured without change_replay
post-apply step—unverified — always runs, after the change ops

A conflict is applied only when its checkbox is ticked. An op whose write raises is reported error with the database message.

Re-applying the same bundle​

The import log uses the bundle's checksum (a SHA-256 of its ops) as the identity. Re-applying the same file is detected before any write runs — the dry-run report carries an already_applied block with the previous applied_at / applied_by. The operator can either confirm and re-apply (rows land again where the target matches the pre-image; the others come back as conflicts) or close the file.

Action replay (CALL_API / CALL_PLUGIN)​

A change-tracked screen captures every API / plugin call it fires (so the package shows it for review). Whether the call is replayed on the target is gated by the action's change_replay flag:

change_replayWhat gets replayed on the target
true (opt-in)The action is re-issued verbatim with the captured resolved params. Used for server-side merge plugins like nomajde.security:j_remerge_security — re-merging the security on the target is correct after the row writes land.
false (default)The capture is shown in the package for review but not re-fired. Used for one-off side effects that shouldn't run twice (email sends, external webhook).

A replayed action runs in the same transaction as the surrounding row writes when the dialect allows it; in practice this means a successful security-merge plugin run lands atomically with the role / assignment writes that triggered it.


Promoting JD Edwards security — the canonical use case​

The motivating workflow for this feature. On the dev instance:

  1. Operator creates a role PRJM_NEW, attaches three menus, edits two existing roles' permissions. Every write lands in the JDE-PROD application's draft package as it happens.
  2. Operator opens Liberty Next → Deploy → Changes, reviews the draft. The detail view shows the three roles grouped as ROLES, the menu attachments as MENUS, the touched security as SECURITY. A CALL_PLUGIN entry to nomajde.security:j_remerge_security is also captured.
  3. Operator excludes one entry — a typo correction they want to roll back manually instead. Submits, then Approves, then Exports.
  4. The browser downloads jde-prod-security-2026-06-09.changeset.json.

On the prod instance:

  1. Operator opens Liberty Next → Deploy → Changes → Apply, drops the JSON in.
  2. Dry-run runs. The report shows 11 would_apply and 1 conflict on the role BUYER_R2 (someone touched it on prod since the package was captured). Operator ticks that row to apply it anyway.
  3. Operator clicks Apply. Every row lands; the post-apply step fires last, re-merging the security for the affected parent roles on prod.
  4. The prod-side import log records the bundle with its checksum.

For the deep reference of the merge SQL the post-apply step runs, see Nomaflow → Bundled jobs → nomajde-remerge-security.


Configuring a screen for change tracking​

The screen's General tab carries three toggles:

FieldWhat
change_trackedMaster toggle. When on, the screen's writes attach to the active package on its application.
change_entityOptional label that groups entries on the detail page. Free text — role, user, relationship, security are conventions. Empty falls back to the query name.
Screen.post_applyIds of post-apply steps (from the post-apply library) to run once on the target after the package's rows have landed. Use sparingly — these are the cross-cutting "now re-merge security" / "now refresh materialised views" steps.

For an action embedded in the screen, an additional toggle on the action's General tab:

FieldWhat
change_replayWhen on, a CALL_API / CALL_PLUGIN invocation of this action is replayed on the target. Default off — capture-only.

API contract​

EndpointMethodWhat
/admin/changesetsGETList packages. Filters: application=<id>, status=<one-of>. Superuser-only.
/admin/changesets/<id>GETPackage detail — header + every entry with old / new values.
/admin/changesets/<id>DELETEDelete the package log only (not the underlying writes).
/admin/changesets/<id>/submitPOSTMove DRAFT → PENDING.
/admin/changesets/<id>/approvePOSTMove PENDING → APPROVED.
/admin/changesets/<id>/rejectPOSTMove PENDING → REJECTED.
/admin/changesets/<id>/entries/<entry_id>/excludePOSTFlip an entry's status to EXCLUDED.
/admin/changesets/<id>/entries/<entry_id>/includePOSTFlip an entry's status to CAPTURED.
/admin/changesets/<id>/exportGETReturns the compacted JSON bundle; flips the package to EXPORTED.
/admin/changesets/applyPOSTBody: { bundle, dry_run, force: [<indexes>] }. Returns the per-op report.
/admin/changesets/appliedGETThe target's import log (every applied bundle).

The bundle shape:

{
"format": "liberty-changeset/1",
"package": { "id": "...", "name": "...", "application": "JDE-PROD", "approved_by": "...", "approved_at": "..." },
"op_count": 12,
"ops": [
{ "seq": 1, "connector": "jdedwards", "query": "roles", "read_query": "roles_by_key",
"operation": "INSERT", "entity": "role", "entity_key": {"RLDF": "PRJM_NEW"},
"new_values": {"RLDF": "PRJM_NEW", "RLDC": "1", ...},
"source_action": "import security" }
],
"checksum": "9f2c…"
}

The format is stable across framework upgrades — a bundle built on 2026.06 applies cleanly on 2026.07. The format field names the bundle version (liberty-changeset/1); post-apply steps travel as the last entries of ops.


What's NOT in this page​

The framework does ship the engine for these but the UI today is intentionally bare:

SurfaceStatus
Schedule an automatic submit / approveNo. Manual lifecycle by design (an approved package goes to prod — a sign-off is the point).
Diff between two packagesNo. Compare on the target via the Applied bundles log + the source's detail view.
Roll back a promoted packageNo. The framework records the apply but doesn't synthesise a reverse bundle. To undo, capture a new package on the source that reverts the rows + promote it.
Live multi-tab updatesNo. A capture on another tab doesn't refresh this page; click Refresh (top-right) to reload.

Common pitfalls​

MistakeSymptomFix
Screen wasn't marked change_tracked.Writes go through; no entries appear in the package.Open the screen's General tab, toggle on, save. The change applies to subsequent writes only — earlier writes don't backfill.
Two operators have a DRAFT package open on the same application.At most one DRAFT exists at a time; the second operator's writes attach to the same package.Coordinate. The package's created_by records the original author; subsequent captures stamp captured_by on each entry, so attribution survives.
Bundle applied on the wrong target (staging instead of prod).Apply lands; rows exist on staging.Capture a reverse package on staging, promote to prod — the framework doesn't synthesise this automatically. Workflow discipline matters more than tooling here.
conflict on every UPDATE in the dry-run.The target's pre-image diverged broadly — typically because the source and target weren't in sync to begin with.Audit the divergence first. Forcing everything propagates the source's state wholesale; sometimes that's right, sometimes the source is the one that's stale.
CALL_PLUGIN captured but didn't fire on apply.change_replay is off on the action.Open the action's General tab, toggle change_replay on, save. Future captures will replay; past ones in already-exported packages stay as-is.
Post-apply step never ran.The contributing screen's Screen.post_apply field is empty.Add the step's id to the screen's post_apply. New captures accumulate it on the package header; existing packages are not back-filled.
Bundle re-applied accidentally.Dry-run shows already_applied with the previous timestamp.Read the existing apply's report. Re-applying is allowed and idempotent on clean rows; only do it intentionally.

What's next​