xmatix migrate compares the configuration checked into your repository with the live tenant (migrate plan) and then pushes the repository's files to that tenant (migrate apply). It is the last step of a change that started with a snapshot export or a pull, was edited and reviewed in git, and now has to reach the tenant. The CI recipes build pipeline jobs on top of it.
xmatix migrate plan # writes xmatix/.snapshot/migrate-plan.json
xmatix migrate plan --keep-server-snapshot
jq '.counts' xmatix/.snapshot/migrate-plan.json
xmatix migrate apply --dry-run
xmatix migrate apply
Overview
The two commands are independent:
migrate planreads only. It exports the tenant into a separate folder, compares that export file by file with your workspace, and writes a manifest that lists every file asadded,modified,removedorunchanged. It changes nothing on the tenant.migrate applywrites. It runssnapshot import, which pushes every artifact file in the workspace's canonical folders. It doesn't read the plan, so it pushes unchanged files too.
The plan is a review document. You read it, or assert on it in a pipeline, before you decide to apply.
In the current release, migrate plan stops every time with "Server snapshot export failed; aborting plan." The plan runs a full snapshot export, and the export's scripts step always fails (see Snapshots). Instead, compare the repository with the tenant by exporting into a clean checkout and reviewing git diff, as in Snapshots, Step 2. migrate apply is not affected.
When to use it
- Before a promotion, to see what would reach the tenant and to spot changes someone made in the product that your repository doesn't have.
- As a drift check, to find out whether the tenant still matches the commit you last applied. Leave out the files that change on every export (see Common problems).
- To push a reviewed change from a workspace to the tenant that workspace points at.
Don't use migrate apply to copy scripts or workflows into a different tenant. See Can I promote from one tenant to another?
How xMatix handles it
What migrate plan does
- It creates a separate workspace and copies your
.xmatix/config.jsoninto it, so the export reaches the same tenant with the same sign-in. By default this workspace is a newxmatix-snapshot-<id>folder in the operating system's temp folder. With--keep-server-snapshotit isxmatix/.snapshot/server/in your workspace, and any copy kept by an earlier plan is deleted first. - It runs
xmatix snapshot export --scope <scope>in that workspace. The export's normal output is hidden; only its errors are shown. If the export fails, the plan prints "Server snapshot export failed; aborting plan." and exits 1. The plan passes no--skip, so the export always includes the scripts step, which fails in the current release. The plan therefore never gets past this step. - It compares six folders in both trees:
xmatix/metadata,xmatix/automation/scripts,xmatix/automation/rules,xmatix/automation/workflows,xmatix/analytics/dashboardsandxmatix/analytics/reports. Every file in them counts, including each script's.meta.json. Each file is identified by its path and hashed with SHA-256. - It writes the manifest, prints a table of counts and exits 0.
| Status | Meaning |
|---|---|
added | The file exists only in your workspace |
modified | The file exists in both, and the hashes differ |
removed | The file exists only in the tenant's export |
unchanged | The file exists in both, and the hashes match |
The plan compares exact file contents, not meaning. Two files that hold the same configuration still show as modified if any byte differs, such as the export time in the metadata file.
The temp-folder export is not deleted after the plan. Clear old xmatix-snapshot-* folders from the temp folder now and then.
What migrate apply does
migrate apply runs xmatix snapshot import in the current folder and returns its exit code. It passes --skip and --dry-run through, and nothing else. The import pushes files in this order, continuing past failures:
- every
*.jsonfile underxmatix/metadata/, withmetadata push; - every
*.csxfile underxmatix/automation/scripts/, withscripts push; - every
*.jsonfile underxmatix/automation/rules/, withrules push; - every
*.jsonfile underxmatix/automation/workflows/, withworkflows push; - every
*.jsonfile underxmatix/analytics/dashboards/andxmatix/analytics/reports/, withdashboards pushandreports push.
What each push does with an added or modified file differs by kind:
| Kind | modified file | added file |
|---|---|---|
| Metadata | Imported as the file says. A full-mode file also deletes metadata it doesn't contain (see Business rules) | Imported |
| Scripts | A new version is saved but not made active | Refused: a script can be pushed only with the .meta.json that a pull writes |
| Rules | Updated by entity and rule name. The file's actions replace the server's, after a confirmation when the rule already has actions | Created by entity and rule name |
| Workflows | Updated by definition id, keeping the published state in the file | A new workflow is created, as a draft unless the file says it is published |
| Dashboards, reports | The push fails in the current release | The push fails in the current release |
Nothing on the server is deleted for a removed file. The exception is metadata, where a full-mode file prunes.
Prerequisites
- The CLI installed and a workspace created with
xmatix init. xmatix logincompleted for the tenant in.xmatix/config.json. Check it withxmatix scripts list. Don't usexmatix whoamiorxmatix doctorfor this, because both fail in the current release (see CLI authentication).- A committed
xmatix/tree, normally written byxmatix snapshot export --skip dashboards,reportsfrom the same tenant. xmatix/.snapshot/in the repository's.gitignore. The plan and the kept server copy are written there, andxmatix initdoesn't ignore that folder.- The access that each kind of push needs (see Permissions).
Procedure
Step 1 — Run the plan
From anywhere inside the workspace, run:
xmatix migrate plan --keep-server-snapshot
In the current release this command stops with "Server snapshot export failed; aborting plan." (see Overview). Use the export-and-diff comparison in Snapshots instead, then continue with Step 3. The rest of this step and Step 2 describe what the plan reports when its export succeeds.
The CLI exports the tenant into xmatix/.snapshot/server/, then prints a table with four rows: added (local only — push will create), modified (will overwrite server), removed (server only — push will NOT delete) and unchanged. The last line names the plan file. The "push will create" wording is not true for scripts, as the table above shows.
Use --out <path> to write the plan somewhere else, and --scope <scope> to export a different metadata scope. Keep the default scope, TenantOwned, unless your repository was exported with another one.
Step 2 — Read the plan
List the files that differ:
jq -r '.entries[] | select(.Status != "unchanged") | "\(.Status) \(.RelPath)"' xmatix/.snapshot/migrate-plan.json
Expect xmatix/metadata/tenant-owned.json and every script's .meta.json to be modified on every run, because each export stamps them with the time. For any other modified or removed file, open both copies and compare them:
diff xmatix/automation/rules/Order/CreditCheck.json \
xmatix/.snapshot/server/xmatix/automation/rules/Order/CreditCheck.json
To compare the metadata without the export time, compare only its tables:
diff <(jq -S .tables xmatix/metadata/tenant-owned.json) \
<(jq -S .tables xmatix/.snapshot/server/xmatix/metadata/tenant-owned.json)
Decide what to do with each difference:
- A
modifiedorremovedfile that you didn't change in the repository was changed in the product. Export it into the repository (xmatix snapshot export --skip scripts,dashboards,reports, thenxmatix scripts pull --all), review it and commit it before you apply. Otherwise the apply overwrites that change. - A
removedfile that you deleted on purpose stays on the tenant. Delete it in the product if it should go. - An
addedscript can't be pushed. Create the script in the product, pull it, and copy your code into the pulled file.
Step 3 — Prepare the metadata file
The metadata file that snapshot export and metadata pull write is a full pack. Pushing a full pack deletes metadata the file doesn't contain, including the tenant's standard metadata. Before an apply, do one of the following:
- set the file's
modetodelta, for example withjq '.mode = "delta"', or - leave metadata out with
--skip metadataand push it yourself as a delta pack.
See What push does.
Step 4 — Preview the apply
xmatix migrate apply --skip dashboards,reports --dry-run
For every file, the CLI prints a would run: line with the push command it would run, grouped by folder. Folders that are missing or empty are reported as skipped. Nothing is sent to the tenant. Check that the paths belong to the workspace you expect.
Step 5 — Apply
Run the apply in a terminal, so that you can answer the confirmations:
xmatix migrate apply --skip dashboards,reports
metadata pushshows the file and its size and asks Continue? for each metadata file.rules pushasks before it replaces the actions of a rule that already has actions on the server.- Each push prints its own result. The run ends with Snapshot import complete., or Snapshot import finished with N failure(s). and exit code 1.
In a pipeline nobody can answer the confirmations, so push each kind with its own flags instead. See CI recipes.
Step 6 — Finish the promotion
- Activate the new script versions. The apply saves them without making them active. Activate each one in the product, or push it again with
xmatix scripts push <file> --activate, which saves one more version and makes it active. - If the apply created any workflows, run
xmatix workflows pulland commit the result. Push doesn't write the new definition id back into your file, so another apply would create the workflow again. - Run
xmatix migrate planagain. Scripts, rules and workflows that you pushed now show asunchanged, apart from the files described in Step 2.
Fields
The plan file, xmatix/.snapshot/migrate-plan.json:
| Field | What it means | Notes |
|---|---|---|
generatedUtc | When the plan was made | UTC |
workspaceRoot | The full path of the workspace that was compared | |
counts.added | Files only in your workspace | Includes every analytics file, because dashboards and reports can't be pulled in the current release |
counts.modified | Files whose hashes differ | Always includes the metadata file and every .meta.json |
counts.removed | Files only in the tenant's export | Apply never deletes these |
counts.unchanged | Files that match | |
entries[].RelPath | The file's path from the workspace root, such as xmatix/automation/rules/Order/CreditCheck.json | The entry fields start with a capital letter. The counts fields don't |
entries[].Status | added, modified, removed or unchanged | |
entries[].LocalHash, entries[].ServerHash | The SHA-256 hash of each side's file | Empty on the side where the file is missing |
Options:
| Command | Option | What it does |
|---|---|---|
migrate plan | --out <path> | Writes the plan to this path instead of xmatix/.snapshot/migrate-plan.json |
migrate plan | --keep-server-snapshot | Keeps the tenant's export under xmatix/.snapshot/server/, replacing an earlier one |
migrate plan | --scope <scope> | The metadata scope to export. Default TenantOwned |
migrate apply | --skip <kinds> | Comma-separated kinds to leave out: metadata, scripts, rules, workflows, dashboards, reports. Any other word is ignored |
migrate apply | --dry-run | Prints each push it would run and contacts nothing |
Exit codes:
| Command | Exit code |
|---|---|
migrate plan | 0 when the plan is written, whatever it found. 1 when the export fails, including when any single pull fails |
migrate apply | 0 when every push succeeds or there was nothing to push, 1 when any push fails. Always 0 with --dry-run |
Business rules
- The plan never changes the tenant or your workspace's artifact files. It writes only the plan file and, with
--keep-server-snapshot, the server copy. - The plan always compares all six folders. It has no
--skip, so the analytics folders are always part of it. - The metadata file's name depends on the scope:
TenantOwnedwritestenant-owned.jsonandTenantFullwritestenant-full.json. A plan run with a different scope from the one your repository was exported with shows your file asaddedand the server's file asremoved. - Apply pushes every file in the canonical folders, not only the ones the plan flagged, and it pushes every
*.jsonunderxmatix/metadata/. A second metadata file in that folder, such as atenant-full.json, is pushed as well. - A full-mode metadata file deletes every row that an earlier pack delivered to the tenant and that the file doesn't contain. A delta-mode file only adds and updates.
- Apply never deletes a script, rule, workflow, dashboard or report on the server.
- Scripts are matched by the script id in their
.meta.json, rules by entity name and rule name, and workflows by definition id. - A rule file with no
actionslist leaves the rule's actions on the server unchanged. - A script push is skipped when the file matches the version recorded in its
.meta.json. It is refused with exit code 2 when someone changed the script in the product after the last pull. - Apply runs every push even after a failure, and reports one exit code at the end.
Example
A developer changes the credit-check rule on orders and promotes it to the tenant the repository was exported from.
- They edit
xmatix/automation/rules/Order/CreditCheck.jsonto raise the limit in the rule's condition. The change is reviewed and merged. - Because
migrate plancan't run in the current release, they check for product changes on a clean checkout of the last applied commit:xmatix snapshot export --skip scripts,dashboards,reports, thenxmatix scripts pull --all.git diffwith thegeneratedAtUtcandlastPulledUtcpatterns shows nothing, andgit statusshows no new files, so nobody changed the tenant since the last apply. They discard the export withgit checkout -- xmatix/. - The metadata hasn't changed either, so they leave metadata out of the apply.
- They run
xmatix migrate apply --skip metadata,dashboards,reports --dry-runand check thewould run:lines. - They run
xmatix migrate apply --skip metadata,dashboards,reports. The five scripts are skipped as unchanged. The rule is updated, and they confirm the replacement of its two actions. The run ends with Snapshot import complete. - A second export into a clean checkout of the merged commit shows no change to
CreditCheck.json.
Training
Practice exercise
In a sandbox workspace with an exported, committed xmatix/ tree:
- Run
xmatix migrate plan. Expected result in the current release: "Server snapshot export failed; aborting plan." and exit code 1. - In the product, change the description of a test business rule. Run
xmatix snapshot export --skip scripts,dashboards,reportsandgit diff -- xmatix/automation/rules/. Expected result: the diff shows the new description in that rule's file. - Commit the change. Export again and run the same
git diff. Expected result: no output. - Change the rule's description in the workspace file. Run
xmatix migrate apply --skip metadata,scripts,workflows,dashboards,reports, and answer the confirmation if one appears. Expected result: the product shows your description, and a new plan shows the file asunchanged.
Quick reference
migrate planstops at its export in the current release. Compare by exporting into a clean checkout instead.- When it runs,
migrate planreads only, and exits 0 whatever it finds. - The metadata file and every
.meta.jsonshow asmodifiedon every run. --keep-server-snapshotputs the tenant's copy underxmatix/.snapshot/server/fordiff.migrate applyissnapshot import. It pushes every file, not only the flagged ones.- Set the metadata file's
modetodelta, or skip metadata. Never apply a full pack. - Add
--skip dashboards,reportsuntil analytics can round-trip. - Activate pushed scripts afterwards. New scripts must be created in the product first.
- Nothing on the server is deleted for a
removedfile.
Permissions
The CLI acts with the permissions of the account that signed in with xmatix login. The server records that account as the author of every change.
- The metadata part of
migrate planandmigrate applyneeds thesetup.metadata.packages.managecapability, the same one that opens Setup → Packages. Without it, the metadata pull or push fails with a 403, and so does the plan. - Scripts, rules and workflows need the same access that their own screens need in the product.
See the security model for how capabilities are granted.
Configuration
- Target tenant. The nearest
.xmatix/config.jsonabove the current folder decides the tenant, for both commands. Keep one workspace per tenant. - Metadata scope.
--scopeonmigrate planmust match the scope your repository was exported with. The default,TenantOwned, holds the tenant's own metadata and the rows earlier pushes recorded, but not edits to standard rows (see Which rows each scope includes). - Metadata mode. The
modeproperty in the metadata file decides whether a push prunes. Set it todeltafor promotions. .gitignore. Addxmatix/.snapshot/.- Kinds. Use
--skiponmigrate applyto leave out kinds you don't manage in the repository, and alwaysdashboards,reportsin the current release.
Common problems
"No xMatix workspace found. Run xmatix init in your repository root first."
The command was run outside a workspace. Change to a folder inside the workspace that holds .xmatix/.
"Server snapshot export failed; aborting plan."
One of the pulls in the plan's export failed. In the current release this happens on every run: the export runs scripts pull without --all, which always fails, and the plan can't skip it. The plan hides the export's normal output, so the message itself is all you see. Compare the repository with the tenant by exporting into a clean checkout instead, as described in Snapshots. An expired sign-in or a missing permission, such as a 403 on the metadata pull, gives the same message.
The plan always shows the metadata file and the .meta.json files as modified
Every export stamps the metadata file with generatedAtUtc, and every script pull stamps lastPulledUtc in each .meta.json. Their hashes therefore never match. Compare metadata with jq -S .tables, and leave .meta.json files out of assertions, as the CI recipes drift gate does.
Every analytics file is listed as added
Dashboards and reports can't be pulled in the current release, so the tenant's export has no analytics files. See Dashboards and reports in the CLI.
My metadata file is added and a different one is removed
The plan ran with a different --scope from the one your repository was exported with, so the export wrote a file with a different name. Run the plan with the matching scope.
"No sibling .meta.json next to …" during apply
The script exists only in your workspace. Push updates scripts that were pulled; it doesn't create them. Create the script in the product, run xmatix scripts pull, and copy your code into the pulled .csx file.
"Server has a newer current version than your local copy."
Someone changed the script in the product after your last pull, and the push stops rather than overwrite that change. Pull the script, merge the two changes, commit and apply again. Use xmatix scripts push <file> --force only when you mean to discard the change made in the product.
Pushed script changes don't take effect
migrate apply saves a new version of each changed script without making it active. Activate it in the product, or push with --activate.
Each apply creates another copy of a workflow
The workflow's definition id in the file doesn't exist on the tenant, so each push creates a new workflow and the file keeps the old id. After the first apply, run xmatix workflows pull and commit the file, so later applies update that workflow. Delete the extra copies in the product.
Standard metadata disappeared after an apply
The metadata file was pushed in full mode, which deletes every row that an earlier pack delivered and that the file doesn't contain. Always set mode to delta or skip metadata. See What push does.
The apply stops at the metadata or rules step in a pipeline
metadata push, and rules push for a rule that already has actions, ask for confirmation. migrate apply doesn't pass --yes, and nobody can answer in a pipeline. Push those kinds yourself, as in CI recipes.
"Entity '…' not found on server." for a rule
The rule belongs to an entity that the tenant doesn't have. Push the metadata that creates the entity first, or leave the rule out.
Common questions
Does apply push only what the plan flagged?
No. migrate apply doesn't read the plan. It pushes every artifact file in the canonical folders, so unchanged files are pushed too. Unchanged scripts are skipped, but rules and workflows are written again with the same content, and a rule's actions are deleted and recreated. Metadata is the exception, because a full-mode file can delete rows. Use --skip to leave out whole kinds, and use the plan to decide whether to apply at all. If you need to push only a few files, run the per-kind push commands on those files instead of migrate apply.
Can I promote from one tenant to another?
Only partly. Point a workspace at the target tenant, copy the files in, then plan and apply. Metadata and rules can move between tenants, because rules are matched by entity and rule name. Rules still have limits: their target fields are stored as ids from the source tenant, and their delay settings are dropped when a rule is created (see Business rules in the CLI). Scripts can't: each .meta.json holds the script's id in the tenant it was pulled from, so the push fails with "Script … no longer exists on the server." Workflows from another tenant are created as new workflows on every apply until you pull them into the target workspace. Treat each tenant's workspace as its own exported copy.
Can apply delete something I removed from the repository?
Not for scripts, rules, workflows, dashboards or reports. A file that you deleted from the repository shows as removed in the plan, and the artifact stays on the tenant until someone deletes it in the product. This is deliberate, so a missing file can never remove configuration. Metadata is different: a full-mode metadata file deletes rows that it doesn't contain. Set mode to delta to stop that.
Why does the plan exit 0 when it finds differences?
The plan is a report, not a gate. It exits 1 only when the export fails. To fail a pipeline on drift, assert on the plan file's entries with jq -e, leaving out the metadata file, the .meta.json files and the analytics folders, and compare metadata with jq -S .tables. In the current release the plan can't get past its export, so the CI recipes drift gate compares an export with the checkout instead.
