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
How a package fills up
Three things have to be in place for a write to land in a package.
| Required | What |
|---|---|
Screen is change_tracked = true | A 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 application | The 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 in | The package records captured_by from the session. Anonymous writes are not tracked. |
Once those are in place, every tracked write produces one change entry:
| Operation | What's captured |
|---|---|
INSERT | new_values — the bound write columns. |
UPDATE | old_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. |
DELETE | old_values only. |
CALL_API | The resolved call parameters + the configured change_replay flag — re-issued verbatim on apply when replay = true. |
CALL_PLUGIN | Same — 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 optionalchange_entityfrom the screen (role,user,relationship,security, …). The UI groups entries by this label on the detail page; a screen with nochange_entitystill captures, just grouped byquery.entity_key— the natural key of the row (resolved from the screen'skey_columns). Drives compaction on export —INSERT then UPDATE then UPDATEon the same key collapses to one effective net entry;INSERT then DELETEcancels 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 toold_valuesfor 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 tracked | Why |
|---|---|
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
| State | Meaning |
|---|---|
DRAFT | The active package on its application. New tracked writes attach here. Entries can be excluded / re-included. |
PENDING | Submitted for approval. No new writes attach (a fresh DRAFT opens for the application on the next capture). Reviewer can approve or reject. |
APPROVED | Frozen, ready to export. Entries can no longer be excluded. |
EXPORTED | A JSON bundle has been downloaded. The package is now the source-side record of what was shipped; the bundle is what reaches the target. |
REJECTED | The 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
| Element | What |
|---|---|
| Filter box | Matches the package name or status. |
| Active package | Pre-selected on open (the draft of the application the operator last touched). Highlighted blue. |
| Package row | Name + meta line (status · captured_by · timestamp · entry_count) + a coloured status badge. |
| Status colours | DRAFT blue, PENDING orange, APPROVED green, EXPORTED grey, REJECTED red. |
| Trash icon | Deletes 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
| Element | What |
|---|---|
| Header | Package name + application + entry count + lifecycle buttons (Submit / Approve / Reject / Export) + the resolved post-apply step ids (when set on contributing screens). |
| Entries grouped by entity | Default 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 head | The label + the entry count + a short sample of the natural keys (e.g. PRJM_NEW · UDC_ADM · BUYER_R2). |
| Entry row | Operation badge + the natural key + the firing action label. Click the chevron to expand the old → new diff. |
| Diff view | Two-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 buttons | Exclude (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.
| Step | What |
|---|---|
| 1. Pick the JSON | The exported bundle. The header shows its name, application and op count once parsed. |
| 2. Dry-run | POST /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 report | Each 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 anyway | Each 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. Apply | POST /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. Confirmation | A 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:
| Op | Target row | Status |
|---|---|---|
| INSERT | absent | would_apply |
| INSERT | already exists | conflict |
| UPDATE / DELETE | equal to the captured pre-image | would_apply |
| UPDATE / DELETE | different, or missing | conflict |
| any | no read query or key captured, or unreadable | unverified — 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_replay | What 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:
- Operator creates a role
PRJM_NEW, attaches three menus, edits two existing roles' permissions. Every write lands in theJDE-PRODapplication's draft package as it happens. - Operator opens Liberty Next → Deploy → Changes, reviews the draft. The detail view shows the three roles grouped as
ROLES, the menu attachments asMENUS, the touched security asSECURITY. ACALL_PLUGINentry tonomajde.security:j_remerge_securityis also captured. - Operator excludes one entry — a typo correction they want to roll back manually instead. Submits, then Approves, then Exports.
- The browser downloads
jde-prod-security-2026-06-09.changeset.json.
On the prod instance:
- Operator opens Liberty Next → Deploy → Changes → Apply, drops the JSON in.
- Dry-run runs. The report shows 11
would_applyand 1conflicton the roleBUYER_R2(someone touched it on prod since the package was captured). Operator ticks that row to apply it anyway. - Operator clicks Apply. Every row lands; the post-apply step fires last, re-merging the security for the affected parent roles on prod.
- 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:
| Field | What |
|---|---|
change_tracked | Master toggle. When on, the screen's writes attach to the active package on its application. |
change_entity | Optional 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_apply | Ids 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:
| Field | What |
|---|---|
change_replay | When on, a CALL_API / CALL_PLUGIN invocation of this action is replayed on the target. Default off — capture-only. |
API contract
| Endpoint | Method | What |
|---|---|---|
/admin/changesets | GET | List packages. Filters: application=<id>, status=<one-of>. Superuser-only. |
/admin/changesets/<id> | GET | Package detail — header + every entry with old / new values. |
/admin/changesets/<id> | DELETE | Delete the package log only (not the underlying writes). |
/admin/changesets/<id>/submit | POST | Move DRAFT → PENDING. |
/admin/changesets/<id>/approve | POST | Move PENDING → APPROVED. |
/admin/changesets/<id>/reject | POST | Move PENDING → REJECTED. |
/admin/changesets/<id>/entries/<entry_id>/exclude | POST | Flip an entry's status to EXCLUDED. |
/admin/changesets/<id>/entries/<entry_id>/include | POST | Flip an entry's status to CAPTURED. |
/admin/changesets/<id>/export | GET | Returns the compacted JSON bundle; flips the package to EXPORTED. |
/admin/changesets/apply | POST | Body: { bundle, dry_run, force: [<indexes>] }. Returns the per-op report. |
/admin/changesets/applied | GET | The 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:
| Surface | Status |
|---|---|
| Schedule an automatic submit / approve | No. Manual lifecycle by design (an approved package goes to prod — a sign-off is the point). |
| Diff between two packages | No. Compare on the target via the Applied bundles log + the source's detail view. |
| Roll back a promoted package | No. 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 updates | No. A capture on another tab doesn't refresh this page; click Refresh (top-right) to reload. |
Common pitfalls
| Mistake | Symptom | Fix |
|---|---|---|
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
- Nomaflow → Bundled jobs → nomajde-remerge-security — the security re-merge plugin most often wired as a post-apply step on JDE promotions.
- Nomaflow → Concepts — the broader Nomaflow mental model that Change packages sits inside.
- Liberty Next → Deploy → Package — the config equivalent: ship screens / menu items / dashboards between installs. The two channels coexist.