CLI automation contract
Exit statuses, error codes, JSON results, and resuming uncertain changes.
Scripts, CI jobs and agents can rely on everything on this page: command names, options, exit statuses, error codes and the JSON result and error shapes. From 1.0 these follow semver. New commands, options, error codes and result fields can arrive in a minor release, so ignore fields you do not know. Human-readable output, messages and help text are prose and may change in any release; branch on code and the exit status, never on a message.
The machine-readable contract ships with the CLI in node_modules/@domino-sdk/relay-cli/cli/contract/. contract.json lists every command with its arguments, options, stability and recovery (see Recover by command), plus the exit statuses, error codes, environment variables and scope precedence. schemas/ holds JSON Schemas (draft 2020-12) for each command's result (schemas/results/<command>.schema.json), the error and the stderr events, and for the files the CLI reads: files in contract.json maps relay.json and the checkout's domino.json to their schemas in schemas/files/. Both files are strict, so a key the CLI does not know fails; a later release still accepts every file an earlier one wrote. Result schemas accept fields they do not name, so a JSON Schema validator such as Ajv accepts a result a later release extends; only the variants of a oneOf, such as a quest's requires entries, reject unknown fields. fixtures/ has an example of each. index.mjs exports the same schemas as zod objects by name (schemas), with the registry of exit statuses and error codes, for tooling that runs in Node.
Client identification
Management API calls and CLI device sign-in calls send X-Domino-Client: @domino-sdk/relay-cli/<version> (node), using the installed CLI's manifest version. The server records validated identities in public request logs. Client metrics use fixed family/runtime labels; exact versions do not create metric labels.
This header grants no access and does not pin an API version. CLI calls retain their existing JSON contracts and do not claim the SDK's tolerant-v1 response capability. Server additions must remain compatible with older and unidentified clients.
Run a command for automation
domino storage list --json returns { "stores": [...] } in test, or live's stores with --environment live. Each store includes kind, name, declared, and createdAt (null until provisioned); active declarations also include description. Unknown future store kinds are preserved in results; consumers should tolerate them. Provider IDs are never returned. See hosted storage for declaration and retention rules. The result schema and example ship as storage-list.schema.json and storage-list.json in the contract's result directories. With publish, domino storage tables NAME, rows NAME TABLE [--cursor C], keys NAME [--prefix P] [--cursor C] and get NAME KEY print the management API's browse answers (storage-tables, storage-rows, storage-keys, storage-get schemas); rows pages by 50 and keys by 100, passing a page's nextCursor back as --cursor for the next page. domino storage delete KIND NAME removes a store the serving build no longer declares and asks for confirmation unless --yes is given; a declared store fails with 409. domino storage clear KIND NAME empties SQL tables (--table, or --all-tables with --keep) or deletes KV keys (--prefix or --all-keys) in test only; --dry-run counts, and without --yes it fails with INVALID_ARGUMENT when it cannot prompt. Its result is the API's clear answer; for KV, keys sums every batch.
Pass --json. The CLI then never prompts, prints the result as one JSON value on stdout, and reports a failure as one JSON object on the last line of stderr:
if result=$(domino deploy --preview --environment test --json 2>errors.log); then
echo "$result" | jq .deployment
else
status=$?
tail -n 1 errors.log | jq -r .code
fi- stdout carries only the result. With
--jsonthat is exactly one JSON value followed by a newline, including[]for an empty list andnullwhen the API answered with no content. Keep stdout and stderr apart when parsing (no2>&1). A reader that closes stdout early, as| headdoes, ends the CLI with status 0. - stderr carries progress, warnings, prompts and the error. With
--json, lines before the error are prose or JSON events (see Events); only the last line is the error. - Secret settings never appear in output.
deployprints each release'sdeploymentDefaults,customValuesandeffectiveSettingswithout them, and lists the set ones inwithheldSettings;quests list,releases getand the other reads do the same (secret settings). A deploy log is safe to keep. - Prompts appear only when stdin and stderr are both terminals,
CIis unset, and neither--jsonnor--no-interactiveis given. A missing value that would be prompted for fails withINPUT_REQUIREDinstead.deploy,stageandsecrets deleteask for confirmation only when they can prompt;--yesskips it.members erase, which can't be undone, asks when it can prompt and otherwise fails withINVALID_ARGUMENTunless--yesis given; so doesmembers ban, which blocks the member at once (its--dry-runpreviews without asking).
Exit statuses
| Status | Outcome | Meaning |
|---|---|---|
| 0 | success | The command completed. The result is on stdout. |
| 1 | failed | The command failed and nothing it was asked to change is uncertain. doctor, check and migrate also print their result on stdout, so read both. |
| 2 | failed | Usage error: unknown command or option, or a missing or invalid argument. The CLI stopped before contacting the API or changing files. Fix the invocation; do not retry it. |
| 3 | uncertain | A change was sent, but whether it took effect is unknown. Resume it with the error's operation, or check the state before retrying. |
| 130 | cancelled | Cancelled by SIGINT (Ctrl-C) or at a prompt. If a change had already been sent, the status is 3 instead; see Cancellation. |
| 143 | cancelled | Cancelled by SIGTERM. If a change had already been sent, the status is 3 instead. |
Every error code maps to one status, except CANCELLED: 130 for SIGINT or a prompt, 143 for SIGTERM. outcome in the error and the exit status always agree.
Errors
With --json, a failure's last stderr line is:
{
"error": "Publication of dep_1 was not confirmed: The API did not confirm the change (ECONNRESET). It may have taken effect. Resume with domino deploy --resume dep_1.",
"code": "OUTCOME_UNCERTAIN",
"outcome": "uncertain",
"details": { "cause": "NETWORK_ERROR" },
"operation": {
"kind": "deploy.publish",
"id": "dep_1",
"scope": {
"apiUrl": "https://relay.domino.run",
"organization": "org_acme",
"project": "spring",
"environment": "test"
},
"resume": [
"deploy",
"--resume",
"dep_1",
"--api-url",
"https://relay.domino.run",
"--organization",
"org_acme",
"--project",
"spring",
"--environment",
"test"
]
}
}| Field | Present | Meaning |
|---|---|---|
error | always | A message for people. It may change. |
code | always | One of the stable codes below. |
outcome | always | failed, uncertain or cancelled, matching the exit status. |
status | API answers | The management API's HTTP status. |
apiCode | API answers | The management API's own code, listed in API error codes. Unknown API codes are kept here; code still classifies them. |
details | some codes | Code-specific data, such as the fields of MANIFEST_INVALID or the Git phase of CHECKOUT_GIT_FAILED. |
operation | resumable change | Only on OUTCOME_UNCERTAIN and CANCELLED: what was sent and how to finish it. See Resume an uncertain change. |
Without --json the same failure is Error: <message>, then, for a field error such as MANIFEST_INVALID, one indented <field.path>: <message> line per invalid field, then Resume: domino … when there is an operation.
A failure from the typed management client follows the same codes. A refusal is classified by its HTTP status, with the API's code in apiCode. An apiCode of promotion-required is classified as ENVIRONMENT_UNSUPPORTED instead. A change that was sent and then lost, timed out or cancelled, or that was answered with 500, 502 or 504, is OUTCOME_UNCERTAIN. Input the client refused before sending is INVALID_ARGUMENT.
Minor releases can add error codes and operation kinds, and the schemas accept them. Treat a code you do not know by its outcome and the exit status.
Error codes
| Code | Exit | Meaning |
|---|---|---|
UNKNOWN_COMMAND | 2 | No command or subcommand has this name. |
UNKNOWN_OPTION | 2 | The command does not accept this option. |
INVALID_ARGUMENT | 2 | An argument, option value or piped input is invalid, there are too many arguments, or options conflict. |
INPUT_REQUIRED | 2 | A required argument, option, subcommand or piped input is missing, and the CLI cannot prompt for it. |
AUTH_REQUIRED | 1 | No credential. Run domino login, or set RELAY_MANAGEMENT_TOKEN. If a sign-in is saved only for another API URL, details.savedApiUrls lists those URLs; pass one as --api-url. |
AUTH_INVALID | 1 | The API rejected the credential (HTTP 401), or it belongs to another organization. |
PERMISSION_DENIED | 1 | The credential lacks permission for this action (HTTP 403). |
LOGIN_FAILED | 1 | Browser sign-in failed, expired, or was approved for another organization. |
UNATTENDED_LOGIN_UNAVAILABLE | 1 | Browser sign-in needs a person, and DOMINO_AGENT_MODE=unattended says none is present. Set RELAY_MANAGEMENT_TOKEN instead. |
SCOPE_INVALID | 1 | A scope value from the environment, relay.json or the checkout is malformed. details.source says where. |
SCOPE_REQUIRED | 1 | Several organizations or projects match, or a resume lacks part of its pinned scope. Pass the option in details.option. |
SCOPE_NOT_FOUND | 1 | The credential cannot reach the selected project in the selected environment, or any project. |
SCOPE_PLACEHOLDER | 1 | relay.json still names a starter's placeholder organization (local). Run domino checkout, or pass --organization and --project. details.file names the manifest. |
PROJECT_MOVED | 1 | The project moved to another organization. See troubleshooting. |
ENVIRONMENT_UNSUPPORTED | 1 | The command does not run in this environment, such as deploy --promote in test, promote in live, or deploy --resume and deploy rollback in live. A definition change in live gets it too, with apiCode: "promotion-required" and status: 422; make the change in test. |
PROJECT_NOT_FOUND | 1 | No relay.json was found, or the --config file does not exist. |
MANIFEST_INVALID | 1 | relay.json is invalid. details lists each field with a suggestion. |
PROJECT_INVALID | 1 | The project lacks configuration the command needs, such as app in relay.json. |
BUNDLE_FAILED | 1 | Bundling the configured quests, types, engagement rules or images failed. |
LOCAL_CONFLICT | 1 | Local files are in the way: a directory exists, a file would be overwritten, or changes are uncommitted. |
CHECKOUT_GIT_FAILED | 1 | A Git step of checkout or create failed. See troubleshooting. |
CHECK_FAILED | 1 | check found blocking diagnostics, listed on stdout, or the app's check script failed. |
DOCTOR_UNHEALTHY | 1 | doctor found a check with status error. Its result is on stdout. A missing relay.json is a warning, not an error. |
MIGRATION_BLOCKED | 1 | migrate found conflicts and wrote nothing. Its result is on stdout. |
BAD_REQUEST | 1 | The management API rejected the request as invalid (HTTP 400). |
NOT_FOUND | 1 | The resource does not exist (HTTP 404). |
CONFLICT | 1 | The resource is not in a state that allows the change (HTTP 409). |
REJECTED | 1 | The API refused the change (HTTP 422 or another 4xx). |
RATE_LIMITED | 1 | Too many requests (HTTP 429). Wait before retrying: details.retryAfter, when the API sent Retry-After, is how long in milliseconds. |
SERVER_ERROR | 1 | The API failed while reading (HTTP 5xx), or answered 501 or 503. details.retryAfter, when present, is how long to wait in milliseconds. |
PREFLIGHT_FAILED | 1 | Hosting is not ready for this project. The message lists the blocking checks. |
STAGE_BUILD_FAILED | 1 | The cloud build failed. Read it with domino logs <details.build>. |
NETWORK_ERROR | 1 | The API could not be reached before any change was sent: DNS, connection, TLS or a redirect. Or a read's answer broke off, or a deployment preview (which publishes nothing) lost its connection. |
TIMEOUT | 1 | A read, or a deployment preview (which publishes nothing), did not finish within 30 seconds, retries included. |
INVALID_RESPONSE | 1 | The API answered a read with something the CLI cannot use. |
OUTCOME_UNCERTAIN | 3 | A change was sent, then the connection failed, timed out, was interrupted, or the API answered 500, 502 or 504. details.cause says which. |
CANCELLED | 130 or 143 | Interrupted or declined before any change was sent: 130 for SIGINT or a prompt, 143 for SIGTERM. For a signal, details.signal names it. |
NODE_VERSION_UNSUPPORTED | 1 | Node is older than the CLI supports. details.minimumVersion is the required version (such as 24.5.0), details.minimum its major version, details.current the running one. Install the latest release of that major and rerun. |
COMMAND_FAILED | 1 | Any other failure. Read the message. |
Retries and deadlines
The CLI sends every management request through the same transport as createManagementClient from @domino-sdk/relay/server, so the two share these deadlines, retries and failure classes; the CLI never follows a redirect. Every request has 30 seconds, retries and the answer's body included. The CLI sends a request again, at most three times in all, only when the API applies a repeat at most once: reads, deploy publishing its test preview, promotion to a live review request, a live deployment submission by preview ID, stage submitting its build request ID, quests archive and quests unarchive with their action ID, groups create, groups add and groups remove with their action ID, webhooks create and webhooks rotate-secret, webhooks pause, webhooks resume and webhooks replay (which send an action ID, generated when you pass no --action-id), every saved resource change (see Resource commands) and every --action-id change, and project creation with its saved action ID. Creation also retries repository provisioning, which reserves one repository per project. It retries when no answer arrived, when the answer broke off after its headers, on HTTP 500, 502, 503 and 504, and on a 429 whose Retry-After fits in the time left. If an error body is cut off, its status still controls retries and its Retry-After is kept in details.retryAfter. It waits Retry-After when the API sent one, and otherwise up to 0.25 seconds, then up to 1 second. A failure that remains is the one reported. Other changes, including domino api with any method but GET, are sent once, unless the answer is 503 with Retry-After: Relay answers that only to a request it refused before doing anything, so every request is sent again after it. deploy's preview request is sent once too, but it only saves a draft, so a timeout or lost answer there is TIMEOUT or NETWORK_ERROR, never uncertain: nothing was published. An uncertain response includes details.recovery: "reconcile" unless it names a saved operation. Read the affected resource before deciding whether to repeat the change, because an actionId alone does not prove a route keeps receipts; the command's recovery in contract.json says when a repeat is safe. A timeout, an unreadable answer and a refusal such as a 400 or 409 are never retried.
Recover by command
Each command in contract.json has a recovery: what to do when it exits 3 with OUTCOME_UNCERTAIN. Read it before the run, because some values need an ID you pass up front. recoveries in contract.json describes each value.
recovery | Commands | After an uncertain change |
|---|---|---|
resume | Saved changes: every command with --operation-id, deploy, stage, operations resume | Run the error's operation.resume. A run with --action-id instead of a saved operation names no operation; repeat it with the same --action-id. |
replay | Commands with --action-id and no saved operation, and changes that set a state, such as groups update or billing cancel | Repeat the same command, with the same --action-id when it takes one. The API applies it once. Without --action-id, a repeat is a new change. |
reconcile | Every other change, including domino api | Read the affected resource, as the table below says, before deciding whether to repeat it. |
null | Reads and local commands | The command changes nothing in the API and never exits 3. |
More values may be added in a minor release; treat a value you do not know as reconcile. The error's own details.recovery stays reconcile whenever it names no operation, because the CLI cannot tell whether you passed the ID.
Rate limits
Domino sets no general quota on management calls: none per key, per project or per organization. Test-member calls (management.testMembers, domino test-members) and attempt reads have no limit of their own, except linking an X account by handle, below. This describes today's behavior, not a promised throughput, and limits may be added.
These calls have their own limits, because each spends a shared outside quota or starts something costly. Past a limit they fail with HTTP 429 rate-limited (RATE_LIMITED). "Per caller" counts, within one organization, each person once, whichever session or personal key they use, and each automation, service or hosted app key on its own.
| Call | Limit |
|---|---|
X handle lookup: management.x.lookup, domino x lookup HANDLE, and linking a test member's X account by username (test-members link-account --username) | 30 a minute per caller and 300 an hour per organization, shared |
X search preview: Console, management.trackers.preview, or domino api POST /tracker-preview | 6 a minute per caller and 60 an hour per organization |
domains add, domains recheck, domains remove | 30 an hour per organization, together; one recheck per domain every 10 seconds |
Starting domino dev | 10 preview starts and 10 development keys a minute per caller |
domino feedback | 10 an hour per caller |
Presenting a personal key (ak_…) or an MCP OAuth token that Clerk does not recognize (a mistyped or made-up value; expired, revoked and other refused credentials don't count), on the management API, git or MCP | 30 every 10 minutes per client address (an IPv4 address or an IPv6 /64), counted by each API server on its own; past it, every ak_ and MCP token from that address answers 429 until the window ends. A refused value presented again within a minute can be refused without being checked again. |
Any call can still answer 429 or 503, with Retry-After when the wait is known. Honor it: the CLI and the management client retry within the call's deadline as described in Retries and deadlines, and a failure that remains reports the wait in details.retryAfter. To run many calls, such as a rehearsal with hundreds of test members, keep a fixed number in flight, give each write that takes an actionId a deterministic one so a retry replays instead of repeating, and slow down when a 429 comes back.
Resume an uncertain change
A change the CLI cannot confirm exits 3 with OUTCOME_UNCERTAIN, after any retries above. When the error has an operation, run domino with its resume array as the arguments, adding --json. operation.scope pins all four scope values (API URL, organization, project and environment), and resume either repeats the scope as flags or reads it from the saved operation journal. It runs in the original scope from any directory and never contains a credential. Project creation uses an empty scope.project until the server assigns one; operations show then reports the created project separately.
A resume (deploy --resume, stage --request-id) runs only in a pinned scope: each of --api-url, --organization, --project and --environment must come from a flag or its RELAY_* variable. Otherwise it fails with SCOPE_REQUIRED and details.missing lists the flags to add; relay.json and the checkout's scope are never used for a resume. An operation is reported only when the CLI knew all four values when it sent the change.
operation.kind | Started by | id | Resuming |
|---|---|---|---|
deploy.publish | domino deploy in test | the deployment preview | deploy --resume <id> publishes that test preview, or returns its committed result. It never creates a second release. Live requests are approved in Console and have no CLI resume command. |
deploy.promote | domino deploy --promote in live (deprecated; use domino promote), in the checkout's scope | the promotion action UUID | operations resume <id>, in the scope it journaled, returns the pending promotion that action prepared, in the deployment record shape, or prepares it once. Approval still happens in Console. |
deployments.rollback | domino deploy rollback in test | the rollback action UUID | operations resume <id> returns the deployment that action published, or rolls back once. |
promotions.prepare | domino promote in test | the promotion action UUID | operations resume <id> returns the promotion that action prepared, or prepares it once. Approval still happens in Console. |
stage.build | domino stage | the build request ID | stage --commit <sha> --request-id <id> returns the build that request created, or creates it once. It needs no checkout: it skips the hosting preflight, the uncommitted-changes check, the missing-secret warnings and the wallet sign-in warning. A first submission the API refuses as given (400, or 422 such as commit-not-found for a commit the hosted repository lacks) saves no operation and prints no retry: fix the cause, then stage again. |
A deploy.publish or deploy.promote operation sends POST /deployments/:deployment/publish or /promote, with the deployment in the path, whichever CLI release saved it.
Recover after restarting the CLI
The CLI saves project creation, quest archive/unarchive, group creation and membership changes, resource changes, webhook endpoint creation and secret rotation, build submission, deployment publication, and live promotion before sending the mutation. The journal lives in $RELAY_CONFIG_DIR/operations, or ~/.config/domino/operations. Keep that directory between CI invocations. Journal resume commands include --directory with the exact journal path; pass that option when using a copied journal elsewhere. Existing stage and deploy resume commands keep their pinned scope flags. Files are private to the local user and contain the exact request inputs, scope, actor and committed results, never authentication credentials. A webhook create or rotation result keeps its signing secret only until the CLI has written it to stdout once, when the operation completes (or the next show or resume, if that output failed); the record then keeps the result with secretRedacted: true, and show adds a note pointing to domino webhooks rotate-secret. Listing and error output omit request inputs.
domino operations list --json
domino operations show OPERATION_ID --json
domino operations resume OPERATION_ID --json
domino operations forget OPERATION_ID --json
domino operations prune --older-than 30 --jsonlist and show work offline. resume uses the saved API URL, organization, project, environment and payload; it ignores checkout configuration and RELAY_* scope variables. Explicit conflicting scope flags are rejected. Authenticate for the saved API origin as the original actor. A completed operation returns its cached committed result without another mutation. For a build, that result acknowledges submission; use domino builds or the existing stage --request-id path to inspect completion.
Nothing expires saved operations on its own. forget removes one record, pending or complete; a forgotten pending operation can no longer be resumed. prune removes completed operations last changed more than --older-than days ago (default 30; 0 removes every completed one) and answers { cutoff, removed }. It leaves pending operations, and records another process holds or that cannot be read.
Use --operation-id UUID on create, quests archive, quests unarchive, groups create, groups add, groups remove or any saved resource change when your automation supplies the ID; a resource change sends it as its actionId. Reusing it with changed inputs, actor or scope fails with CONFLICT. Without this flag, a new invocation starts a new operation; use operations resume for recovery.
Creation saves the server-assigned project ID before repository setup. create resumes its own operation while a step ends uncertain because it timed out or lost its connection, or answers 502, 503 or 504: it backs off from 2 to 15 seconds for up to 5 minutes in all, holding the operation, and succeeds with the normal result. With --json, stdout still carries one result, and a failure is one error object on stderr. After 5 minutes it fails as before, with operation: OUTCOME_UNCERTAIN says Domino may still be setting up the project, and an API that stayed unavailable is a SERVER_ERROR. Either way, run its resume. A definite refusal fails at once. If setup or Git checkout fails, the error still includes operation and its exact resume command, even when the outcome is a known failure with exit 1. Resuming provisions the same project's repository and finishes its checkout. It clones into an operation-owned sibling directory, saves the finished checkout and renames it into place. An interrupted clone with local changes is preserved and reports LOCAL_CONFLICT; preserve those edits before resolving the conflict. A live process owns the operation until it exits; another resume reports LOCAL_CONFLICT. Cancellation keeps the journal, and the next process recovers a stopped owner's lock.
| Mutation | Recovery |
|---|---|
create | Saved project-creation receipt, repository and checkout checkpoints. |
quests archive, quests unarchive | Saved action ID, exact lifecycle request and scope. |
groups create, groups add, groups remove | Saved action ID, exact request and scope (group.create, group.member). groups update, archive and unarchive set a state, so repeating one is safe. |
Resource changes: points award|spend|adjust|set|reverse, trackers create|update|ingest, leaderboards exclude|include|bind-exclusion-group, redemptions cancel, entitlements restore, completions restore, lifecycle verbs and the rest in Resource commands | Saved request and scope; the operation ID is the request's actionId, so a resume repeats the same request, which the server applies once. A new resource command saves its management client method, and operation.kind names it, such as points.record; a lifecycle verb, trackers refresh, `quests enroll |
stage | Saved build request ID and commit; existing --request-id remains supported. |
deploy test publication | Saved preview ID; existing --resume remains supported. |
deploy --promote | Run domino promotions list before repeating. A pending promotion may already exist; approval and rejection happen only in Console Live → Deployments. |
promote | Saved action ID; operations resume <id> returns the same promotion. domino promotions list shows whether a newer one superseded it. |
deploy --preview, image uploads | Inspect domino releases list and the project's image/catalog state before rebuilding; publication is separate. |
secrets set, secrets delete | Inspect domino secrets list; values are never read back. Confirm the intended value before replacing it. A secrets set of an app login key (di_…) that the key check refused (INVALID_ARGUMENT) stored nothing. |
domains add, domains recheck, domains remove | Inspect domino domains list. Adding a hostname the project already has fails with already-exists; re-checking twice within 10 seconds fails with rate-limited. |
tokens create, tokens scope, tokens revoke | Inspect domino tokens list. An uncertain tokens create may have issued a key whose secret you never saw: revoke the unconfirmed key with that name before creating another. A domino login session creates and scopes test keys only: a live tokens create or tokens scope is refused with console-approval-required and changes nothing, so ask the user to create the live key in Console Settings → API keys. |
billing checkout, billing topup, billing portal | Inspect domino billing show. Each returns a link to a Stripe page; the project changes only once someone pays there. A repeated checkout in the same ten-minute window returns the same Checkout session, and one for a project whose subscription holds its plan fails with subscription-exists. |
billing change, billing cancel, billing resume | Inspect domino billing show. Each is safe to repeat: asking for the plan the subscription is already on, or one already held for the renewal, changes nothing more, and cancelling or resuming twice leaves it as the first call did. subscription's plan moves once Stripe reports the change, usually within a minute. |
starters export | Changes nothing in the project; it only writes local files. A failure while writing can leave a partly written directory, which a repeat refuses with LOCAL_CONFLICT: delete that directory, or export into another one. |
feedback | Inspect the Linear issue list for the submitted report before sending another. |
logout | Inspect domino whoami; revoke or sign in again as appropriate. |
api POST/PUT/PATCH/DELETE and management writes without documented receipts | Read the affected resource first. An actionId field alone does not make replay safe. For webhook writes, list endpoints and deliveries before recreating, rotating or replaying; a webhooks replay sent with --action-id is repeated with the same ID instead, and answers the delivery the first replay queued. |
These reconcile-only writes are sent once and are not entered into the replay journal.
Cancellation
SIGINT (Ctrl-C) and SIGTERM stop the command. With no change outstanding they report CANCELLED, exit 130 for SIGINT and 143 for SIGTERM; while a change is outstanding they report OUTCOME_UNCERTAIN, exit 3. Git children started by this CLI are stopped before it exits on a signal. Recovery records remain on disk. Interrupting stage while it waits for the build reports CANCELLED with the stage.build operation: the build keeps running, and resuming returns it. Declining a confirmation or cancelling a prompt reports CANCELLED too; prompts come before any change. domino dev treats the first signal as a normal stop and prints its result; a second signal exits at once.
Events
With --json, a command writes JSON events to stderr while it runs. Each has an event field:
{"event": "operation", "operation": {…}}: written once the credential check passes, right before a resumable change is sent, after its recovery journal is saved, with the sameoperationa recoverable error would carry. Record it to resume the change if the CLI process dies.{"event": "browser-login", "url": "…", "code": "…", "expiresAt": 1767225600000}:domino loginis waiting for approval of this code at this URL. A person has untilexpiresAt(epoch milliseconds, 15 minutes after the request started) to approve it; after that, rundomino loginagain. Console's approval page shows the request's time, its IP's country and the CLI's version and OS, so the person can tell it is theirs.
Other stderr lines are prose. The error, when there is one, is always last.
Results
Each command's result schema is schemas/results/<command>.schema.json, with spaces in the command replaced by -, such as secrets-set. contract.json names it for every command. status, quests, releases, members list, builds, logs, whoami and api print the management API's response body unchanged; their route in contract.json names the endpoint, and the schema lists only the fields the CLI relies on. The resource commands print the typed result of the management client method they call, the same value createManagementClient returns, and route names its endpoint. --help and domino help [command...] with --json print {"help": "…"}, and domino help with an unknown command fails with UNKNOWN_COMMAND; --version with --json prints {"version": "…"}.
domino members list [--q TEXT] [--needs-review] [--limit N] [--cursor C] --json prints { items, total, nextCursor } (members-list schema), the project's members most recently joined first with their accounts and activity counts. Pass nextCursor back as --cursor for the next page until it is null, keeping the same --q and --needs-review. It needs read; see operator member reads for what --q matches.
domino quests replay <attempt> --json fetches the attempt's capture (GET /attempts/{attempt}/capture, publish permission) and evaluates local quest code against it with replayAttempt from @domino-sdk/relay/testing. Captures exist for test-environment attempts only: with live selected the command fails with ENVIRONMENT_UNSUPPORTED before any request, and an attempt whose evaluation has not started fails with CONFLICT (attempt-not-evaluated); a capture taken mid-run has recorded: null. It replays the discovered quest whose key the capture names; pass --quest FILE (and --export NAME for a named export) for another module or for a quest created in Console, whose capture has no key. --out FILE also saves the capture; --capture FILE replays a saved one without contacting the API, and then <attempt> is optional. The result (quests-replay schema) is { attempt, quest, decision, recorded, changed, divergence, calls, out? }: decision is what the code decides now, recorded what quest code decided in the captured run, changed whether they differ (kind, reason, rewards or completion key), divergence the first capability call the capture could not answer (the replay stops there, with decision and changed null), and calls the number of calls it answered. A changed decision still exits 0; branch on changed.
Members in members get, members list, members lookup and the attempts' people carry appIdentities: the member's linked app identities, each { kind, subject }. domino members lookup --kind app:game --subject 42 finds the member holding your app's user 42; --kind x|discord|telegram --subject ID finds an account by its numeric user ID, and --kind wallet --chain CHAIN --address ADDRESS a wallet. --provider with --subject is the deprecated form, which still finds accounts of any provider, such as email.
Member accounts in members get, members list, members lookup and the attempts' people carry status and reason only when Relay keeps the account's OAuth tokens to read it in the background: status is connected or needs-reconnect, and reason (revoked, expired or missing-scope) says why it needs reconnecting. See operator member reads. members get also carries creatorAccounts: the member's linked creator accounts, such as a YouTube channel, each with provider, subject, revision, linkedAt, status (connected or needs-reconnect, with reason) and the channel's profile; it is empty when the member linked none. Unlink one with domino api POST /members/<id>/creator-accounts/unlink body.json and a body of provider, subject, revision and reason (review permission).
Quest releases (quests, releases, members get, attempts) carry announcements when the quest posts announcements, each with provider (telegram or discord), action (post-message), chat or channel, text and an optional replyToTriggeringMessage; it is omitted otherwise. A Discord channel setting's source carries postable: true when it is an announcement's channel.
Quest releases (quests, releases, members get, attempts) carry retryCooldownMs when the quest has a retry cooldown and completionCooldownMs when it has a completion cooldown. Each quest in members progress and members get carries retryAt while that member's retry cooldown runs, and nextAvailableAt while a cooldown or an exhausted budget holds the quest until a known time. All are omitted otherwise. Releases also carry authorBudgets when the quest has a per-giver budget.
domino reference --json prints { topics }, every reference shipped with the installed CLI, and domino reference <topic> --json prints { topic, markdown }. Each capability's reference in domino capabilities names one of these topics. An unknown topic fails with INVALID_ARGUMENT and lists the topics.
The management client accepts additional metadata fields in a deployment record's source and discards those fields. It still validates the source kind (direct, promotion or rollback) and the deployment identifier required for promotion and rollback.
leaderboards list and leaderboards history return each board's definition as published, including includeUnscored when a board lists members without points, and ties when a board sets its live tie order ("earliest" ranks equal scores by who reached them first, so leaderboards standings returns them in that order with distinct ranks; absent means "shared").
domino build --hosting-artifacts --json prints the --out result: file is the .domino-build directory beside relay.json, files lists the artifacts written there (quests.json and catalog.json), and the counts follow. It refuses --out with INVALID_ARGUMENT.
The build, check and deploy result counts include offers. Build results and deployment previews also carry the authored offers array in their bundle, so automation that inspects publication contents can handle offers alongside point ledgers, quests and leaderboards. A quest or type built against an SDK runtime carries runtime: { digest, sdkVersion } and only its own code; the bundle's runtimes array holds each runtime's modules once. Write the bundle through unchanged: dropping runtimes makes Relay refuse a reference it has not stored. A manifest holds up to 1,000 quests and 200 quest types; more fails domino build and domino deploy with BUNDLE_FAILED before anything is sent.
Agents in parallel worktrees of one project publish with domino deploy --only <quest-key> (repeatable), so a deployment from one checkout doesn't republish, or revert, quests another checkout changed. The deployment holds only the named quests, the SDK runtimes they import, the relay.json images whose key their code names as a string literal, and supportedInteractions; every other quest keeps its release and every other resource stays as published, so the result lists only those quests. supportedInteractions is the one project-level value it sets, from the checkout's relay.json, so rebase before deploying when another worktree changed it. The key is the literal key a quest file passes to defineQuest. An unknown key fails with INVALID_ARGUMENT (exit 2) naming the discovered keys, before any request; --only with --resume, --promote, --dry-run or --out fails the same way. It works with --preview and --yes. See publish only some quests.
A deploy preview or published result carries calendar: the project timezone change the deployment makes from relay.json's calendar, as { "timeZone", "effectiveAt" }, or null when it makes none. A null effectiveAt means it keeps timeZone and cancels a pending change. Publishing makes exactly the change the preview showed; if a day boundary passed or a calendar change since then would make it differ, it fails with "Preview again" and publishes nothing. See the project calendar.
A deploy preview or published result carries questChanges: each quest in it by key, with change created (new quest), updated (a new release replaces the active one, named with its createdAt in replaces) or unchanged (the active release already has the same code, SDK runtime, provider, effective settings and archive state, so publishing keeps it and its ID). Check the updated keys before publishing: a quest your change did not touch appearing there means the bundle carries other code for it than what is published. Without --json the confirmation lists each created and updated quest and counts the unchanged ones. A result from an older Relay omits it.
A deploy preview or published result carries warnings: what the deployment publishes anyway but the operator should fix, as { "code", "message", "integration", "quests" }. Code integration-required names the quests (by key) that need an unconnected integration, such as Discord; their attempts fail with that code until it is connected, and connecting it admits the failed attempts of automatic quests without a tracker again. Code x-post-rewards-ungated names the quests that reward X posts without an inGroup prerequisite (an X tracker trigger paying the post's author, or a content quest on an X tracker, with declared rewards or an engagement rule); X does not allow apps that reward users for posting, so add inGroup(group) with a group of approved creators. Code quests-not-in-bundle names the quests published in test from code, not archived, that a full deployment leaves out, such as a starter's deleted sample quests; they stay as published, so archive each one you no longer want with domino quests archive <key> --reason "…". Quests created in Console and deploy --only are never listed. domino stage prints the warnings of the deployment its build published, and its --json result carries them in warnings. The list is empty when there is nothing to report, and a result from an older Relay omits it. Treat an unknown code as a notice its message explains. A warning never fails the command or changes its exit code; without --json the CLI also prints each one on stderr as Warning: ….
Scope and credentials
Each scope value comes from, in order: a flag (--api-url, --organization, --project, --environment), then RELAY_API_URL, RELAY_ORGANIZATION, RELAY_PROJECT or RELAY_ENVIRONMENT, then the checkout's domino.json in the Git common directory, then the nearest relay.json (or --config FILE). The API defaults to https://relay.domino.run. domino doctor --json reports each value's source in scopeSources. domino secrets, domino storage and domino schedules are the exception for the environment: they use test unless the --environment flag says live, and ignore RELAY_ENVIRONMENT and configuration. When either names live, they warn on stderr and still use test; their results say which environment they used in environment.
The credential is RELAY_MANAGEMENT_TOKEN when set, otherwise the token domino login saved for the API URL in $RELAY_CONFIG_DIR/credentials.json (default ~/.config/domino).
Running without a person
A headless agent or CI uses an organization automation key (dak_…) as RELAY_MANAGEMENT_TOKEN, or saves it with domino login --token-stdin. An organization administrator creates it in Console Settings → API keys; never ask for one in chat. It reaches every project of the organization or a chosen list, with chosen permissions (read, publish, inventory, points, ingest, enroll) and, separately, permission to create projects. A listed key also reaches the projects it creates. It works for domino create, git push through the credential helper, domino stage, authoring, test members and the staging test toolbar, in the test environment only:
- Any write in live, including drafts, previews and secrets, is refused with
console-approval-required. Ask the owner to publish from Console. What each credential may do in live compares it with the other credentials. - Grants, the team,
/projectchanges,/tokens, CLI sign-in approval and creating keys are refused withcredential-not-allowedorpermission-required. - Identity integrations (app login keys) are refused with
permission-required, listing included. An organization administrator's owndomino loginsession lists them withdomino identity-integrations list; only Console creates them. domino createwithout the key's create-projects permission fails withpermission-required; a project outside its list fails withscope-mismatch.- A project it creates connects a Discord server another project of the organization already connects, with
domino integrations discord connect --server <id>(list them withdomino integrations discord servers) and the key'spublishpermission, then reads the connection withdomino integrations discord get. Installing Domino's bot into a new server takes an organization administrator in Console: stop and ask onserver-not-connected-in-organization.
domino whoami --json prints credential: { kind: "automation", id, name, projects, createProjects, permissions }, with projects null for every project. Its changes are audited as automation:<id>.
Unattended mode
Whoever launches an agent with nobody present to answer it, such as a CI job, a scheduled run or a sandbox, sets DOMINO_AGENT_MODE=unattended in its environment. The agent cannot turn the mode on itself. domino doctor --json and domino whoami --json report it as agentMode: unattended, or interactive for any other value or none. In unattended mode:
- Login. Browser sign-in refuses with
UNATTENDED_LOGIN_UNAVAILABLE, and a command with no credential fails withAUTH_REQUIRED. SupplyRELAY_MANAGEMENT_TOKEN;domino logincan still save a token it reads from that variable or--token-stdin. - Feedback.
domino feedbackappends the report as one JSON line, with asavedAttimestamp, to./domino-feedback.jsonlor the path inDOMINO_FEEDBACK_FILE, and does not send it. It needs no credential. The result is{ "sent": false, "saved": true, "file", "report" }.--submit, orDOMINO_FEEDBACK=submit, sends it as usual.
Claude and ChatGPT
Operators can connect Claude, ChatGPT, Claude Code or VS Code to Domino's MCP server at https://mcp.domino.run and sign in with their Domino account; the guide is Connect an AI assistant. Its tools act as that operator, through the same management operations as the CLI, so their roles apply:
- Reads, in test and live:
list_projects,get_project,list_quests,get_quest,list_campaigns,list_submissions,get_submission,find_member,get_member,get_leaderboard,list_offers,get_usage,list_analytics_metrics,query_analyticsandlist_audit_events. - Writes, in test only:
create_test_member,act_as_test_member,reset_test_member,review_submissionanddelete_submission_evidence. In live they are refused withconsole-approval-required, like every live write of an MCP connector (what each credential may do in live).
There is no tool for publishing, points, deployments, access or arbitrary management calls; use the CLI for those. When an owner asks how to check their campaign from a chat assistant, point them to the guide instead of building an integration.
Stable and experimental commands
domino --help lists stable commands under Commands and experimental ones under Experimental commands; contract.json marks each with stability. An experimental command's options, output and result can change in a minor release. It never changes how a stable command behaves.
| Experimental command | Why |
|---|---|
dev | Its preview transport, progress output and result are still changing. Script stage instead. |
billing | Billing is open to Domino staff only; its commands, results and plan names may change before it opens to everyone. |
connections | Plugin connections are new while the plugin contract is built; their commands and results may change. connections authorize prints an OAuth plugin's authorizationUrl for the person signed in to the CLI to open; they finish it in Console as themselves. An automation key can't start one (403 credential-not-allowed). A destination needs no person in test: connections connect --dry-run connects it without an account, connections log lists what it would have sent, connections activity reads its sync status and connections resync sends every member again. |
members consent | Members' marketing consent is a new feature family; the command and its result may change. |
members ban, members unban | Member bans are a new feature family; the commands, the --dry-run preview and the ban result may change. |
tiered-rewards | Archiving tiered rewards is a new management resource; its commands and results may change. |
Commands the CLI runs for Git, such as credential-helper, are not part of the contract.
Webhook operations
domino webhooks covers endpoint list/create/update/disable, secret rotation, delivery logs, and replay. create and rotate-secret print a signing secret; treat their JSON output as a credential. Other commands omit secrets. Both are saved operations: pass --operation-id <uuid> to supply the ID, sent as the actionId, and after an uncertain answer run operations resume to get the same endpoint and secret. Their journal records keep that secret only until it has been printed once: a resume of an operation that has not completed returns it, and once it has, operations show and operations resume answer secretRedacted: true instead, and repeating the command with the same --operation-id fails with CONFLICT. Rotate the secret to get a new one. A CLI session can read live webhooks but cannot change them. A live API key with publish can perform these operational writes. See Webhook reference for command arguments and response shapes.
Deprecated generic resource lifecycle commands also use the durable operation journal. resources archive and resources unarchive save resource.lifecycle with the selected kind, key or ID, desired archive state and reason before sending. operations resume reuses that scoped payload and action ID, including after a fresh process starts. resources list reports archivedAt as a timestamp or null, and lists offers in offers, which archive only with offers archive|unarchive, and tiered rewards in tieredRewards, which archive only with the experimental tiered-rewards archive|unarchive; a successful mutation returns { resource, changed }.
Resource commands
Each family has list and get style reads and its changes, all built on the management client from @domino-sdk/relay/server. Every command takes the scope flags and --json; reads need read, and changes the permission their management method documents.
| Family | Reads | Changes |
|---|---|---|
| Points | points get MEMBER LEDGER, points list MEMBER LEDGER [--cursor], point-ledgers list | points award|spend|adjust|set|reverse MEMBER LEDGER --reason …, point-ledgers create NAME, point-ledgers update LEDGER --name …, point-ledgers archive|unarchive |
| Trackers | trackers list, trackers get, trackers records, trackers summary, trackers content, trackers outcomes, x lookup HANDLE (an X handle's stable user ID) | trackers create KEY [--source KIND] (--chain and --token for --source wallet-balances) and trackers update TRACKER (trackers of the managed sources as data, in test), trackers ingest TRACKER FILE, trackers retry-outcome, trackers pause|resume|refresh|archive|unarchive |
| Leaderboards | leaderboards list, leaderboards standings, leaderboards history, leaderboards exclusions, leaderboards exclusion-group, leaderboard-settlements list|get|export | leaderboards exclude|include BOARD MEMBER --reason …, leaderboards bind-exclusion-group BOARD GROUP --reason …, leaderboard-settlements approve|reject|retry-measurement, leaderboards archive|unarchive |
| Referrals | referral-programs list, referral-relationships list (each relationship and qualification a member import recorded names it in import), referral-relationships history MEMBER | referral-programs qualify PROGRAM MEMBER [--disqualify], referral-rewards resolve ENTITLEMENT, referral-relationships pause|resume, referral-programs archive|unarchive |
| Offers and redemptions | offers list, redemptions history [--member] [--cursor] [--limit] | redemptions cancel REDEMPTION --expected-revision N, offers pause|resume, offers archive|unarchive OFFER --reason … (publish, in test), redemptions pause|resume |
| Tiered rewards (experimental) | tiered-rewards list | tiered-rewards archive|unarchive KEY --reason … (publish, in test) |
| Rewards | — (read entitlements with members get MEMBER) | entitlements restore ENTITLEMENT --expected-revision N --reason … (a voided entitlement; review permission) |
| Completions | — (read revoked ones in revokedCompletions of members get MEMBER; a revoke that freed the occurrence lists budgets it returned) | completions restore COMPLETION --reason … (a revoked completion; review permission; budgets it charged again, or 409 budget-exhausted) |
| Profiles | profiles settings | profiles configure FILE, profiles moderate MEMBER [--reset-name] [--remove-avatar] [--lock|--unlock] |
| Calendar | calendar get | calendar configure --time-zone ZONE |
| Members and groups | members list|get|progress|lookup|identities|counters (deprecated: identity-links|oauth-accounts), groups list|get|members|member | members unlink-identity|relink-identity|identity-sign-in (deprecated: relink), members moderate-counter, groups create|update|archive|unarchive|add|remove |
| Quests and releases | quests list [--lifecycle STATE]|get (each quest carries lifecycle: active, paused or archived; repeat --lifecycle for more than one state), quests enrollments, releases list [--quest]|get, attempts list [--quest]|get, attempts evidence-retention, metrics get, usage active-members, usage real-people-in-test, usage plan, pauses list | quests pause|resume|archive|unarchive|enroll|withdraw, attempts accept-anyway ATTEMPT --reason TEXT, attempts redact-evidence ATTEMPT --reason TEXT, attempts redact-evidence-before --quest QUEST --decided-before MS --reason TEXT (loops until done; review, in live an organization admin's key or a service key), attempts set-evidence-retention --days N|--keep (publish; shortening needs an organization admin's key or a service key) |
Test members (test only, publish) | test-members list, test-members history MEMBER [--since MS] | test-members create NAME, test-members act MEMBER QUEST [--input JSON | --photo FILE] [--release RELEASE], test-members pass MEMBER QUEST [--completion-key KEY], test-members accept-anyway MEMBER ATTEMPT [--live-rules --reason TEXT], test-members advance MEMBER (--days N | --hours N | --minutes N | --to MS|INSTANT), test-members accept-referral MEMBER (--code CODE | --inviter MEMBER), test-members redeem MEMBER OFFER, test-members link-wallet|unlink-wallet MEMBER --chain CHAIN --address ADDRESS, test-members link-account MEMBER --kind x|discord (--subject ID | --username HANDLE), test-members unlink-account MEMBER --kind x|discord --subject ID (deprecated: --provider and --id), test-members record MEMBER --tracker TRACKER [--data JSON] [--as author|messageAuthor], test-members reset MEMBER, test-members purge MEMBER... --reason TEXT |
| Dashboards | dashboards list [--include-archived], dashboards get KEY [--range 7d|30d|90d|all] (see dashboards) | dashboards archive|unarchive KEY --reason … |
Member imports (publish; reads need read) | imports list, imports get IMPORT, imports rows IMPORT [--status STATUS] | import --file FILE [--source NAME] [--import IMPORT] [--dry-run], imports apply|cancel|stop IMPORT, imports revert IMPORT --reason TEXT (see member imports; live imports are applied in Console; read state: a stopped or reverted import is stopped or reverted, while its 1.x status reads applied) |
To simulate a whole campaign with the test-member commands, read campaign testing (domino reference campaign-testing). For declaring trackers, their collection options, record filters, trigger outcome codes and engagement accounting, read trackers (domino reference trackers). For quests that check a wallet's EVM contract events or Solana transactions, read on-chain actions (domino reference onchain). For the evaluator readers a quest uses to check what a member did, such as Discord membership, X, wallets, submissions, AI text review and image checks, read verify member actions (domino reference verification). For campaign windows, prerequisites, automatic and repeatable quests, reject codes, deferral, cooldowns, streaks, progress counters and local quest tests, read quest rules and evaluation (domino reference quest-rules).
test-members history names each attempt's and skipped outcome's quest by ID as quest and by authoring key as questKey, absent for a quest created without a key. attempts list items name only the quest ID; map an item's release to the page's releases[] entry with that id, whose key is the quest's key.
Every change except point-ledgers update, the groups update\|archive\|unarchive state changes and the member import commands is saved before it is sent. Member imports resume by themselves: run domino import again with --import IMPORT, since a row with the same ref replaces the earlier one. Saved changes resume as described in Recover after restarting the CLI. The CLI checks the input with the SDK's own schema first: input it refuses exits 2 with INVALID_ARGUMENT and leaves nothing saved or sent. A change the API refuses outright on its first attempt (exit 1, outcome: "failed") is not kept either, since nothing was applied; only a change that may have been applied stays saved for operations resume. When no environment comes from --environment, RELAY_ENVIRONMENT or relay.json, a change is saved for the only environment your credential reaches, such as a personal API key's own, and otherwise for test. Release history (releases list and releases get) also needs publish.
Quest and release records can include optional presentation.steps, image, imageAlt, and requirements. Steps retain their authored order and can carry an HTTPS link action. configuration.presentation records a Console or quest-instance override; configuration.presentationDefaults records the defaults used when resetting it. These fields also appear wherever attempt or member results include a release. Read the resolved presentation to render a quest; the configuration fields describe its authoring state. See quest authoring for the shapes. Older records can omit these fields.
A text setting in a release's settings can carry an optional picker, such as "telegram-chat" from settings.telegramChat(): Console then offers a picker for its value, which is still a string. Treat an unknown picker as plain text. Six pickers also constrain the value: "amount" (settings.amount()), a non-negative base-unit integer string, with an optional decimals field for showing it in whole tokens, "chain" (settings.chain()), "solana", Solana mainnet's CAIP-2 ID or an EVM chain ID, and "nft" (settings.nft()), an NFT collection such as "eip155:1/erc721:0x…", "eip155:8453/erc1155:0x…/1" or "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:<collection mint>", whose EVM contract publication and Console edits also check through ERC-165, and whose Solana mint they check is a Metaplex collection NFT, "contract-call" (settings.contractCall()), a contract's CAIP-10 ID and a view or pure function signature returning one uint, int, bool or address, such as "eip155:8453:0x… function stakedOf(address) view returns (uint256)", whose chain they also check, and "contract-event" (settings.contractEvent()), an EVM contract event such as "eip155:8453:0x… Transfer(address indexed from, address indexed to, uint256 value)", whose chain publication and Console edits check reports a finalized or safe block, and whose address they check holds a contract, and "solana-program" (settings.solanaProgram()), a base58 Solana program address, which they check is an executable program. A release with such a setting may carry solanaIdls: by setting key, { program, source, idl }, the program's Anchor IDL as JSON, stored from its on-chain IDL account when the setting was saved (source: "chain") or pasted by an operator ("pasted"); treat other source values as unknown. ctx.wallet.transactions() decodes the program's instructions with it (Solana transactions).
The bare status, quests and releases commands, and groups show, are deprecated and removed in 2.0.0; contract.json gives every deprecated command, and every deprecated option, such as members lookup --provider, a deprecated note naming its replacement. They still print Console's former pages, and without --json the three page reads warn on stderr. Use metrics get, attempts list, quests list and pauses list for status; quests list and quests get for quests; releases list and releases get for releases; and groups get for groups show.
usage active-members reads the project's live active members this UTC month against its plan's limit (null when the plan has none), and the six earlier months, newest first (management.usage.activeMembers(), GET /management/v1/usage/active-members). An active member sent a signed-in participant request on a UTC day of the month after the day they joined; imports, tracker records and test members never count. See active members.
usage real-people-in-test reads the real people who joined the test environment against the plan's limit, and whether the limit is enforced (management.usage.realPeopleInTest(), GET /management/v1/usage/real-people-in-test). Test members never count. When it is enforced and full, new people's sign-ins to test fail with limit-reached; people who already joined still sign in. See real people in test.
usage plan reads the project's plan as it stands (management.usage.plan(), GET /management/v1/usage/plan), with read permission and whichever environment you select: plan and its source (stripe, staff, grandfathered, trial, or null on Free), whether it may serve live and why (live.reason: plan, trial or grace with until, or free or trial-ended), liveState while live is in notice or offline, the organization's one trial (unused, active, grace or used, and its project), whether the plan includes a custom domain, the AI builder balance by pool (ai), the Stripe subscription as billing show gives it to an administrator (its plan is always launch, growth or scale, since usage plan is stable), and enforcement, whether plan limits are enforced. It buys nothing; see billing below for that. See going live needs a plan.
billing show reads whether Stripe billing is open to you (available, and reason: admin-required, staff-only or not-configured) and, when it is, the project's subscription as Stripe last reported it (management.billing.read(), GET /management/v1/billing). billing checkout --plan PLAN [--interval month|year] returns a Stripe Checkout URL that subscribes the project, and billing portal the organization's Stripe customer portal (management.billing.checkout(), management.billing.portal()); open the URL in a browser. An annual checkout (--interval year) has a promotion code field on Stripe's page; a monthly one has none, so a code such as a first-year offer needs --interval year. While a code discounts the current period, subscription.discount gives what the period costs (amount) instead of its list price (listAmount), both in US cents with tax included, and when that period ends (until); a renewal at the list price, a plan change or the subscription ending clears it. billing checkout and billing portal need an organization administrator's sign-in, never an API key. Until the billing launch they are open to Domino's own organizations only, and everyone else gets billing-staff-only. The plans are launch, growth and scale today. --plan takes any name, and Relay refuses one it does not offer with 400 (BAD_REQUEST). The billing results read a plan the same way: subscription.plan and scheduledChange.plan can name a plan Relay added after this CLI. The billing commands are experimental: their results and plan names can change in a minor release.
billing organization (management.billing.organization(), GET /management/v1/organization/billing) reads the organization's billing for any of its members, with no project selected: each project you can read with its plan, source, live, liveState and Stripe subscription, the organization's Free projects against their limit (freeProjects), the free AI allowance its Free projects share (freeAi), the trial, enforcement, and in billing whether you may open the customer portal, as billing show says. An app key, or a Playground session fixed to one project, gets credential-not-allowed.
billing change --plan PLAN [--interval month|year] (management.billing.changePlan(), POST /management/v1/billing/plan) moves a paying project's subscription to another plan. A higher plan, or annual billing, applies now: Stripe invoices the prorated difference at once, and when that payment needs the customer (a bank's authentication, say), nothing changes until the returned invoiceUrl is paid. A lower plan, or monthly billing, is held for the renewal, so the period already paid keeps its plan; billing show names it under scheduledChange. Asking for the current plan drops a held change. billing cancel (management.billing.cancel()) ends the subscription when its period does; the project keeps its plan until then, and billing resume takes it back. subscription.endedFor is project-deleted while a deleted project's subscription runs out (restoring the project resumes it), and project-transferred when the project came from another organization whose subscription ends with its period: billing checkout then subscribes the project again with no charge until that period ends. billing topup [--quantity 1-10] (management.billing.topup(), POST /management/v1/billing/topup) returns a Checkout URL buying $10 packs of AI builder credit for the project, on any plan; the credit is added once Stripe reports the payment, once per purchase, and never expires. While an invoice is overdue, billing show's subscription.unpaid gives when it first failed and the two deadlines: from promotionsRefusedAt every promotion in the organization is refused with plan-required (reason: "unpaid"), and at offlineAt the project's live goes offline until it is paid. A project without a subscription that holds its plan gets no-subscription, and a plan change while the subscription is set to cancel gets subscription-canceling. The same administrator and staff rules apply.
Project analytics
domino analytics get [--range 7d|30d|90d|all] [--ledger KEY] reads how a project is doing in one environment; --range defaults to 30d. It needs read. Without --json it prints a summary of every total and its change; --json prints the read itself, which management.analytics.read({ range, ledger }) from @domino-sdk/relay/server also returns (GET /management/v1/analytics?range=…&ledger=…). When someone asks how a project is doing, quote these numbers instead of computing your own from other reads.
Range. The range ends now (range.to). 7d, 30d and 90d are that many project-calendar days, today included; all starts on the day of the project's earliest dated record the read counts: a membership, attempt, decision, completion, entry in the chosen ledger, fulfilled redemption, accepted referral or active qualification. That is usually the first membership, but a completion imported with its original date can be older. 7d and 30d have one bucket per day; 90d and all one per week, from Monday, with the first and last weeks cut to the range. Days start at local midnight; when the project's timezone changes, the day or week already open finishes in the old timezone, and each bucket's key names its timezone (2026-10-08@Europe/Budapest, 2026-W41@UTC). previous is the range of equal length that ends where this one starts; it is absent for all. Each time is an ISO string with its epoch milliseconds beside it: from and fromMs, to and toMs, and each bucket's start, startMs, end and endMs.
Metrics. Each metric is { total, previousTotal, series }: the range's total, the previous range's total (absent for all), and one value per bucket in range.buckets. Records count as they stand now: a completion revoked or a redemption canceled later leaves the bucket it was in.
| Metric | Counts | Dated by |
|---|---|---|
members.signUps | Members who joined the project, as the Members page counts them | When they joined |
members.active | Distinct members with an attempt; the total counts each member once | Attempt creation |
quests.attempts | Attempts | Attempt creation |
quests.completions | Quest completions never revoked, as Console counts them | Completion |
quests.rejections | Rejected attempts | Decision |
quests.needsReview | Attempts the evaluator sent to staff review, whatever staff decided later | Attempt creation |
points.awarded | Points in the ledger's award entries | Entry's effective date |
points.spent | Points in the ledger's spend entries, as a positive number | Entry's effective date |
points.net | Every entry's signed amount: awards, spends, adjustments (offer refunds and imported balances among them) and reversals | Entry's effective date; a reversal takes the date of the entry it reverses |
points.membersEarning | Distinct members awarded more than 0 points; the total counts each member once | Entry's effective date |
offers.redemptions | Fulfilled offer redemptions | Redemption |
referrals.accepted | Accepted referrals | Acceptance |
referrals.qualified | Active referral qualifications, one per program and friend. A suspended referral still counts, as it does toward inviter rewards, although Console labels it Suspended | First qualification |
referrals.acceptedQualified | Of the referrals accepted in the range, how many have an active qualification now, in any program, each friend once, suspended ones included. Only { total, previousTotal }, no series. acceptedQualified.total ÷ accepted.total is the share of the range's friends who have qualified since, never above 100%; a friend accepted before the range who qualifies inside it counts in qualified but not here | Acceptance |
A decision is dated on the wall clock. Attempt creation, completion and quest points are dated on the member's clock, which an advanced test member moves up to 90 days ahead; activity dated after now is left out until then. An evaluator that asks for review on a quest with human review off rejects the attempt: it counts as a rejection, not as review.
Quests. quests.byQuest lists each quest with an attempt, completion or decision in the range, most attempted first, by quest ID with its current title:
attempts,completionsandneedsReviewas above, withseries.attemptsandseries.completionsper bucket.decided: attempts accepted, progressed or rejected in the range, by decision.rejected: those rejected.rejectionRate:rejected ÷ decided, ornullbelow 10 decided attempts.topReasons: the 5 most common rejections. Rejections with a code group by it and by who wrote it,codeSource:evaluatorfor an evaluator's reject code,relayfor one Relay writes (budget-exhausted,key-used,defer-expired,defer-limit-exceeded). An evaluator may writekey-usedtoo, so Relay'skey-usedand an evaluator's are separate rows.reasonis the latest one's reason; a budget's reason names the budget, but its rejections still form one group. Rejections without a code group by exact reason, withcodeSource: null. Equal counts list coded groups first.rejectionsBySource:budgetisbudget-exhausted;expiredisdefer-expiredordefer-limit-exceeded;staffis a reviewer's rejection, of an attempt waiting for review or of a deferred one, and Relay's rejection of a quest whose completion an operator revoked;keyUsedis Relay'skey-used, an attempt whose completion key, unique across members, another member's completion holds;evaluatoris every other rejection, an evaluator's ownkey-usedincluded.
Points. The points section reads one ledger: --ledger by key or ID, archived ledgers included, or else the first ledger that is not archived. points.ledgers lists every ledger with archived, and points.ledger is the one read, or null, with zeros, when there is none. spent and net appear only when the ledger has ever had a spend entry (hasSpending). An unknown ledger fails with 404 not-found.
Sections that depend on the project. offers appears only when the project has an offer, with byOffer listing every offer, most redeemed first. referrals appears only when it has a referral program, archived ones included. testEnvironment appears only in test: lastResetAt (and lastResetAtMs), when a test member was last reset, which deletes that member's history, and shiftedClockMembers, how many have an advanced clock.
For range all, members.signUps.total and quests.completions.total equal metrics get's members and completions, imported completions included, except for records dated after now (a test member's advanced clock) or not at all.
Metric catalog
domino analytics metrics lists Domino's closed metric catalog, read from the API (management.analytics.catalog(), GET /management/v1/analytics/catalog): each metric's id, definition, kind (count, distinct or rate), the breakdowns and filters it accepts, the charts it allows, whether it needs a point ledger, and a rate's minimumDenominator. It needs read.
domino analytics query --metric ID [--range 7d|30d|90d|all] [--breakdown DIMENSION] [--filter NAME=KEY[,KEY]]... [--ledger KEY] [--limit N] reads one metric (management.analytics.query({ metric, range, breakdown?, filter?, ledger?, limit? }), GET /management/v1/analytics/query?metric=…&range=…&filter.quest=a,b). --range defaults to 30d and buckets exactly as project analytics does. --filter repeats, one per filter, and its keys are keys or IDs. Without --json it prints the total with its change and one line per breakdown row, with Other last; --json prints the result. There are no formulas or custom queries: answer a question the catalog can't express by saying so, not by combining metrics.
The result has metric, kind, range, previous (absent for all), ledger (the key of the ledger a points metric read) and value ({ total, previousTotal, series }). With no breakdown or filter, value equals the same metric in analytics get; groups.*, trackers.records and leaderboards.settledParticipants are only in the catalog. With --breakdown, breakdown has the dimension, its rows and other:
- Rows. Each row has
key(a resource's key, or its ID when it has none; a group's key; a source; an outcome; or a reason's code or text),title,archived,total,previousTotalandseries. Rows come by total, largest first, with an empty rate last; ties go to the larger denominator, then by title. A quest, offer, program, group, tracker or leaderboard breakdown has a row for every matching resource that is not archived, with zeros when it had no activity, and for an archived one with activity in the range or the range before. other. Rows past--limit(1 to 100, default 100) are summed intoother, withrowssaying how many. For a rate,otheris the summed numerators over the summed denominators of the rows it holds.valuecovers every row,otherincluded.
Filters narrow what is counted before it is split. A list matches any of its keys; different filters all apply. quest, offer, program and leaderboard keep those resources. tracker keeps activity on the quests whose current release is triggered by the tracker, and on trackers.records the tracker's own outcomes. group keeps members in the group now, whatever their membership was when they acted, and on groups.joins and groups.leaves the changes in that group.
Rates are fractions from 0 to 1, and null below 10 in the denominator: in the total, the previous total, each bucket and each row.
| Metric | Breakdowns | Filters |
|---|---|---|
members.signUps | none | group |
members.active | none | group |
quests.attempts, quests.completions, quests.needsReview | quest | quest, group, tracker |
quests.rejections | quest, source, reason | quest, group, tracker |
quests.rejectionRate | quest | quest, group, tracker |
points.awarded, points.spent, points.net, points.membersEarning | none | group; --ledger is required |
offers.redemptions | offer | offer, group |
referrals.accepted | none | none |
referrals.qualified | program | program |
referrals.qualificationRate | program | program |
groups.joins, groups.leaves | group | group |
trackers.records | outcome, tracker | tracker |
leaderboards.settledParticipants | leaderboard | leaderboard |
The counts are defined in project analytics. The catalog adds:
quests.rejectionRate: rejected attempts ÷ attempts accepted, progressed or rejected, both by decision. Its quest rows areanalytics get'sbyQuest[].rejectionRate.referrals.qualificationRate: of the referrals accepted in each bucket, the share whose friend has an active qualification now, suspended referrals included; for aprogramrow or filter, a qualification in that program. Every program row shares the denominator, so the rows don't add up to the total. With no filter its total isacceptedQualified.total ÷ accepted.total.sourcerows areevaluator,staff,budget,expiredandkeyUsed, asrejectionsBySourcecounts them.reasonrows group by rejection code when there is one, otherwise by the exact text, titled with the latest rejection's reason; a code and the same text stay separate rows, and Relay'skey-usedis the rowrelay:key-used, apart from an evaluator'skey-used.referrals.acceptedtakes no program: a friend accepts an inviter, not a program.groups.joinsandgroups.leaves: members added to and removed from a group, by when the change happened. A change counts only when it changed the member's membership from their previous change in that group, so a first change counts only as a join. Repeats that changed nothing, and the undated rows Domino recorded for memberships older than its group history, don't count. Each change is one Console Audit history record (group-member-added,group-member-removed).trackers.records: tracker trigger outcomes, one per quest and record observation, by when the record was observed.outcomerows areadmittedandskipped. A retry or re-decision overwrites an outcome, so each bucket shows today's outcome of its records, not the outcome they had then. They are the records a tracker's trigger outcomes list in Console.leaderboards.settledParticipants: the rows of leaderboard settlements, one per member and settlement, by the settlement's cutoff. Pending and approved settlements count; a settlement rejected later leaves its bucket. Settlements are periodic, so it draws asnumber,barortable, notline.- With
--range all, a group, tracker or settlement metric starts at its own earliest record when that is older than the project's.
--ledger takes a key or ID, archived ledgers included, so an archived ledger's history stays readable.
| Code (422) | Meaning |
|---|---|
unknown-metric | No metric has this ID. |
breakdown-not-accepted, filter-not-accepted | The metric doesn't take this breakdown or filter. |
ledger-required, ledger-not-accepted | A points metric needs --ledger; other metrics take none. |
invalid-limit | --limit is outside 1 to 100. |
unknown-reference | A quest, tracker, offer, program, leaderboard or ledger key names nothing; the error has filter and key. |
group-not-found | No group has this key in the environment; the error has group. |
Dashboards
A dashboard is a set of catalog-metric panels defined in code with defineDashboard and deployed from relay.json dashboards (see dashboards for the definition and the deploy rules). To choose its metrics and check it before handing it over, read analytics. Both reads need read.
domino dashboards list [--include-archived] (management.dashboards.list({ includeArchived }), GET /management/v1/dashboards?includeArchived=true) lists each dashboard's key, title, description, position, archivedAt and updatedAt (when its current version was published), ordered by position with dashboards without one last, then by title. Archived dashboards appear only with --include-archived.
domino dashboards get KEY [--range 7d|30d|90d|all] (management.dashboards.read({ key, range }), GET /management/v1/dashboards/KEY?range=…) reads one dashboard, archived ones included, over --range, or its defaultRange without one. Without --json it prints each section and each panel's total, breakdown rows and Other, or the panel's error: use it to check a dashboard you built. --json prints the read: the summary fields, definition (as stored, with defaultRange and each panel's width filled in), range, and panels, one per panel in reading order, each with its key and either result or error.
resultis the panel's metric query result for the dashboard's range, metric, breakdown, filters and ledger. Every panel reads as of the same instant. A broken-downlineorbarpanel reads the top 8 rows with the rest inother; atablepanel up to 100.erroris{ code, message }for a panel whose query failed:group-not-foundfor a group the environment lacks,panel-timeoutfor a query that ran past its time budget (each statement 2 seconds, and the read about 18 seconds, after which panels not yet read answerpanel-timeout),panel-failedfor any other failure, or another metric query code. The other panels still have results.
An unknown key is 404 not-found. domino dashboards archive|unarchive KEY --reason TEXT (management.dashboards.archive|unarchive(key, { reason }), POST /management/v1/dashboards/KEY/archive|unarchive) archives and restores one in test (publish), returning { dashboard, changed }; live refuses it with promotion-required. Each is a saved change; see lifecycle retries. Archiving takes it out of the default list; deploying its code again leaves it archived.
Lifecycle retries
Resource-specific archive, unarchive, pause and resume commands use the matching management path verb, and trackers refresh its own. Each is a saved resource change: pass --operation-id <uuid> to supply its ID, and resume an uncertain one with operations resume. --action-id <uuid> instead sends that action ID without saving the change, as these commands did before they were saved; retry identical inputs with the same ID. Passing both fails with INVALID_ARGUMENT. trackers pause|resume, groups archive|unarchive and schedules pause|archive|resume take an optional --reason TEXT for the audit log; quest, offer and redemption pauses require one. Quest, offer and redemption pauses require the latest --expected-revision (pauses list shows quest and redemption revisions, offers list offer revisions); a stale revision refuses the change. --revision is its deprecated name, removed in 2.0.0, and still works. redemptions cancel and entitlements restore take the same two options. Passing both with different numbers fails with INVALID_ARGUMENT, and passing neither with INPUT_REQUIRED. The retained resources archive|unarchive and quests archive|unarchive commands support the saved operation journal and operations resume. webhooks disable remains a deprecated alias of webhooks pause. trackers resume on an X tracker that Domino paused because its X budget is spent (pauseReason: "x-budget-exhausted" in trackers get) exits with the API's 409 x-budget-exhausted until resumesAt; one paused with test-window-ended resumes for another 14 days. trackers create|update --refresh-seconds below the plan's minimum still saves, and effectiveRefreshSeconds shows the interval the tracker runs at. trackers outcomes --json and trackers retry-outcome --json items carry enrollment (quest or campaign, an open string) on a not-enrolled skip, naming the enrollment the member lacks; outcomes recorded before it omit it.
Retry operator corrections
Quest enrollment and withdrawal, settlement reject and identity changes are saved resource changes with --operation-id, like the lifecycle commands above; --action-id sends them unsaved, and an actionId in a members unlink-identity, relink-identity, identity-sign-in or relink file counts as --action-id. Inventory rename/archive/unarchive/stock correction, webhook delete and webhook replay accept --action-id. Every test-members change accepts --action-id too, checked as an action ID (1–100 letters, digits, - or _) before anything is sent; see run the days for its answers. Save that ID with the exact input before sending. After an uncertain response, retry the same command with the same ID, revision or creation-time fence and reason. A new correction uses a new ID. A reused ID with different input returns action-id-reused.
Read state with quests enrollments, stock-items list, webhooks get or states, leaderboard-settlements get, or members identities before preparing a correction. members identities marks an X, Discord or Telegram account a member import vouched for with imported: true, and one an app login's server vouched for at sign-in also with vouchedBy: { integration, integrationName }. Every identity's source says where it came from: member, import (with the import's ID as import), app-login or test-link. identity-integrations list reports each key's vouchesAccounts. See operator undo commands and webhook deletion for arguments and refusal recovery. CLI session writes remain Test-only.
Scheduled jobs
domino schedules list --json reads the active staged build in test, or the live app's build with --environment live. It returns { state, schedules }; each schedule has its declaration, nextRun, blocked and nullable lastRun. A run includes id, build, startedAt, scheduledAt, manual, status, nullable duration in milliseconds, nullable error and result: the app's { outcome, summary?, metrics?, diagnostics? }, null when it sent none and absent on runs from before results. Times are epoch milliseconds. nextRun: null means the schedule is stopped or blocked. The six control commands share this result shape. schedules runs NAME --json returns { runs }, the schedule's latest 20 runs newest first, in the same run shape. All seven ship result schemas in the automation contract.
With publish, schedules run NAME queues one manual run, and pause, archive and resume change project-wide state, with an optional --reason TEXT for the audit log. recover NAME --yes acknowledges the previous job has stopped and permits future ticks. JSON mode never prompts, so recovery without --yes fails with INVALID_ARGUMENT before sending. A queued or blocked job refuses another run with CONFLICT; inspect it and recover only after checking its work has stopped. See hosted jobs.
Badge catalog and member corrections
domino badges list --json returns the managed catalog, including archivedAt. domino badges member MEMBER --json returns { items: [{ badge, awardedAt }] }. The create/update/archive/unarchive commands return a managed badge. Award/revoke commands return { badge, member, awarded, changed }: changed: false means the member already had the requested presence. Each write uses the normal saved operation journal and supports uncertain-answer recovery. Award and revoke require --reason.
Rewards in quests get, quests list, releases get, attempts get and the other results that carry a reward bundle may include a group membership reward: a kind: "tier" reward whose reward.group is the group key. Treat reward.badge and reward.group as distinct from an ordinary tiered reward, which has neither. groups members and groups member list a member a quest reward added like any other; Console Audit history names the completion that added them. See group membership rewards.
The same results' releases also carry integrationModules, every integration module the quest declares, beside integrations, which lists only discord and x; and rewardEntries, every reward as { type, data }, beside rewards. Read the open fields to see a module or reward kind this CLI's SDK does not know. Results from older Relay versions omit both.