Skip to main content

Acumatica ERP - GitOps CLI

acu configures Acumatica ERP from YAML files in a git repo (GitOps).

No UI clicks, no Configuration Wizard.

Tested against Acumatica ERP 26.101.0225 on Windows Server 2025, contract REST endpoint 25.200.001. Other versions will likely work, but only this combination is verified.

Why

Acumatica configuration normally lives in the web UI: wizards, screens, and manual data entry that nobody can review, version, or reproduce.

acu moves that configuration into YAML files in a git repo, so a tenant can be rebuilt from scratch, audited in a pull request, and checked for drift like any other infrastructure.

Quick start

uv tool install acumatica-cli

acu config init --host erp.example.com my-erp
cd my-erp                                # edit .env: set ACU_PASSWORD, ACU_TENANT
                                         # pin+where = matrix.yaml cell (default_api + base_url)
                                         # start from a brand-new empty tenant

acu config check                         # read-only preflight (incl. matrix.yaml)
acu tenant create --login DEV            # create + bootstrap (SSH; --id optional)
# or hosted: acu --tenant DEV bootstrap
acu --tenant DEV apply config/           # seed config/{bootstrap,baseline,setup,master}/
acu --tenant DEV run scenario/           # once capital → buy → build → sell
acu --tenant DEV diff config/            # prove zero drift (exit 2 on drift)
acu --tenant DEV state                   # capture state/ trial-balance
acu check --yes --tenant DEV             # cold lifecycle create→apply→run→diff (leave tenant)

Bare apply / diff (no path args) also prefer config/ when those trees exist. See docs/demo-seed.md for the entity map, once-guard, apply-order notes, NumberingSequence vs prefs *NumberingID, curated *Preferences field depth (V41), and Role/User + password seed rules.

Hosted Acumatica (no SSH): the tenant already exists; set a blank ACU_SSH= in .env (scaffold omits the key — without it, acu defaults to Administrator@<ACU_BASE_URL host> for SSH boxes).

acu config init --host customer.acumatica.com my-erp
cd my-erp                                # edit .env: ACU_TENANT, ACU_PASSWORD; add ACU_SSH=
acu config check                         # REST preflight; ssh probe is skipped
acu --tenant DEV bootstrap               # publish AcuBootstrap via REST only
acu --tenant DEV apply config/
acu --tenant DEV diff config/
# offline UI fallback when REST publish is blocked:
acu bootstrap --export AcuBootstrap.zip  # import + publish on SM204505

CLI map

acu [--cell ID] [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
    [--username U] [--password P] [--version] [--completion [SHELL]]
│
├── tenant                            tenant CRUD (ac.exe over SSH — control plane)
│   ├── list                          CompanyID, sign-in name, internal CD, type
│   ├── create --login NAME [--id N]  create + bootstrap; re-run to republish (SSH)
│   │          [--type SalesDemo|T100|U100] [--parent N] [--hidden] [--no-init]
│   │                                 omit --id → next free CompanyID (max list + 1)
│   ├── delete --id N | --login NAME [--yes]
│   │                                 delete the tenant and its data, recycle app pool
│   └── recycle [--yes]               restart site app pool (tenant map + free API slots)
│
├── bootstrap [--export PATH]         publish AcuBootstrap (REST); --export = offline zip
├── apply [--dry-run] [FILES...]      push YAML via REST (idempotent PUT upserts)
├── diff  [FILES...]                  drift check vs the live tenant (exit 2 on drift)
├── run   [--dry-run] [FILES...]      execute transaction scenario YAML (exit 1 on any miss)
├── check [--all] [--yes] [--tenant L] cold lifecycle create→apply→run→diff; leave tenant (V47)
├── state [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
│                                     capture derived state into state/ (not seed)
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
│                                     inverse of apply into config/{bootstrap,baseline,setup,master}/
├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
│                                     offline snapshot artifact → inventory/ (not seed)
├── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
│                                     inventory/ + optional config/ → findings/ only
├── schema [--out DIR]                dump the endpoint's OpenAPI schema (swagger.json)
│
└── config                            configuration ops
    ├── init [--host HOST] [DIR]      scaffold full data repo (config/, scenario/, matrix.yaml)
    ├── show                          print the resolved config as a complete .env
    └── check [--strict]              preflight: discovery, secrets, matrix, REST, endpoints, SSH

apply and diff without FILES prefer config/<name>/ when any seed child exists under config/; otherwise root bootstrap/, baseline/, setup/, then master/ when present. A path like config/ expands nested seed dirs in that fixed order. run without FILES defaults to scenario/. Scenario YAML may use ${current_period} (host-local MMyyyy) on steps, expect params, and once.present params; config/views / state keep Period pinned (see docs/demo-seed.md). state without FILES defaults to config/views/; writes go to state/ (--out). extract always writes under config/{bootstrap,baseline,setup,master}/ (catalog-driven; never root SEED_DIRS). inventory is offline (no REST/SSH/password): SM203520 Settings XML ZIP or ac.exe export xml folder writes to inventory/. reconcile is offline: compare inventory/ to optional config/ and write findings/ only (never writes seed). Optional snapshot_map.yaml (data-repo root or package defaults) maps DAC tables to catalog entities and normalizes join (pad-trim, key/field aliases, Account/Sub FK CD resolve, enum label to code). See docs/demo-seed.md. acu --completion emits a completion script for bash, zsh, or fish — source it from your shell profile. Run acu --help for the full mental model (workflow, planes, exit codes, command map) — enough for an agent to learn the tool without extra docs. Run acu <command> --help (or -h) for flags, examples, and prerequisites.

Dual readers, one writer

Two read paths, one mutator (V35). Do not confuse them with each other or with state:

Command Plane Input Writes Role
extract REST (live) tenant via contract API config/{bootstrap,baseline,setup,master}/ Inverse of apply — seed YAML
inventory Offline SM203520 Settings XML ZIP or ac.exe export xml folder inventory/ (summary.yaml + tables/) Full-table snapshot IR — not seed
reconcile Offline inventory/ + optional config/ findings/ only Cross-check gaps/deltas — never mutates seed or tenant
state REST (live) config/views/ state/ Derived balances/totals — not seed, not inventory
apply REST (live) seed YAML under config/ tenant Sole tenant writer (keyed PUT)

inventory/ and findings/ are engagement outputs: not SEED_DIRS, never loaded by apply/diff, not scaffolded by config init. Binary .adb snapshots are rejected (XML only). See docs/ac-exe.md for export / SM203520 notes and docs/demo-seed.md for the extract/state/inventory map.

The data repo

Your configuration lives in its own git repo. acu config init scaffolds a single full seed under config/ (features, company, credit terms, expanded COA, masters) plus lifecycle scenario/, observer config/views/, and README. The Bootstrap endpoint contract is package SoT (bootstrap_project.xml inside the CLI — Bootstrap/1.4.0); config init never writes project.xml, and data repos must not keep one (a present file hard-errors on bootstrap/publish). There is no --flavor.

Path What it holds
config/bootstrap/ virgin-tenant config: features, company, credit terms (no project.xml)
config/baseline/ reference data: subaccounts, COA, ledger, UOMs
config/setup/ one-time actions: financial year, master calendar, open periods
config/master/ inventory/distribution masters: numbering (05-…) before prefs, warehouse, items, parties + Role/User (90-roles then 91-users)
scenario/ lifecycle txns for acu run: once capital, then buy, build, sell
config/views/ observer views for acu state (inquire: / entity: / gi:; not SEED_DIRS)
state/ committed derived-state observations (evidence, not seed; money/qty fixed-point)
inventory/ engagement: offline snapshot tables from acu inventory (not seed; not SEED_DIRS)
findings/ engagement: acu reconcile cross-check output (never apply path)
matrix.yaml multi-host pin+where: cells id+erp+default_api+base_url (V27); --cell selects
.env secrets + optional where override (ACU_*); never Default API pin

Legacy data repos may still keep root bootstrap/master/; bare apply/diff prefer config/ when present and never merge both trees.

Files in each directory apply alphabetically; the numbered prefixes (10-, 20-, and so on) encode dependency order. Commit matrix.yaml with the seeds so every clone knows verified ERP line, Default API half, and REST where per cell.

Seed YAML is state: apply upserts it, diff proves it. acu extract is the inverse of apply: GET live tenant rows into seed YAML under config/{bootstrap,baseline,setup,master}/ (hard-cut). Packaged seed_catalog.yaml is the sole extract registry (entity, endpoint, keys, file, strip/include, filter-split); the demo entity map in docs/demo-seed.md mirrors those catalog paths. Features synthesize to config/bootstrap/features.yaml. Existing files skip unless --force; empty live sets skip; row failures continue (exit 1 only if any row failed — drift stays with diff).

acu --tenant DEV extract --out . --force   # refresh config/** from live tenant
git diff config/                           # review extract delta before commit
acu --tenant DEV apply config/             # replay extracted seed
acu --tenant DEV diff config/              # expect exit 0

Seed endpoint: symbols

Dual-served entities (on both Bootstrap and Default) need an explicit endpoint: line.

Value Resolves to
omitted Default/<api_version> for Default-only entities
bootstrap active Bootstrap/<ver> from the packaged contract only
default Default/<api_version> — tracks the resolved API version
Bootstrap/1.4.0 or Default/25.200.001 literal pin (ignores the resolved Default version)

api_version resolves as --api-version flag, else active matrix.yaml cell default_api, else code default 25.200.001 (never ACU_API_VERSION in .env). base_url resolves as --url, else ACU_BASE_URL, else active cell base_url. Prefer symbolic default over a pinned Default/25.200.001 so the seed tree travels with the dataset pin.

Installation

Requires Python 3.14 or newer.

uv tool install acumatica-cli

pipx install acumatica-cli and pip install acumatica-cli work too.

Or clone and install editable for development:

git clone https://github.com/kborovik/acumatica-cli.git
cd acumatica-cli
gmake install    # editable install as a global uv tool

Verify with acu --version.

Configuration

Secrets live in one .env file (ACU_* vars). Non-secret where and the Default contract pin live in committed matrix.yaml (cell base_url + default_api). Optional ACU_BASE_URL overrides cell where for ad-hoc probes.

# ACU_BASE_URL optional when matrix.yaml cell carries base_url
ACU_TENANT=LAB5                                        # sign-in name of the tenant API sessions use
# ACU_SSH omitted → defaults to Administrator@ + resolved base_url host
# ACU_SSH=                                         # hosted opt-out (blank key)
ACU_USER=admin                                         # optional, defaults to admin
ACU_PASSWORD=...                                       # required for live commands

There is no ACU_API_VERSION env key (unknown ACU_* vars are ignored). Ad-hoc override: acu --api-version 24.200.001 … (version half only, never Default/25.200.001 — a full path would nest as /entity/Default/Default/...).

Committed matrix.yaml is the sole data-repo pin+where registry (1..N cells):

cells:
  - id: "default"
    erp: "26.101.0225"           # claimed product line/build
    default_api: "25.200.001"    # sources Instance.api_version when --api-version absent
    base_url: "http://acu-dev1.vm.internal/AcumaticaERP"

--cell <id> selects a cell (omit means first cell). When present, live commands source api_version from cell default_api and base_url from the cell when flag/env leave them unset. acu config check reports ok matrix (cell=...; api_version from default_api=...; ...). Missing matrix.yaml only warns on config check unless you pass --strict. acu check (lifecycle) requires matrix.

Multi-host matrix (V44)

One trunk seed in the data repo serves every host. Version fan-out is not long-running product branches (acu-25r1, acu-26r1, …).

Piece Role
Trunk seed Canonical config/ + scenario/ (newest supported matrix)
matrix.yaml cells Each host: id+erp+default_api+base_url; --cell / acu check --all
Optional overlays Surgical seed deltas keyed by Default half (e.g. overlays/default-24.200.001/)
OpenAPI Live acu schema dump only (gitignored); never multi-version swagger trees in package or data repo

Overlays live under overlays/default-<default_api>/ (scaffolded by acu config init). No --overlay flag.

Bare compose (pin auto): when path args are omitted, acu apply / acu diff append overlay config seed dirs when present; acu run replaces same-basename scenario files from the pin overlay. Pin = resolved api_version (matrix cell default_api). Explicit path args disable auto-compose.

# matrix cell default_api: 24.200.001 → uses overlays/default-24.200.001/
acu --cell lab25 apply
acu --cell lab25 run
acu --cell lab25 diff

# explicit path args (no auto) — later path wins same keys
acu apply config/ overlays/default-24.200.001/
acu diff config/ overlays/default-24.200.001/

# cold lifecycle every cell (SSH + tenant required); tenants left for inspect
acu check --all --yes --tenant LAB5

Add a future half by creating overlays/default-<new-half>/ with the minimal rewrite; no long-running product branches and no multi-version OpenAPI trees (V11/V44).

Sibling data-repo retirement of release branches: acumatica-gitops#2.

Worth knowing:

  • The .env file is found by walking up from the current directory, so any subdirectory of the data repo works.
  • Without a .env, global flags plus the process environment (and matrix cell where) supply the configuration.
  • When ACU_SSH is absent, acu defaults to Administrator@ + the resolved base_url hostname. A present blank ACU_SSH= is the hosted opt-out. Only acu tenant / acu check require a non-empty value post-default.
  • acu config show prints the resolved .env (password excluded; never ACU_API_VERSION) and comments active cell id/erp/default_api/base_url plus api_version source when matrix.yaml is present.
  • Redirect it to turn resolved state into a working config: acu config show > .env.

Verify before touching anything live:

acu config check           # discovery, secrets, matrix, REST, endpoints, SSH
acu config check --strict  # missing matrix.yaml becomes fail
acu apply --dry-run        # show what would be written, write nothing

Development

Requires GNU Make at least 3.82 — the Makefile uses .ONESHELL. On macOS use Homebrew's gmake (brew install make); /usr/bin/make is 3.81 and fails the guard. Elsewhere plain make is fine when it is GNU Make.

git clone https://github.com/kborovik/acumatica-cli.git
cd acumatica-cli
gmake install    # editable install as a global uv tool
gmake check      # offline gate: ruff, basedpyright strict, pytest

The default test suite is fully offline. REST is faked with httpx.MockTransport, SSH with a monkeypatched subprocess.run — no live instance is needed. gmake check must pass before every commit. GitHub Actions runs the same gate on every push and pull request to main.

Release

Human release notes live in root CHANGELOG.md (Keep a Changelog). During development, append user-facing work under ## Unreleased in ### Added / ### Changed / ### Fixed as appropriate. Empty Unreleased (no bullets) hard-fails the release — nothing to ship.

gmake release patch   # or minor | major

gmake release is the sole release path (never local gh release create):

  1. gmake check (ruff, basedpyright, offline pytest)
  2. Fail if ## Unreleased has no bullets
  3. Bump pyproject.toml version (major | minor | patch)
  4. Promote Unreleased body to ## [vX.Y.Z] - YYYY-MM-DD, leave an empty ## Unreleased
  5. Commit CHANGELOG.md + pyproject.toml (+ lock if bumped) together, tag vX.Y.Z, push

GitHub Actions on tag v* re-runs CI, builds sdist+wheel, publishes to PyPI via OIDC trusted publishing, and creates a GitHub Release whose notes are the promoted CHANGELOG section for that tag (plus the artifacts).

Live end-to-end tier

gmake e2e runs the opt-in live tier against a real Acumatica instance (pytest marker e2e, deselected by the default suite).

Configuration is one file: a decrypted .env at the repo root names the instance — ACU_BASE_URL, ACU_TENANT, ACU_PASSWORD (and optional ACU_SSH; omitted defaults to Administrator@ + base-url host). gmake e2e refuses to start without it.

The tier is self-contained. Each run scaffolds a synthetic single-org company from the packaged acu config init templates into a temporary directory, copies the real .env into it, and runs the installed acu binary from there — no data repo, no pre-existing fixtures on the instance. Scratch tenants (E2E, E2EA, E2EB, E2ESCEN) are created on the way in and always deleted on the way out, so nothing persists. The packaged full config init seed (under config/) is the only scaffold.

gmake e2e                                # whole tier, about 20 minutes
gmake e2e FILE=test_provision_lifecycle  # apply/diff focus
gmake e2e FILE=test_scenario_lifecycle   # scenario + state focus

License

This project is licensed under the PolyForm Noncommercial License 1.0.0. Noncommercial use is free under that license. Commercial use requires a separate license — contact lab5.ca.

See LICENSE and NOTICE.

Copyright 2026 Konstantin Borovik.

Release files for acumatica-cli 0.25.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for acumatica-cli 0.25.2
File Size Uploaded
acumatica_cli-0.25.2.tar.gz 124.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for acumatica-cli 0.25.2
File Interpreter ABI Platform
acumatica_cli-0.25.2-py3-none-any.whl Python 3 none any Details

Total release size: 275.5 kB

Release files / acumatica_cli-0.25.2.tar.gz

Download URL acumatica_cli-0.25.2.tar.gz
Size 124.0 kB
Tags Source
SHA-256 checksum
How to use checksums
b4c3c9aa1b6e95a2976cd6636f04b999dd3e47535747bb142576de729733df33
BLAKE2b-256 checksum
How to use checksums
7b1c1941d982b303c96717770f55df474bdaf420bcd88874df8a37c80de80c7f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release files / acumatica_cli-0.25.2-py3-none-any.whl

Download URL acumatica_cli-0.25.2-py3-none-any.whl
Size 151.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d699f36332774d5318ad7aaacf0a7e30a58fd6aa4d494ee80d6f1378fdc2746
BLAKE2b-256 checksum
How to use checksums
c43dd4a729240f900fd8cc8694edabd0683a3dcc1624398949d37ad8af36baba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.35.0

2 release files

0.34.0

2 release files

This release

0.25.2 This release

2 release files

0.19.0

2 release files

0.18.2

2 release files

0.18.1

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

0.15.4

2 release files

0.15.3

2 release files

0.15.2

2 release files

0.15.1

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.1

2 release files

0.13.0

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page