blog
Configuration-as-code for Acumatica: a tenant built from YAML in git
· updated · acumatica · configuration-as-code · erp-gitops · acumatica-cli · yaml
TLDR
Acumatica configuration lives in the web UI with no reviewable history, so tenants drift and auditors get screenshots.
YAML in git plus acu apply and acu diff turns a credit-terms edit into a pull request, an idempotent upsert, and a zero-drift proof with an exit code.
The data lives in kborovik/acumatica-gitops; the CLI is kborovik/acumatica-cli.
Configuration lives only in the web UI
Acumatica configuration lives in wizards and screens with no file history, so a second environment is set up from memory and screenshots and drifts until something breaks.
Acumatica configuration lives in the web UI: wizards, screens, and manual data entry. When someone changes a field on one tenant — credit terms, a posting class, an order type — there is no file that records the previous value, the new value, or the reason for the change. A second environment is usually set up from memory and screenshots, so it drifts from the first without anyone noticing until something breaks. When an auditor asks what changed and why, the answer is a folder of screen captures rather than a reviewable history.
Two tenants that should match cannot be compared by a machine. There is no pull request for a credit-terms edit, and no automated check that the live tenant still matches what was intended.
Desired config as YAML, apply and diff as the loop
Treat desired configuration as YAML in git: acu apply writes idempotent keyed PUT upserts, acu diff exits 0 on zero drift and 2 on drift.
Treat desired configuration as YAML in a git repo instead of as state that only lives in the UI.
acu apply writes that YAML to the tenant as idempotent keyed PUT upserts.
acu diff reads the live tenant and compares it to the same files; exit 0 means zero drift, exit 2 means drift.
A configuration change becomes a file edit that can be reviewed, applied, and proven against the live system.
The configuration data lives in kborovik/acumatica-gitops.
The CLI that applies and diffs it lives in kborovik/acumatica-cli and on PyPI as acumatica-cli.
This post covers the configuration repo and the apply/diff loop.
Transaction scenarios are the next post; CLI internals are a later post (0005).
A config change as a pull request
The daily path is almost always one field in one file: dry-run apply, apply, then diff exit 0 — reviewed like any other infrastructure change.
The daily path is not a full rebuild of the tenant. It is almost always one field in one file, reviewed the same way any other infrastructure change is reviewed.
Credit terms live in bootstrap/credit-terms.yaml.
A change from net-30 to net-45 is a two-line edit:
- DayDue00: 30
- Descr: Net 30 Days
+ DayDue00: 45
+ Descr: Net 45 Days
The reviewer reads that diff instead of a screenshot of CS206500. Preview the write without changing the tenant:
acu apply --dry-run bootstrap/credit-terms.yaml
Ship it:
acu apply bootstrap/credit-terms.yaml
Prove the live tenant matches the repo:
acu diff bootstrap/credit-terms.yaml
diff exits 0 when every sourced field matches.
If the same field was also changed in the web UI and never written back to YAML,
diff names the entity and field, shows source versus live, and exits 2.
A scheduled job that runs acu diff turns that exit code into an alert.
Re-running apply is safe: each record is a keyed upsert, not a recreate.
Rebuild from an empty tenant
The trust test is a virgin tenant over REST: publish AcuBootstrap, apply bootstrap → baseline → setup, then diff must exit 0 — no SSH required.
The trust test is a virgin tenant, not a warm one that already carries partial setup. Do not apply this sample onto a tenant that already has company setup, features, or master data. The empty tenant must already exist on the instance; creating tenants is optional and is the only step that needs SSH.
Point acu at the empty tenant (.env or flags), then:
acu bootstrap
acu apply
acu diff
acu bootstrap publishes the AcuBootstrap package over REST into the session tenant.
No SSH is required.
Bare acu apply with no paths walks bootstrap/, baseline/, and setup/ in that order.
Day-to-day re-apply of an already-seeded tenant can pass baseline/ and setup/ only and skip bootstrap seeds that are already in place.
Optional, only if you need a new login on the instance and have host access: acu tenant create creates the tenant and publishes AcuBootstrap, then the same apply / diff path runs.
That control-plane step needs ACU_SSH (or --ssh); everything else in this post is REST.
The repo README documents both the default REST path and the optional create path.
What the seed files look like
The sample company is a full working configuration — bootstrap seeds, baseline upserts, setup actions with done_when probes, and numbered files for dependency order.
The sample company is Lab5 Gateway Works (AcctCD: LAB5): a single-branch USD manufacturer that assembles IoT gateway kits.
The seed tree is a full working configuration, not a fragment: GL, inventory, sales and purchase order types, AR/AP/CA, 6 vendors, 7 customers, 9 parts, 3 kits (GW-EDGE, GW-CELL, GW-RAIL), and kit specifications.
| Path | Role |
|---|---|
bootstrap/ | Virgin-tenant seeds (features, company, credit terms) and project.xml (AcuBootstrap contract) |
baseline/ | Config upserts (GL through kits) |
setup/ | One-shot actions with a done_when probe (financial year, master calendar, open periods) |
scenario/ | Transaction scenarios for acu run only; never applied or diffed (regression-testing post) |
target.yaml | Verified Acumatica ERP build and Default API generation (not secrets) |
.env | Instance URL, tenant, credentials (gitignored; created locally after clone) |
State files use one domain per file: entity, key, records.
A short real example is the posting ledger:
entity: Ledger
key: LedgerID
records:
- LedgerID: ACTUAL
Description: Actual Ledger
Type: Actual
That is baseline/30-ledger.yaml.
Dual-served entities add a symbolic endpoint: line (bootstrap or default) so the seed tracks the bootstrap contract or the configured Default API version without a hard-coded path pin.
Numbered filename prefixes encode dependency order because directory expansion sorts alphabetically. Subaccounts (10) come before accounts (20) before ledger (30). Warehouse and locations (53) come before the item classes that reference them (54).
bootstrap/ and baseline/ are desired state: apply upserts them, diff proves them.
setup/ is different: each file is an action (for example generate financial year) with a done_when probe.
If the probe already finds the result, apply skips the action and diff reports no drift for it.
If the probe is empty, apply invokes the action and diff reports that the setup step is not done.
Verified target matrix in the repo
.env stays local for secrets; target.yaml commits the Acumatica ERP build and Default API generation the seeds were proven against, and live commands refuse a mismatch.
Connection details stay local rather than in the repo.
.env holds ACU_* variables (ACU_BASE_URL, ACU_TENANT, ACU_USER, ACU_PASSWORD; optional ACU_API_VERSION).
ACU_SSH is optional and only needed for acu tenant create.
The file is gitignored; flags can replace it for one-off runs.
What the repo does commit is the platform matrix the seeds were verified against.
target.yaml is small on purpose:
erp: "26.101.0225"
default_api: "25.200.001"
erp is the Acumatica ERP build the configuration tree was last proven on.
default_api is the Default API generation the REST data-plane commands expect
(the path pin behind entities that ride the Default contract rather than the bootstrap package).
Neither field is a secret; both are the contract between this seed repo and the instance it is allowed to talk to.
Live data-plane commands refuse to run when default_api and ACU_API_VERSION disagree.
That check stops a quiet mismatch where the CLI talks a different endpoint generation than the one the YAML was written for.
acu config check runs the same preflight before a longer apply: discovery, secrets, target match, REST, and endpoints (SSH only if configured).
Bump the matrix in a pull request when you intentionally retarget a newer Acumatica ERP or API build;
until then, a wrong version fails closed instead of applying against an untested combination.
Tenant config as a build artifact
The tenant configuration becomes a build artifact: rebuild from empty, review in a pull request, detect drift with a non-zero exit code — substrate for the regression gate in the next post.
The tenant configuration becomes a build artifact with the same properties other infrastructure already has: rebuild from empty, review in a pull request, and detect drift with a non-zero exit code. The instance that host runs on is built by kborovik/acumatica-devops; see the DEV/TEST environments post.
This repo is also the substrate for automated regression testing. After apply, a scenario can buy parts, assemble kits, sell, invoice, and collect, then assert inventory and GL deltas. That gate is the next post.
Links
- Data repo: kborovik/acumatica-gitops
- Tool repo: kborovik/acumatica-cli (PyPI:
acumatica-cli) - Instance automation: kborovik/acumatica-devops (DEV/TEST environments post)
- Regression gate: Automated regression testing for Acumatica configuration-as-code
- Verified on: Acumatica ERP 26.101.0225, Default API
25.200.001(seetarget.yaml; Bootstrap contract version inbootstrap/project.xml)