xMatix
Sign in Request demo
xMatix
PRODUCTS
SalesField Sales & SFACRMRewardsClaimsInventoryProcurementWarehouse ManagementField ServiceServiceSupportTelephony & MessagingFinance & AccountingPayrollExpense ManagementCommercePortalsAnalytics & ReportingData StudioMobile AppSee all products →
PLATFORM
Platform overviewApp BuilderAutomationIntegrationsSecurity & GovernanceChange ManagementDevelopers
SENSE AI
Sense AI overviewSense AssistSense ControlSense VisionAI StudioTrust & governanceIn Claude & ChatGPTUse cases
SOLUTIONS
Auto DMSConsumer Goods DMSSales Force Automation (SFA) FMCG & DistributionManufacturing & Dealer NetworksAutomotive & DealershipsPharma & HealthcareConsumer DurablesAgri-InputsBuilding MaterialsService NetworksWarehousing & 3PLFinancial AccountingERP SoftwareIndia GST ComplianceUAE VAT & e-InvoicingSaudi ZATCA & VATAll solutions →
RESOURCES
Knowledge CenterDeveloper & CLIBlogGuidesWhat is xMatix?Company facts
COMPANY
AboutCareersPartnersEventsContactAuthorsLegal
Sign in Request demo
Home/Docs/Developer/CI recipes
REFERENCE · Last reviewed

CI recipes

These recipes run the xMatix CLI in a pipeline. They cover four jobs: a drift gate that fails when the live tenant no longer matches the repository, a promotion job that applies a tagged repository state to the tenant, a release-time snapshot check, and a local pre-commit hook. Each recipe works with the current CLI release. Several CLI commands behave differently in a pipeline than at a terminal, so the recipes avoid the shortcuts that would fail, and this page explains why.

Overview

A pipeline uses the same commands a developer runs by hand: migrate plan, snapshot export, the per-kind push and pull commands and scripts list. Five things shape every recipe on this page:

  • Sign-in needs a person once. Device-code sign-in is the only method, so jobs run on a persistent runner where someone has completed xmatix login.
  • xmatix doctor is not a gate. In the current release it always exits 1, so a pipeline that starts with it never gets further. The recipes use xmatix scripts list as the pre-flight check instead.
  • Two fields change on every pull. Every metadata export stamps a fresh generatedAtUtc, and every pulled script's .meta.json stamps a fresh lastPulledUtc. A plain file-hash or git diff comparison therefore always reports a change, even when nothing changed in the product. The recipes compare content with those fields left out.
  • Two pushes ask for confirmation. metadata push always asks, and rules push asks when the rule already has actions on the server. migrate apply and snapshot import don't pass --yes, and nobody can answer the prompt in a pipeline, so the promotion recipe pushes those kinds itself.
  • The export needs --skip scripts. snapshot export runs scripts pull without --all, so its scripts step always fails. The recipes skip it and run xmatix scripts pull --all separately. For the same reason migrate plan, which runs a full export it can't skip from, always stops with "Server snapshot export failed; aborting plan." The drift gate therefore compares an export with the checkout instead of using the plan.

When to use it

  • Drift gate. Run it nightly against the commit that was last applied to the tenant, to catch changes someone made in the product without exporting them.
  • Promotion. Run it when a release tag is pushed, to apply the reviewed repository state to the tenant it was exported from.
  • Release-time snapshot. Run it at release time to prove that the repository holds everything the tenant has.
  • Pre-commit hook. Use it on a developer's machine to stop a metadata file being committed when it no longer matches the tenant.

How xMatix handles it

What the pipeline signs in as

Every command runs as the user who completed xmatix login on the runner, with exactly that user's access. The token cache sits in that operating-system user's home folder under .xmatix/tokens/<tenant>/, protected by the operating system: the keychain on macOS, data protection on Windows and the secret service (keyring) on Linux. The CLI has no plain-file fallback. A pipeline job finds the cache only when all three of these are true:

  • the job runs as the same operating-system user who signed in;
  • the runner keeps its home folder between jobs;
  • on Linux, a secret service is available to that user, even though no one is logged in to a desktop.

Commands renew the session silently. When renewal is no longer possible, the job fails and a person runs xmatix login on the runner again. See CLI authentication.

What decides the tenant

The checked-out .xmatix/config.json does. Its API base URL decides which organization every command acts on, and its tenant slug names the token cache. Keep one workspace per tenant, so a job can never pick up another tenant's config.

What migrate plan reports

In the current release migrate plan stops at its export every time (see above), so none of the recipes use it. This section describes what it reports when its export succeeds.

migrate plan exports a fresh copy of the tenant into a temporary workspace and compares it file by file with the checkout. It does this for metadata, scripts, rules, workflows, dashboards and reports. It writes xmatix/.snapshot/migrate-plan.json, which contains:

  • counts, with the numbers added, removed, modified and unchanged;
  • entries, with one object per file: RelPath, Status, LocalHash and ServerHash.

The entry names start with a capital letter and the count names don't, so write jq filters to match. added means the file exists only in the checkout, removed means it exists only in the tenant, and modified means the content hash differs. The plan exits 0 whatever it finds. It exits 1 only when the export itself fails, so a gate always needs its own assertion.

Because of the two changing fields, xmatix/metadata/tenant-owned.json and every script's .meta.json show as modified on every run. Dashboards and reports can't be pulled in the current release, so any analytics files in the checkout show as added. Real drift in scripts, rules and workflows still shows correctly, and metadata drift is found by comparing the seed-pack's tables (see the drift gate below).

What migrate apply does

migrate apply runs snapshot import. That pushes every *.json under xmatix/metadata/, then every .csx script, rule and workflow, then dashboards and reports, and finally reports one exit code for the whole run. --dry-run prints each push it would run and contacts nothing. In a pipeline, a full apply goes wrong in four ways:

  • metadata push stops to ask for confirmation, which nobody can answer, so the metadata step fails.
  • When the push does run (interactively), it sends the metadata files as they are. Files written by snapshot export or metadata pull are full packs, and a full pack deletes metadata that the file doesn't contain. See What push does.
  • rules push asks the same question for each rule that already has actions on the server, so updates to existing rules fail.
  • Scripts are pushed without --activate. Each changed script gets a new version, but the active version doesn't change.
  • Dashboard and report pushes fail in the current release. See Dashboards and reports in the CLI.

Scripts, rules, workflows, dashboards and reports are never deleted on the server: an artifact that exists only in the tenant stays there. Metadata is the exception: a full-mode metadata file prunes, as described above.

Prerequisites

  • A persistent runner with access control, with the CLI installed and on PATH, and jq and git available.
  • xmatix login completed once on the runner, as the operating-system user the jobs run as (see above).
  • A repository whose xmatix/ tree was created by xmatix snapshot export --skip scripts,dashboards,reports and xmatix scripts pull --all, and committed.
  • xmatix/.snapshot/ added to the repository's .gitignore. The plan and the kept server copy are written there, and xmatix init does not ignore that folder.
  • The runner's account holds the access the jobs need (see Permissions).

Procedure

Step 1 — Prepare the runner

  1. Sign in on the runner as the operating-system user the jobs run as. From a checkout of the repository, run a bare xmatix login and complete the device-code sign-in as the pipeline's integration account.
  2. Run xmatix scripts list. Expected result: the tenant's scripts appear. This proves the token works and the API accepts it, and every recipe below starts with the same check.
  3. Add xmatix/.snapshot/ to the repository's .gitignore and commit it.

Step 2 — Add the drift gate

The gate runs on a schedule against the last commit that was applied to the tenant. It does not run on pull requests: a pull request that changes an artifact differs from the tenant by design, so a gate on it would always fail.

name: drift-gate
on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:
jobs:
  drift:
    runs-on: [self-hosted, xmatix-runner]
    defaults:
      run:
        shell: bash
    steps:
      - uses: actions/checkout@v4
        with:
          ref: production
      - run: xmatix scripts list > /dev/null
      - run: xmatix snapshot export --skip scripts,dashboards,reports
      - run: xmatix scripts pull --all
      - name: Compare with the commit
        run: |
          git status --short -- xmatix/
          git diff --exit-code -I '"generatedAtUtc"' -I '"lastPulledUtc"' -- xmatix/
          test -z "$(git ls-files --others --exclude-standard -- xmatix/)"

In this example, production is a branch that you move to each release after it has been promoted. Use whatever marks the last applied state in your repository.

  • The export and the script pull overwrite the checked-out files with the tenant's current versions. Scripts are pulled on their own because the export's scripts step always fails in the current release.
  • git diff with the two -I patterns ignores hunks in which only the export or pull time changed, and fails when a metadata row, script, rule or workflow was changed in the product. The -I option needs git 2.30 or later.
  • The last line fails when the export wrote a file the commit doesn't have, such as a rule created in the product. Rules on entities the CLI can't see are written to xmatix/automation/rules/_unknown/ on every pull, so add that folder to .gitignore or the gate never passes (see Business rules in the CLI).
  • Export never deletes a local file, so an artifact deleted in the product is not caught.

When the gate fails, export the tenant into a branch with the same two commands and review the change. Don't force an apply over it.

Step 3 — Add the promotion job

The job applies the tagged state to the tenant the repository was exported from, pushing each kind itself.

name: promote
on:
  push:
    tags: ['v*']
jobs:
  promote:
    runs-on: [self-hosted, xmatix-runner]
    defaults:
      run:
        shell: bash
    steps:
      - uses: actions/checkout@v4
      - run: xmatix scripts list > /dev/null
      - run: xmatix migrate apply --dry-run
      - name: Metadata
        run: |
          jq '.mode = "delta"' xmatix/metadata/tenant-owned.json > "$RUNNER_TEMP/tenant-owned.json"
          xmatix metadata push "$RUNNER_TEMP/tenant-owned.json" --yes
      - name: Scripts
        run: |
          find xmatix/automation/scripts -name '*.csx' -print0 |
            xargs -0 -I{} xmatix scripts push {} --activate -m "$GITHUB_SHA"
      - name: Rules
        run: |
          find xmatix/automation/rules -name '*.json' -print0 |
            xargs -0 -I{} xmatix rules push {} --yes
      - name: Workflows
        run: |
          find xmatix/automation/workflows -name '*.json' -print0 |
            xargs -0 -I{} xmatix workflows push {}
  • The dry run lists every push that migrate apply would make. Read it in the job log to check that the job is using the right workspace and folders. It changes nothing.
  • Metadata goes first, so the entities that scripts and rules refer to exist. The job pushes a copy of the file with mode set to delta. A pulled file is a full pack, and pushing a full pack deletes every metadata row that an earlier pack delivered and that the file doesn't contain, which includes the tenant's standard metadata. See What push does.
  • scripts push --activate makes each changed script's new version active and stores the commit in the version's comment. It skips a script whose body matches the active version. It refuses, with exit code 2, a script that someone changed in the product since the last pull, so the job stops instead of overwriting that change.
  • rules push --yes creates or updates each rule by entity and rule name, and replaces its actions with those in the file.
  • workflows push updates each workflow by its definition id, or creates it when the id is not found. It keeps the published state recorded in the file.
  • xargs exits non-zero when any single push fails, so the step fails.

Step 4 — Add the release-time snapshot check

Run this at release time on any CI system to prove that the repository holds what the tenant has:

xmatix scripts list > /dev/null
xmatix snapshot export --skip scripts,dashboards,reports
xmatix scripts pull --all
git diff --exit-code -I '"generatedAtUtc"' -I '"lastPulledUtc"' -- xmatix/
test -z "$(git ls-files --others --exclude-standard -- xmatix/)"
  • snapshot export and scripts pull --all overwrite the checkout's files with the tenant's current versions. The export exits 1 if any pull fails, which is why it skips the scripts step.
  • git diff with the two -I patterns ignores hunks in which only the export time or pull time changed. It fails when anything else changed. The -I option needs git 2.30 or later.
  • The last line fails when the export wrote a file the repository doesn't have, such as a rule created in the product. git diff alone never shows new files.

When the check fails, keep the diff and the list of new files as a build artifact. Together they show exactly what was changed in the product. Export never deletes a local file, so an artifact deleted in the product is not caught here.

Step 5 — Add a local pre-commit hook

This hook stops a commit when the staged tenant-owned.json no longer matches the tenant. Save it as .git/hooks/pre-commit and make it executable:

#!/usr/bin/env bash
set -euo pipefail
git diff --cached --quiet -- xmatix/metadata/tenant-owned.json && exit 0
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
xmatix metadata pull --out "$tmp" > /dev/null
if ! diff -q <(jq -S .tables "$tmp") \
             <(git show :xmatix/metadata/tenant-owned.json | jq -S .tables) > /dev/null; then
  echo "xmatix/metadata/tenant-owned.json does not match the tenant."
  echo "Pull it again, or push your change first."
  exit 1
fi
  • The hook does nothing for commits that don't stage the metadata file.
  • It compares the staged version (not the working copy) with a fresh export, using only the sorted tables.
  • It can't tell which side is newer. A hand edit that you haven't pushed yet also blocks the commit. Push first, or commit with git commit --no-verify when you mean to.

Fields

Exit codes the recipes rely on:

CommandExit codesNotes
xmatix doctor0 all checks pass, 1 any check failsAlways 1 in the current release, so read its token line instead
xmatix scripts list0 on success, non-zero on failureThe pre-flight check used on this page
xmatix migrate plan0 when the plan is written, 1 when the export failsAlways 1 in the current release, because its export always fails. Never fails because of drift
xmatix migrate apply, xmatix snapshot import0 when every push succeeds or there is nothing to push, 1 when any push fails--dry-run always 0
xmatix snapshot export0 when every pull succeeds, 1 when any pull failsAlways 1 unless --skip includes scripts. Dashboard and report pulls exit 0 while writing nothing
xmatix scripts pull --all0 on success, non-zero on failureScripts with no current version are skipped
xmatix metadata push0 imported, 1 failed or declinedWithout --yes it asks for confirmation
xmatix metadata diff0 no drift, 1 drift, 2 errorAlways reports drift against a pulled file (see Common problems)
xmatix scripts push0 pushed or unchanged, 1 failed, 2 changed on the server, 3 compile error--force skips the server-change check
xmatix rules push0 pushed, 1 failed or declinedWithout --yes it asks before replacing existing actions

The plan file, xmatix/.snapshot/migrate-plan.json:

FieldWhat it meansNotes
generatedUtcWhen the plan was madeUTC
workspaceRootThe workspace the plan comparedA full path on the runner
counts.addedFiles only in the checkoutIncludes analytics files, which can't be pulled
counts.removedFiles only in the tenantApply never deletes these
counts.modifiedFiles whose content hash differsAlways includes tenant-owned.json and every .meta.json
counts.unchangedFiles that match
entries[].RelPathThe file's path from the workspace root, such as xmatix/automation/rules/<entity>/<rule>.jsonCapitalised, unlike counts
entries[].Statusadded, removed, modified or unchanged
entries[].LocalHash, entries[].ServerHashSHA-256 of each side's fileEmpty on the side where the file is missing

Business rules

  • No command in this page's recipes deletes anything in the tenant.
  • migrate plan and snapshot export use the TenantOwned metadata scope unless you pass --scope.
  • Every metadata export carries a new generatedAtUtc, and every script pull writes a new lastPulledUtc.
  • migrate apply and snapshot import pass --skip and --dry-run through, and nothing else.
  • A script's .meta.json holds the id of the script in the tenant it was pulled from. scripts push finds the script by that id.
  • Rules are matched by entity name and rule name, and workflows by definition id.
  • The plan runs its export in a temporary workspace with a copy of your config, so it signs in with the same token cache.

Example

A team keeps the configuration of its production tenant in a repository:

  1. An administrator signs in once on the team's runner as the integration account and runs xmatix scripts list to check it.
  2. A developer changes a credit-check script and a discount rule, opens a pull request, and the change is reviewed and merged.
  3. A release manager tags the merge v2.4.0. The promotion job runs. The dry run lists the pushes, metadata imports, and the script gets a new active version with the commit in its comment. The rule's actions are replaced. The release manager then moves the production branch to the tag.
  4. Two days later, a consultant adds an action to the discount rule in the product without exporting it. That night the drift gate's git diff fails and shows the new action in the rule's file.
  5. The developer exports the tenant into a branch, commits the changed rule, gets it reviewed and merged, and moves production again. The next night the gate passes.

Training

Practice exercise

In a sandbox workspace with an exported, committed xmatix/ tree:

  1. Run xmatix snapshot export --skip dashboards,reports. Expected result: the scripts step fails with "Specify a script name or pass --all." and the export exits 1.
  2. Run the four commands from Step 4 without changing anything in the product. Expected result: git status shows the metadata file and the .meta.json files as modified, but the git diff with the two -I patterns passes, and so does the untracked-files check.
  3. In the product, change the description of a test business rule in the sandbox. Run the Step 4 commands again. Expected result: git diff fails and shows the changed description.
  4. Commit the export and run the Step 4 check again. Expected result: it passes.

Quick reference

  • Pre-flight with xmatix scripts list, never xmatix doctor.
  • Export with --skip scripts,dashboards,reports and pull scripts with xmatix scripts pull --all.
  • migrate plan can't run in the current release. Detect drift with an export and git diff.
  • Leave tenant-owned.json, .meta.json and analytics out of hash checks. Compare metadata with jq -S .tables.
  • Push metadata as a delta pack with --yes, rules with --yes and scripts with --activate.
  • Skip dashboards and reports in every export and import.
  • git diff misses new files. Check git ls-files --others too.
  • Promote only into the tenant the scripts were pulled from.
  • Only a full-mode metadata push deletes anything on the server, so never push one.

Permissions

The pipeline has no permissions of its own: it can see and change exactly what the signed-in account can in the product, and the server records that account as the author of each change. Use a dedicated integration account with only the access the jobs need. Treat the runner's home folder, which holds its token cache, like any other credential store.

Exporting and importing metadata (metadata pull, metadata push, metadata diff, and the metadata part of snapshot export and migrate plan) requires the setup.metadata.packages.manage capability. This is the same capability that opens Setup → Packages. Without it, those commands fail with a 403 response. Scripts, rules and workflows need the access their own areas need in the product. See the security model for how capabilities are granted.

Configuration

  • Runner. A persistent, access-controlled machine that keeps its home folder between jobs, with the CLI, jq and git 2.30 or later. On Linux, make the secret service available to the job's user.
  • Workspace. One committed .xmatix/config.json per tenant. Commands use the nearest .xmatix/ folder above the current directory, so run each job from its tenant's workspace.
  • .gitignore. Add xmatix/.snapshot/. xmatix init ignores only the cache and token folders inside .xmatix/.
  • Metadata scope. --scope on migrate plan and snapshot export changes which metadata is exported. The default, TenantOwned, holds the tenant's custom metadata and the rows earlier pushes recorded, but not edits to standard rows (see Which rows each scope includes). The recipes assume it.
  • Analytics. Add --skip dashboards,reports to every snapshot export and snapshot import until analytics round-trips.

Common problems

The job stops at xmatix doctor with "problem(s) detected"

In the current release, doctor's api check always fails with a 404, and its scopes check fails whenever scopes is empty. So doctor always exits 1. Use xmatix scripts list > /dev/null as the pre-flight step instead. To read the token line, run doctor with || true.

"No cached account; run xmatix login first." on the runner

The job can't see a token cache for this tenant slug. Either it runs as a different operating-system user from the one who signed in, the runner doesn't keep its home folder between jobs, or nobody has signed in on it yet. Sign in on the runner as the job's user. On Linux, also check that the job can reach the secret service.

The drift gate fails every night on tenant-owned.json or .meta.json

Those files change on every pull: the export time and the pull time. A plain git diff or file-hash check can never pass. Keep the two -I patterns from Step 2, and check that git is 2.30 or later.

The drift gate fails on every pull request

The gate compares the checkout with the tenant, and a pull request that changes an artifact differs from the tenant by design. Run the gate on a schedule against the last applied commit, not on pull requests.

migrate apply fails in the pipeline at the metadata or rules step

metadata push, and rules push for a rule that already has actions, ask for confirmation, and migrate apply doesn't pass --yes. No one can answer the prompt in a pipeline. Push those kinds yourself with --yes, as in Step 3.

migrate apply pushes scripts without --activate, so the new version is saved but the old version stays active. Push scripts with xmatix scripts push <file> --activate, as in Step 3, or activate the new version in the product.

"Server has a newer current version than your local copy."

Someone changed the script in the product after it was last pulled, and scripts push exits 2 rather than overwrite that change. Pull the script, merge the two changes, commit, and promote again. Use --force only when you mean to discard the change made in the product.

"Script … no longer exists on the server." when promoting to another tenant

A script's .meta.json holds the script's id in the tenant it was pulled from, and the target tenant has a different id. Script promotion works only into the tenant the scripts were pulled from. Create the script in the other tenant, pull it from there into that tenant's workspace, and keep the two workspaces separate.

metadata diff reports drift right after a pull

In the current release, metadata diff matches rows by their exact text. The pulled file is indented and the fresh export isn't, so every row counts as added and removed, and the command exits 1. Don't use it as a gate. Compare jq -S .tables of the two files, as the drift gate and the pre-commit hook do.

The release check passes, but a rule created in the product is missing from the repository

git diff --exit-code ignores files git doesn't track yet, and a newly exported rule is one. Add the git ls-files --others --exclude-standard -- xmatix/ check from Step 4.

Dashboard or report steps fail, or analytics files show as added

Dashboards and reports can't be pulled or pushed in the current release. Skip them with --skip dashboards,reports, and leave the analytics folders out of the drift assertions. See Dashboards and reports in the CLI.

Metadata commands fail with 403

The pipeline account lacks the setup.metadata.packages.manage capability. Grant it through the account's security profile.

Common questions

Which user should the pipeline sign in as?

Use a dedicated integration account with exactly the access the jobs need. The CLI acts with the signed-in account's permissions, so that account's grants are the most a pipeline can change, and the server records it as the author of every change. Sign it in once on the runner, as the operating-system user the jobs run as. Keep it separate from any person's account, so the audit trail shows pipeline changes as pipeline changes. When its session can no longer be renewed, a person signs in on the runner again. No other method exists, because the CLI offers only device-code sign-in.

How does the pipeline know which tenant it targets?

From the checked-out .xmatix/config.json. Its API base URL decides the organization, and its tenant slug names the token cache. Make this explicit in the repository: one repository, or one directory with its own workspace, per tenant. Then a job can't apply one tenant's files to another tenant by picking up an unexpected config. Every command uses the nearest .xmatix/ folder above the directory it runs in, so cd into the right workspace before each command.

Can the drift gate and promotion target different tenants?

Yes, when each job uses its own tenant's workspace and repository state. For example, you can drift-gate production while promoting a different branch to a test tenant. Promotion moves scripts only into the tenant they were pulled from, because scripts are matched by their id in that tenant. Rules are matched by entity and rule name. So you can't copy a tested script from one tenant into another just by promoting the same files. Each tenant's workspace needs its own exported copy.

Can I run the recipes on a hosted, short-lived runner?

No. The token cache must survive between jobs, and on Linux it lives in the user's secret service. A short-lived runner starts each job without a cache, so the first command fails with "No cached account". Because sign-in always needs a person to enter a device code, the job can't sign itself in. Use a persistent runner with access control.