Skip to content

The Consystence CLI

The csy command-line client is a first-class, supported way to drive the platform — alongside the browser. It is not a thin wrapper: every surface the web UI renders can be driven from the terminal, because the CLI is one of the platform’s peer rendering branches over the same semantic UI tree. For CI, scripts and partner automation it is the primary interface.

The binary is a self-contained, single-file executable named csy (assembly name csy, built on .NET 10) — no separate .NET runtime install is needed. It speaks to org cells, to an on-prem site/edge server, or to the account control plane — selected by the active context (see below). The compiled-in default server URL is https://api.consystence.com, and every command group targets the platform’s cell-, site-, or account-direct /api surfaces.

Each tagged release will ship the CLI two ways:

  • Release binary — a self-contained, single-file build per platform (csy-<version>-linux-x64, csy-<version>-win-x64.exe). There is no runtime to install: download the binary for your platform and put it on your PATH as csy.
  • .NET global tooldotnet tool install --global Consystence.Cli installs the same CLI as a framework-dependent tool, invoked as the same csy command. Unlike the release binary, this channel requires the .NET runtime.

Verify the install:

Terminal window
csy --help # branded help; lists every command group
csy auth status # shows the current credential, if any

A context bundles one endpoint and its credential: a server URL, a server type (cell, site, edge, or account), an auth-mode derived from that type, and an optional tenant/org slug. This is gh-style host targeting — switch contexts to point the same commands at a different cell, a site server, or the account control plane.

Terminal window
# Define a context for your org's cell, then make it current
csy context add prod \
--server https://acme.consystence.com \
--type cell \
--tenant acme
csy context use prod
csy context list # all contexts (current marked with *)
csy context current # endpoint, type, and auth-mode of the current one
CommandPurpose
context listList defined contexts
context currentShow the current context’s endpoint, type, auth-mode
context use <name>Switch the current context
context add <name> --server <url> --type <cell|site|edge|account> [--tenant <slug>] [--api-scope <scope>]Define a new context
context remove <name> [--yes]Delete a context and its stored credential
context rename <old> <new>Rename a context (migrates its stored credential)

The context’s --type is what decides how you authenticate — you never pick an auth flow by hand.

The CLI consumes the platform’s existing MultiAuth schemes; it does not define new ones. There are two ways to authenticate, and the mode is derived from the current context’s type rather than chosen with a flag.

Interactive login (OIDC against Entra External ID)

Section titled “Interactive login (OIDC against Entra External ID)”

For cell and account contexts, csy auth login performs an OIDC sign-in against the id.consystence.com CIAM identity provider and stores the resulting Bearer JWT. The default interactive flow opens your browser for an authorization-code + PKCE sign-in on a loopback redirect; on headless hosts, pass --device for the OIDC device-authorization grant (RFC 8628) and enter a code in any browser.

Terminal window
# Interactive sign-in — opens the browser (authorization-code + PKCE, loopback redirect)
csy auth login
# Headless hosts (SSH, CI) — device-authorization grant: enter a code in any browser
csy auth login --device
csy auth status # mechanism + credential source (identity is `auth whoami`)
csy auth whoami # decode the stored token locally (oid, tid, preferred_username)
csy auth logout # clear the current context's credential (keeps the context defined)

For a site context, the same command runs the site-PIN path (the LocallyIssuedJwt scheme) against the site server:

Terminal window
csy auth login --email [email protected] --pin <site-pin>

For CI, scripts and partner automation, supply a tenant API key (the csk_-prefixed value) and the CLI sends it as the X-Api-Key header; the server selects the ApiKey scheme. Mint keys at POST /api/admin/api-keys — the plaintext value is shown once (see API keys).

Terminal window
# Global flag (highest precedence) — applies to any command
csy org get --org acme --token csk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Or via environment variable (ideal for CI)
export CONSYSTENCE_TOKEN=csk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
csy site list

Credential precedence is: --token flag, then CONSYSTENCE_TOKEN, then the ApiKey field in config, then a stored interactive Bearer token.

The CLI reads ~/.consystence/config.json. The on-disk shape mirrors the context model: a singleton ciam block (authority, clientId, apiScope — the API scope keeps the issued token’s aud on the Consystence API, not Microsoft Graph), a current-context name, and a contexts map keyed by context name. A null ciam.authority falls back to a compiled default for the default browser flow (the --device flow needs it set explicitly); ciam.clientId has no compiled default and must be set before the first interactive login. Leave apiScope unset and the login requests only the base OpenID scopes — the token then lacks the Consystence API audience, so set it before using bearer auth against /api. Context fields are kebab-case: server-url, server-type, auth-mode, api-scope, tenant-slug, api-key, and the CLI-managed token fields (access-token, token-expires-at, refresh-token). The long-lived refresh token is stored in the OS keyring (Windows Credential Manager, macOS Keychain, libsecret), keyed by context; the per-context refresh-token field is a plaintext fallback written only where no keyring exists, and the CLI warns once when it falls back.

{
"ciam": {
"clientId": "<public-client-app-id>",
"apiScope": "api://<api-app-id>/access_as_user"
},
"current-context": "prod",
"contexts": {
"prod": {
"server-url": "https://acme.consystence.com",
"server-type": "cell",
"auth-mode": "ciam-bearer",
"tenant-slug": "acme"
}
}
}

csy <group> <command>. Every group offers --help. The full set of groups:

GroupWhat it manages
accountThe account control plane (account.consystence.com): whoami, set-type, the tenant directory (tenant list / tenant open), plus the self-service org subgroup (create, list, get, licences, request-tier, provision), the member subgroup (invite, revoke-invite, remove), and an internal operator subgroup for platform staff (cell fleet management), not intended for integrators — see the not-yet-GA caveat above
aiThe AI plane: health (provider availability + circuit state) and complete (a test completion; --system, --model, --tier, --max-tokens) over the org-admin-gated /api/ai surface
authSign-in: login, logout, status, whoami
configPlatform config store: list, get, set, unset, audit, reload — see below
contextCLI connection contexts (endpoints + credentials)
controllerControllers (the platform’s term for a PLC) at a site: create, list, get, update, assign, decommission
electricalElectrical power-distribution topology: node create|list|get|update|remove, edge create|list|remove, topology
instanceDevice instances at a site: create, list, get, update, delete, plus the live read verbs tags (current values with quality), trend (a historian window), and commands (the instance’s command-audit ledger)
orgOrg membership and email-domain bindings: get, members, domains
provisionProvisioning-plane operator commands (platform-admin, cloud cell only): re-bootstrap --org [--yes] re-runs an org’s tenant bootstrap from the server’s bootstrap source
serverOn-prem site/edge server: status
siteSites under an org: create, list, get, delete, plus alarms (active alarms, read-only) and commands (the site-wide command-audit ledger)
statusAnonymous public platform-status feed (GET /status.json) — no auth; --url overrides the origin
tenantTenant/org selection, control-plane CRUD (create, list, get — gated, not yet GA), and YAML export / import
trend-importBulk historian import: upload a trend-data CSV plus its tag-mapping CSV as an import batch and poll its status — csy trend-import <file> --mapping <csv> --site <id> [--force] [--poll-interval <s>] [--no-wait], over /api/historian/import-batches — cloud cell only: a site server does not serve the import surface
uiThe interactive renderer/REPL over the semantic UI tree (see below)

Licences are no longer a standalone command group — read them via org get (an org’s licence pools appear inline on its detail) or account org licences <org>.

A few groups are context-sensitive and refuse to run against the wrong context with a clear message and a non-zero exit code rather than misfiring: the server commands target a site/edge server and exit cleanly against a cell context, the control-plane tenant create / tenant list / tenant get verbs are gated off on a cell (see the not-yet-GA caveat above), and the provision surface itself is cloud-only and platform-admin-gated, so it fails server-side against anything else.

Most org-scoped commands accept an explicit --org slug, and otherwise default to the current context’s selected slug:

Terminal window
csy tenant select acme # set the active tenant/org slug locally
csy tenant current # show it
csy org get # org detail (identity + its licences)
csy org members --list # active members
csy org members --pending # pending join requests
csy org members approve <accountId> --role admin

Org membership is exactly owner / admin / member — see roles. The accountId is the member’s stable Entra oid. An org’s licences come back inline on org get; the account control plane also exposes account org licences <org> for the pool/allocation view.

Terminal window
csy site create --code chpp --name "Coal Handling" --region australiaeast
csy controller create --site chpp \
--name "Pump house PLC" --platform PLCnext \
--asset-number PLC-001 --protocol ethernet-ip --ip 192.0.2.20
csy instance create --site chpp \
--name "Process pump 2" --type consystence.pump.centrifugal --asset-number PP-02 \
--controller ctl_7Q4W9K2M \
--tag Running=Program:Pump1.Running

--controller is required: an instance is bound to the controller its tags are acquired from — pass the ctl_… id minted by the controller create step above. Controllers and instances are minted with canonical ids (ctl_…, dev_…) — you supply the operator-facing asset number, not the id. A device type (e.g. consystence.pump.centrifugal) is reusable; an instance is a per-site placement keyed by its asset number. See instances & commissioning.

The tenant group round-trips a site’s configuration as tenant YAML — the same declarative format used for provisioning. Export validates against the route org (you must be owner/admin of it); import validates locally first, then applies.

Terminal window
# Export one site to YAML (stdout, or --output <file>)
csy tenant export --site chpp --org acme --output acme-chpp.yaml
# Validate without applying, then import for real
csy tenant import acme-chpp.yaml --org acme --dry-run
csy tenant import acme-chpp.yaml --org acme --force

--dry-run parses and validates only (no server call); --force overwrites an existing tenant. A file whose org.slug does not match the --org route is rejected. This is the scriptable companion to tenant provisioning.

csy config is the operator surface over the cell’s audited platform-config store. It reads and writes runtime overrides on top of the appsettings/env defaults, with every write validated and audited server-side.

Terminal window
csy config list --prefix Historian # stored overrides + served/effective provenance
csy config get Historian:Retention:DefaultRetentionDays
csy config set Historian:Retention:DefaultRetentionDays 90 --reason "extend retention"
csy config unset Historian:Retention:DefaultRetentionDays --reason "revert to default"
csy config audit --key Historian:Retention:DefaultRetentionDays --days 30
csy config reload # force the cell to re-read the store

Keys are IConfiguration paths. Add --org <slug> to any subcommand to target a tenant’s per-tenant config instead of the global store (that variant is authorised against the org’s own owner/admin, not platform staff). Writes to high-blast-radius key families (Jwt, Oidc, Tenancy, Server, Ai) prompt for confirmation — a wrong authority would trust the wrong IdP for the whole cell — skippable with --yes for scripted use. set/unset surface the server’s warnings verbatim, including the “stored but not served yet” signal for keys the cell does not yet read.

csy ui is not a help menu — it is a full renderer of the server-driven semantic UI tree, a peer rendering branch alongside the browser’s wire renderer. A native WinUI3 client is a further planned branch, but is not yet built — the shipped renderers today are the browser wire renderer and this CLI renderer. It connects to the server’s /hubs/ui-wire SignalR hub with the credential the CLI already resolved and drives the same server-side UI session actor (keyed by sessionId) the browser drives — so what you navigate in the terminal is the identical session model, just rendered as text.

flowchart LR
  CLI["csy ui<br/>CLI renderer + REPL"] -->|"text patches over<br/>/hubs/ui-wire (SignalR)"| Session["UI session actor<br/>keyed by sessionId"]
  Session --> Scene["Semantic UI tree<br/>authoritative scene graph"]
  Scene -.->|"same tree,<br/>other branches"| WEB["Browser wire renderer<br/>same wire format → DOM"]

It has two modes:

Interactive REPL — open a route, inspect the rendered view, fire actions, and wait for state to settle:

Terminal window
csy ui admin/api-keys # open a starting route, then drive it
# REPL verbs: goto · inspect · action · wait

Headless script — run a declarative YAML scenario for CI. The scenario opens routes, performs actions and asserts state, then exits with a structured result:

Terminal window
csy ui --script scenario.yaml --no-color
# exit 0 = all steps passed · 1 = a step failed · 2 = scenario/connect error

The --sentinel flag emits a flushed __csy_ready__ marker before each prompt so a shell driver can read until the session settles — combine it with --no-color and wait until <predicate> for deterministic machine driving. Script mode is what replaces browser click-throughs in integration testing. For the wider rendering model, see multi-renderer architecture.