statelyai
statelyai discovers local machine source files and can connect them to Stately Studio. Use it locally to inventory machines, or connect a project to open, compare, push, and pull machines.
statelyai discovers local machine source files and can connect them to
Stately Studio. Use it locally to inventory machines, or connect a project to
open, compare, push, and pull machines.
Run the CLI
Run without installing:
npx statelyai --helpOr install it globally:
npm install --global statelyaiChoose a workflow
| Goal | Command | Writes |
|---|---|---|
| List local machines | statelyai scan | Nothing |
| Edit one local file visually | statelyai open <file> | Local file, when you save in the editor |
| Set up local discovery | statelyai init --local --scan | Local statelyai.json |
| Set up project sync | statelyai init --scan | Remote project and local statelyai.json |
| Inspect project machines | statelyai status | Nothing |
| Preview project uploads | statelyai push --dry-run | Nothing |
| Upload local machines | statelyai push [file] | Remote machines and local @statelyai IDs |
| Download project machines | statelyai pull | Linked local files and new files under newMachinesDir |
| Compare two machines | statelyai diff <source> <target> | Nothing |
| Fail on differences in CI | statelyai diff <source> <target> --fail-on-changes | Nothing |
Quick start
For hosted Stately, authenticate once:
statelyai loginInitialize the current repository and scan for XState files:
statelyai init --scan--scan suggests include globs and asks before saving them. Without
--scan, init creates statelyai.json with an empty include list.
It also lists every discovered machine with its symbol, file, line, and linked
Stately ID when present.
For local discovery without authentication or a Studio project:
statelyai init --local --scan
statelyai statusRun statelyai init later to connect the local config to a Studio project
without losing its discovery globs.
To list machines without creating any config:
statelyai scanPreview the files and remote operations:
statelyai push --dry-runThen push and pull:
statelyai push
statelyai pullpush creates remote machines for unlinked source machines, updates already
linked machines, and writes returned IDs into source comments. pull updates
linked files. If newMachinesDir is configured, it also creates local files
for machines that exist only in the Studio project.
Authentication
Browser OAuth is the default:
statelyai loginUse an API key instead:
statelyai login --api-keyCreate or manage keys in Stately API key settings.
For noninteractive input:
printf '%s\n' "$STATELY_API_KEY" | statelyai login --stdinFor automation, prefer an environment variable instead of storing a credential. Resolution order is:
STATELY_ACCESS_TOKENSTATELY_API_KEYNEXT_PUBLIC_STATELY_API_KEY- the credential stored by
statelyai login
The CLI loads .env.local from the current directory before resolving these
variables.
Use statelyai status --auth to report the selected source without printing
the credential. statelyai logout removes stored credentials but does not modify
environment variables.
Stored credentials use macOS Keychain or Linux Secret Service when available,
with a private config file as fallback. Set STATELYAI_CREDENTIALS_BACKEND=file
to force file storage, or STATELYAI_CONFIG_DIR to choose its directory.
Stored OAuth sessions refresh automatically before expiration. If the OAuth
server rejects the refresh token, the CLI removes the unusable credential and
directs you to run npx statelyai login. Transient refresh failures preserve
the credential so a later command can retry.
statelyai.json
push and pull use statelyai.json from the current directory unless
--config supplies another path.
{
"$schema": "https://stately.ai/schemas/statelyai.json",
"version": "1.0.0",
"projectId": "project_123",
"studioUrl": "https://stately.ai",
"defaultXStateVersion": 5,
"include": ["src/**/*.ts"],
"exclude": ["**/*.test.*", "**/*.spec.*"],
"newMachinesDir": "src/machines"
}| Field | Purpose |
|---|---|
$schema | Published JSON Schema URL. |
version | Config format version. Currently 1.0.0. |
projectId | Remote Studio project ID. Omitted for local-only projects. |
studioUrl | Studio API origin. Omitted for local-only projects. |
defaultXStateVersion | XState version used when creating remote machines. Minimum 5. |
include | Source globs used by project-wide push and pull. |
exclude | Globs removed from discovery. Defaults to tests and specs. |
newMachinesDir | Destination for remote-only machines created by pull. |
Project-wide discovery identifies configured JavaScript and TypeScript files
that import XState and call createMachine(...) or .createMachine(...).
Mutating project commands rewrite legacy configs with one sources entry to
the current top-level shape. status and push --dry-run normalize legacy
config in memory without writing it. Multiple legacy entries cannot be merged
safely and require manual migration.
Commands
login
Store an OAuth credential or API key.
statelyai login
statelyai login --api-keyFlags: --api-key, --stdin, --base-url.
logout
statelyai logoutlogout deletes stored credentials. Environment variables are unchanged.
status
Inspect the configured project and classify local and remote machines:
statelyai statusStatuses are linked, local-only, remote-only, and missing-remote.
Local-only configs require no credentials and never contact Studio.
Use --json for automation or --auth to show only credential resolution.
status is read-only and never migrates statelyai.json.
Flags: --config <path>, --base-url <url>, --json, --auth.
init
Create a local config or reuse a Studio project and write statelyai.json.
statelyai init --name "Checkout" --visibility Private --scan
statelyai init --local --scanFlags:
--name <name>sets the remote project name.--visibility Private|Public|Unlisteddefaults toPrivate.--scanproposes source globs interactively.--localskips authentication and remote project creation.--forcereplaces an existing config.--base-url <url>overrides the Studio API origin.
scan
List every local XState machine without authentication or statelyai.json:
statelyai scan
statelyai scan --jsonThe output includes the machine symbol, source file, line, and linked Stately ID when present.
open
Start a local bridge and open one source file in the browser editor:
statelyai open src/checkout.machine.tsSaved file changes refresh the editor. Saving visual edits writes them back to the source file.
Flags:
--editor-url <url>selects the editor origin. Default:https://editor.stately.ai.--host <host>sets the local bridge host. Default:127.0.0.1.--port <port>selects a port. Default: a random available port.--no-openstarts the bridge without launching a browser.--debuglogs editor protocol messages.
diff
Compare two locators after normalizing them to graph form:
statelyai diff src/checkout.machine.ts machine_123 --fail-on-changesLocators may be:
- a local JavaScript or TypeScript machine file
- a local XState JSON, Stately graph JSON, or Studio digraph JSON file
- a Studio machine ID
- a Studio machine URL
--fail-on-changes exits with status 1 when the normalized graphs differ.
Use --base-url for remote IDs at another Studio origin. plan remains a
hidden compatibility alias for diff.
push
Push every discovered machine in statelyai.json:
statelyai pushPush one file while still using the project and defaults from the config:
statelyai push src/checkout.machine.tsPreview discovery and link/update decisions without a credential:
statelyai push --dry-run--dry-run does not create or update machines, write @statelyai IDs, or
migrate legacy configuration.
Flags: --config <path>, --base-url <url>, --dry-run.
pull
Pull all linked files from the configured project:
statelyai pullPull a linked file using its @statelyai ID:
statelyai pull src/checkout.machine.tsPull a machine ID or URL to an explicit target:
statelyai pull machine_123 src/checkout.machine.tsNew targets support JavaScript/TypeScript, .digraph.json, and .graph.json.
For existing JSON targets, the current file shape determines the output format.
Project-wide pull skips linked files with uncommitted Git changes. Pass
--force to overwrite them. Remote-only machines are skipped until
newMachinesDir is set.
Flags: --config <path>, --base-url <url>, --force.
CI examples
Fail when a local machine differs from Studio:
npx statelyai diff src/checkout.machine.ts machine_123 --fail-on-changesProvide STATELY_API_KEY or STATELY_ACCESS_TOKEN through the CI runner's
secret environment.
Check project discovery without network credentials or writes:
npx statelyai scan
npx statelyai push --dry-runSet NO_COLOR=1 or CI=true for plain output.
Common problems
- The server returns
401: runstatelyai login, or setSTATELY_ACCESS_TOKENorSTATELY_API_KEY. - A remote command says
Not logged in: runstatelyai login, then retry, or usestatelyai init --localfor a local-only project. Add--forceto replace an existing config. pushfinds no files: checkincludeandexclude;initwithout--scanintentionally leavesincludeempty.- You only need local inventory: run
statelyai scanor initialize withstatelyai init --local --scan; neither requires authentication. pushmatches files but finds no machines: project discovery currently requires XState imports pluscreateMachine(...)orsetup(...).createMachine(...).- Remote-only machines are skipped: set
newMachinesDir, then runstatelyai pullagain. pullrefuses to overwrite a file: commit or stash its changes, or pass--forceif overwriting is intentional.- A linked remote machine was deleted or is inaccessible: interactive
pushoffers to relink it as a new remote machine and replaces the local ID.
Run statelyai <command> --help for generated command syntax and flags.
Self-hosting
Skip login when the server has authentication disabled. The CLI sends no
authorization header when no credential exists; the server decides whether
authentication is required. This also applies to open editor-sync requests
and CI commands.
For OAuth, point login at the deployment's protected resource:
statelyai login --base-url https://editor.example.com/api/mcpPassing only an origin uses its /api/mcp resource. The resource must
advertise an authorization server. If that server does not support dynamic
client registration, set STATELY_OAUTH_CLIENT_ID.
URL settings are intentionally scoped:
| Setting | Used by | Meaning |
|---|---|---|
--base-url | init, status, diff, push, pull | Studio API origin. |
studioUrl | statelyai.json project sync | Default Studio API origin for that project. |
login --base-url | login only | OAuth protected-resource URL used for discovery. |
--editor-url | open only | Visual editor origin. Default https://editor.stately.ai. |
A complete setup may look like:
statelyai login --base-url https://editor.example.com/api/mcp
statelyai init --base-url https://studio.example.com --scan
statelyai open src/checkout.machine.ts \
--editor-url https://editor.example.comFor enterprise self-hosting options, see pricing and contact us.