Benefits Orchestrator (bmo) — Setup and Operations#
bmo is the operator CLI for the Benefits deployment system. It drives one or more Agents — uploading packages, triggering SQL operations via the Command Center service, managing versions, and querying environment state. All configuration lives in appsettings.json alongside the executable, which you edit interactively via bmo config.
Prerequisites#
- Windows (x64)
- Network access to each Agent's HTTP endpoint (default port
5100) - HTTP access to the Command Center service (default port
8090) — see commandcenter-setup-and-operations.md - Azure Blob Storage access if using
--via blobdeploys
Installation#
Developer workstation#
Run Register-BMO.ps1 from the repo root. It publishes the project and sets a bmo alias in your current session:
Operations box#
Copy the published output of Benefits.Orchestrator to a local folder (e.g. C:\Tools\bmo\). bmo.exe and its appsettings.json must sit in the same directory — settings are read from the executable's location.
Add the folder to PATH so bmo is accessible from any prompt.
First-run: bmo config init#
Before using any other command, run the interactive setup wizard:
The wizard covers seven steps:
| Step | What you configure |
|---|---|
| 1 | VersionsPath — local folder where deployment zip files are kept (default F:\Versions) |
| 2 | Command Center service — BaseUrl (e.g. https://cc.internal:8090) and auth mode + credentials for outbound calls to Command Center |
| 3 | Agent client auth — ApiKey, mTLS, or EntraId + credentials for outbound calls to Agents |
| 4 | Package store — Azure Blob AccountUri and container name (skip if not using blob transport) |
| 5 | Service Bus — namespace and topics (skip if using HTTP transport) |
| 6 | Agents — name + BaseUrl for each target server (e.g. http://vmbenefitsbmp1:5100) |
| 7 | Environments — RequiresAcceptedStatus per environment (SQL connection strings no longer configured here — see note below) |
Note on step 7: In v2.0.0 the SQL connection strings moved to the Command Center service's
appsettings.json.bmocommunicates with SQL exclusively via the Command Center HTTP API. The connection string prompt inbmo config initstep 7 is retained for backward compatibility but the values are not used bybmo; configureConnectionStrings.CommandCenter_<Env>in the Command Center service instead.
When finished, verify with:
Managing config after first-run#
All subcommands below edit appsettings.json interactively:
bmo config agent add|remove|list # manage Agent endpoints
bmo config environment add|remove|list # manage per-environment settings
bmo config auth # switch auth mode or update credentials
bmo config package-store # update AccountUri / ContainerName
bmo config servicebus # configure Service Bus transport (opt-in)
Day-to-day workflows#
Global flags#
These are available on every command:
| Flag | Purpose |
|---|---|
--verbose / -v |
Route phase progress and full error/stack-trace detail to the console (normally only logged to file). Use when troubleshooting. |
--yes / -y |
Skip confirmation prompts (for automation). Destructive commands confirm interactively unless this is set; non-interactive sessions never prompt. |
--quiet / -q |
Disable progress spinners/animations. Errors and structured output are unaffected. |
--version |
Print the bmo version. |
Exit codes (consistent across commands): 0 success · 1 operational failure · 2 invalid arguments/usage.
Action commands deploy, manage remove, manage uninstall, and sync-agent also accept -o json for machine-readable per-server/per-package results.
Preflight check#
Verify connectivity before deploying — Command Center reachability (and auth), every configured agent, and package-store configuration:
bmo doctor # human table; exits non-zero if any check fails
bmo doctor -o json # machine-readable, for CI
Deploy a version#
| Argument / flag | Default | Notes |
|---|---|---|
ENV |
— | Target environment (Acceptatie or Productie) |
VERSION |
— | Version string matching the package (e.g. 25.5.29) |
ZIP_PATH |
— | Path to the deployment zip. Required for --via http. |
--server <name> |
all configured agents | Restrict to one or more named agents |
--via |
http |
http: Orchestrator pushes the zip to the Agent. blob: Agent pulls from the package store. |
--archive |
off | Copy the zip into blob storage after an http deploy |
--no-start |
off | Install or update the Windows Service but leave it stopped. The service's StartMode stays Automatic (Delayed) so the next reboot will start it normally. Use this for maintenance windows or coordinated multi-host rollouts. |
--via http — use this when you have a zip locally and want to deploy directly without going through the package store. Useful for development and hotfixes.
--via blob — use this for production deploys. The package must already be in the store (pushed via bmo package push or your CI pipeline). Environments with RequiresAcceptedStatus: true require the package to be in Accepted status before bmo will proceed.
--no-startcaveat: While the service is stopped the Agent's state monitor will emit aWarning-levelHealthAlertlog every monitoring interval (default 5 min). This is expected and resolves once the service is manually started.
Promote a package: UAT → Production#
# 1. Review what is waiting in UAT
bmo package list --status Uat
# 2. Inspect the package manifest
bmo package show 25.5.29
# 3. Promote to Accepted
bmo package accept 25.5.29
# 4. Deploy to production
bmo deploy Productie 25.5.29 --via blob
Reject a package (marks it terminal; no further deploys possible):
Supersede an older version when it has been replaced:
Delete / prune packages from the store (blob + manifest). delete removes one version; prune bulk-removes by status and is a dry run unless --apply:
bmo package delete 25.5.28 # confirmation prompt (skip with --yes)
bmo package prune --status Rejected # dry run: list what would be deleted
bmo package prune --status Superseded --apply # actually delete
Bring up a new Agent server#
Replay all Accepted packages onto a freshly installed server in a single command:
Inspect environment state#
bmo query status # installations on all agents (all environments)
bmo query status Productie # optional positional: filter to one environment
bmo query status --server vmbenefitsbmp1 # single agent
bmo query status --match 25.5.* # narrow to a version family
bmo query status --match 25.5.10..25.5.29 --strict # CI gate: exit 1 if any matched component is not Running
bmo query customers Productie # list all customers
bmo query stages Productie --customer "BCS" # stages for one customer
bmo query versions Productie # versions not yet assigned to any stage
bmo query installed Productie --match 25.5.* --health # version rows with aggregated health column
All query commands accept:
-o table(default) |-o json|-o md— output format--query <jmespath>— filter or reshape JSON output
--match grammar (shared by query status, query installed, and manage uninstall)#
| Shape | Interpretation | Example |
|---|---|---|
/.../ |
Regex (case-insensitive) | /^25\.[5-6]\..+/ |
Contains .. |
Semver range (inclusive bounds; either side may be empty) | 25.5.10..25.5.29, ..25.5.29, 25.5.10.. |
Contains * / ? |
Glob | 25.5.* |
| Otherwise | Exact match | 25.5.29 |
Bare * matches all versions.
query status --strict for CI#
--strict exits non-zero (1) if any matched component is not Running. Combine with --match to gate a release; use it bare to gate the whole fleet. Exit codes:
| Code | Meaning |
|---|---|
0 |
All matched components Running (or no --strict, no unreachable servers) |
1 |
At least one matched component not Running, or at least one server unreachable |
2 |
Invalid --match pattern |
JSON output remains clean (no extra warning text on stdout); the exit code is the only out-of-band signal in -o json mode.
query installed --health#
Off by default. When set, the command adds an aggregated Health column (Running / Partial / Stopped / Unreachable / Unknown) by fanning out one GetAgentInstallations call per distinct server. JSON/Markdown shape is unchanged when --health is absent — existing scripts are not affected.
Deploy history#
Read past version transitions for an environment (newest first) from the Command Center deploy log, with foreign keys resolved to customer / stage / old→new version:
bmo logs Productie # most recent 100 entries
bmo logs Productie --customer "BCS" --since 2026-06-01
bmo logs Productie --limit 20 -o json # machine-readable; supports --query
Rollbacks / downgrades — why there is no bmo rollback#
There is intentionally no rollback command. A customer-stage version change is not just a binary swap: upgrades run forward database migrations driven by the stage's scheduled tasks (TblToolSchedule), and those migrations are generally one-way. Re-deploying older binaries would leave the schema ahead of the application — an unsafe state. Recovery is therefore deliberate:
- Bad binary, schema unchanged: redeploy a prior build explicitly with
bmo deploy <ENV> <oldVersion>. - A true downgrade is a migration-aware operational procedure (schema + scheduled jobs + data), not a one-shot CLI action.
Use bmo logs to inspect what changed and decide deliberately.
Lifecycle and cleanup#
# Dry-run: show what would be removed
bmo manage lifecycle Productie --older-than-days 90
# Apply the cleanup
bmo manage lifecycle Productie --older-than-days 90 --apply
# Remove a specific version from all agents in an environment
bmo manage remove Productie 25.4.10
Version cleanup#
Use bmo query installed <ENV> --unused to see all versions installed on agents that are no longer
assigned to any active customer stage. Then use bmo manage uninstall <ENV> --match <PATTERN> --apply
to remove them.
Dry-run first, then apply:
bmo query installed Acceptatie --unused
bmo manage uninstall Acceptatie --match 25.5.* --apply --yes
Windows Service control#
bmo manage service Productie 25.5.29 Start
bmo manage service Productie 25.5.29 Stop
bmo manage service Productie 25.5.29 Restart
Service resource management#
Adjust CPU priority and affinity on a running Windows Service without RDP access:
bmo manage service-resources <ENV> <VERSION>
[--priority <Idle|BelowNormal|Normal|AboveNormal|High>]
[--affinity <spec>]
[--clear-priority]
[--server <name>]
| Flag | Description |
|---|---|
--priority |
Set process priority. Persisted to the IFEO registry — survives services.msc restarts and reboots. RealTime is not exposed. |
--affinity |
CPU core affinity. Accepts comma-list (0,1,2), range (0-3), hex bitmask (0xF), or decimal core index (15). Apply-once — lost when the service restarts (Windows limitation). |
--clear-priority |
Remove the IFEO registry priority override, restoring the OS default on the next service start. |
--server |
Target specific server(s). Defaults to all configured agents. |
Examples:
# Reduce UAT service to BelowNormal priority on all agents
bmo manage service-resources Acceptatie 25.5.29 --priority BelowNormal
# Pin to cores 0 and 1 only
bmo manage service-resources Acceptatie 25.5.29 --affinity 0,1
# Combined: lower priority and pin to a range
bmo manage service-resources Acceptatie 25.5.29 --priority BelowNormal --affinity 0-3
# Remove the priority registry override
bmo manage service-resources Acceptatie 25.5.29 --clear-priority
Persistence model:
| Setting | Survives services.msc restart? |
Survives reboot? | Survives version redeploy? |
|---|---|---|---|
| Priority (via IFEO registry) | Yes | Yes | No — new version = new executable path. Re-apply after redeploy. |
| Affinity (live process only) | No | No | No |
The service must be running when
service-resourcesis called. Targeting a stopped service returns an error.
IIS pool management#
bmo manage iis Productie_25.5 Recycle
bmo manage iis Productie_25.5 Start
bmo manage iis Productie_25.5 Stop
Customer data#
# Export all customer/stage data for an environment
bmo manage customer-data export Productie C:\Backup\productie.bmpkg
# Export a single customer
bmo manage customer-data export Productie C:\Backup\bcs.bmpkg --customer "BCS"
# Import into another environment
bmo manage customer-data import Acceptatie C:\Backup\bcs.bmpkg
# Inspect a package before importing
bmo manage customer-data show C:\Backup\bcs.bmpkg
# --verbose also scans log-table date ranges and prints a table of the active &
# upcoming tool schedules in the package (Key / Stage / Recurrence / Effective
# window / ToolCode / LastRun) so they can be vetted before import. "Active &
# upcoming" = Active=true and not past EndEffective; future-dated one-shots are
# included because they fire on the target after import.
bmo manage customer-data show C:\Backup\bcs.bmpkg --verbose
# Remove a stage
bmo manage remove-stage Productie <stage-id>
Certificates#
bmo manage certificate list --server vmbenefitsbmp1
bmo manage certificate download --server vmbenefitsbmp1
bmo manage certificate upload --server vmbenefitsbmp1
bmo manage certificate delete --server vmbenefitsbmp1
Configuration reference#
Orchestrator.*#
| Setting | Default | Description |
|---|---|---|
Agents |
{} |
Named Agent endpoints: { name: { BaseUrl, UseSasForBlob } } |
ApiKey |
"" |
Shared secret sent as X-Api-Key to Agents when AuthMode = ApiKey |
AuthMode |
ApiKey |
ApiKey | mTLS | EntraId — for outbound Agent calls |
PollingIntervalSeconds |
3 |
Frequency of job status polls against the Agent |
PollingTimeoutMinutes |
10 |
Maximum wait for a deploy job to complete |
VersionsPath |
F:\Versions |
Local directory used as the default zip source |
Environments.<Env>.RequiresAcceptedStatus |
false |
Reject --via blob deploys unless the package status is Accepted |
Orchestrator.CommandCenter.*#
| Setting | Default | Description |
|---|---|---|
BaseUrl |
"" |
HTTP base URL of the Command Center service, e.g. https://cc.internal:8090 |
AuthMode |
ApiKey |
ApiKey | mTLS | EntraId — for outbound Command Center calls |
ApiKey |
"" |
API key sent to Command Center when AuthMode = ApiKey |
ClientCertificate.* |
— | Certificate settings for AuthMode = mTLS (same shape as ClientCertificate.* below) |
EntraId.TargetScope |
"" |
App Registration scope for AuthMode = EntraId |
ClientCertificate.* (for Agent AuthMode = mTLS)#
| Setting | Default | Description |
|---|---|---|
Source |
Store |
File | Store | KeyVault |
StoreLocation |
LocalMachine |
Windows cert store location (Store source) |
StoreName |
My |
Windows cert store name (Store source) |
Thumbprint |
"" |
Certificate thumbprint (Store source) |
FilePath / Password |
"" |
PFX file path and password (File source) |
KeyVaultUrl / CertificateName |
"" |
Key Vault URL and certificate name (KeyVault source) |
EntraId.* (for Agent AuthMode = EntraId)#
| Setting | Description |
|---|---|
TargetScope |
App Registration scope, e.g. api://<ClientId>/.default |
PackageStore.*#
| Setting | Default | Description |
|---|---|---|
AccountUri |
"" |
Azure Blob Storage account URI |
ContainerName |
deployments |
Blob container holding manifests and zip files |
ConnectionStrings.PackageStore |
— | Full Azure Storage connection string (alternative to AccountUri) |
Auth modes for outbound calls to Agents#
| Mode | How it works | What to configure |
|---|---|---|
ApiKey (default) |
Sends X-Api-Key header |
Orchestrator.ApiKey — must match Authentication.ApiKey on the Agent |
mTLS |
Client certificate in TLS handshake | ClientCertificate.* — thumbprint must be in the Agent's ClientAuthentication.AllowedThumbprints |
EntraId |
Acquires bearer token via MSAL | EntraId.TargetScope — must match the audience configured on the Agent's AzureAd section |
The same auth modes apply to Command Center calls via Orchestrator.CommandCenter.AuthMode.
Service Bus transport (optional)#
By default bmo communicates with Agents over HTTP. To switch to scatter-gather via Azure Service Bus, add a ServiceBus section to appsettings.json or run bmo config servicebus:
"Orchestrator": {
"ServiceBus": {
"Namespace": "my-namespace.servicebus.windows.net",
"SnapshotContainerUrl": "https://...",
"CommandsTopic": "deploy.commands.v1",
"RepliesTopic": "deploy.replies.v1"
}
}
When ServiceBus.Namespace is set it replaces the HTTP transport for deploy operations. Each Agent must also have Agent.ServiceBus.Namespace configured to match. See agent-setup-and-operations.md for the Agent-side config.
Troubleshooting#
| Symptom | Likely cause | Fix |
|---|---|---|
ZIP_PATH is required |
Using --via http without a zip argument |
Add the path or switch to --via blob |
401 / 403 calling Agent |
Auth mismatch | Compare bmo config show with Authentication.ApiKey on the Agent |
401 / 403 calling Command Center |
Auth mismatch | Compare Orchestrator.CommandCenter.ApiKey in bmo config show with ClientAuthentication.ApiKey on the Command Center |
Deploy blocked on RequiresAcceptedStatus |
Package not promoted yet | Run bmo package accept <version> first |
Agent not in bmo query status |
Wrong URL or Agent unreachable | Check Orchestrator.Agents.<name>.BaseUrl; test with curl http://<host>:5100/health |
| Command Center unreachable | Service stopped or wrong URL | Check Get-Service Benefits-Mgmt-CommandCenter on the CC host. Verify Orchestrator.CommandCenter.BaseUrl. |
bmo cannot find appsettings.json |
Running bmo from wrong directory |
Run from the directory that contains bmo.exe |
| Certificate load error | Cert not in store or wrong thumbprint | Verify with Get-ChildItem Cert:\LocalMachine\My \| Where-Object Thumbprint -eq '...' |
service-resources returns "service not running" |
Targeting a stopped service | Start the service first with bmo manage service <ENV> <VERSION> Start, then apply resource settings |
Operator checklist#
- [ ]
bmo config initcompleted without errors - [ ]
bmo config showreviewed — all agents, environments, Command Center URL, and credentials correct - [ ] Command Center health check passes:
curl http://<cc-host>:8090/healthzreturnsHealthy - [ ]
bmo query statusreturns health for all expected agents - [ ]
bmo package listreturns results (package store connected) - [ ]
bmo deploy Acceptatie <version> <zip> --via httpsucceeds on a test package - [ ]
bmo manage lifecycle Acceptatie --older-than-days 90(dry-run) completes without errors - [ ] For production:
bmo package accept <version>→bmo deploy Productie <version> --via blobsucceeds - [ ]
ApiKeystored securely — not committed to source control