Operations CLI
The how-to is Operations CLI; this page is the contract scripts rely on. The exit codes, the error object and the stdout shape are stable across releases.
Commands¶
forze ops list [--app REF] [--json]
forze ops describe [--app REF] OP [--json]
forze ops call [--app REF] OP
[--input JSON|@PATH|- | --FIELD VALUE ... | --batch @PATH|-]
[--tenant UUID] [--idempotency-key KEY]
[--timeout SECONDS] [--grace SECONDS]
[--correlation-id UUID] [--causation-id UUID]
[--api-key-file PATH] [--pretty]
OP is the operation key, dots included: orders.get. --app names the OpsApp as
module:attribute, or a zero-argument callable returning one, and falls back to
FORZE_OPS_APP. A group from build_ops_cli(app) has the declaration bound and no --app.
OP completes in the shell after describe and call once completion is installed with
forze --install-completion. Completion loads the declaration from --app or
FORZE_OPS_APP as list does, never builds the runtime, and offers nothing when the
declaration cannot load. A group from build_ops_cli completes from its own declaration,
installed by your root CLI's --install-completion. Every TAB imports the application
module, so its import side effects run in the operator's shell.
list and describe never build the runtime. They write text, or JSON with --json.
describe shows the title and description, the kind, the declared authn requirement and
permissions, idempotency support, the deadline, the typed flags, the query surface, and the
input and output JSON Schemas.
call options¶
| Option | Effect | Refused when |
|---|---|---|
--input JSON / @PATH / - |
the input object, inline, from a file, or from stdin; validated into the operation's input DTO, unknown keys refused | the operation takes no input; the JSON does not parse (NaN and Infinity included) or is not an object; the file cannot be read; the input is inline and the DTO declares a SecretStr or SecretBytes field |
--batch @PATH / - |
one call per line of a JSONL file or of stdin, in one runtime; see Batches | the value is neither @PATH nor -; combined with --input, typed flags, --idempotency-key or --pretty |
--tenant UUID |
a requested tenant, validated against the principal's membership, or matching the key's own tenant | the identity is not an API key; nothing can validate it |
--idempotency-key KEY |
binds the key: a repeat with the same input replays the stored result | the operation does not support one |
--timeout SECONDS |
bounds the operation; only ever tightens its declared deadline | not a positive, finite number |
--grace SECONDS |
how long a first SIGINT or SIGTERM waits for the call to finish; default 10 | negative, or not finite |
--correlation-id UUID, --causation-id UUID |
carried into the invocation metadata | malformed |
--api-key-file PATH |
reads the API key from a file instead of the environment | the identity is not an API key; the file cannot be read |
--pretty |
indents the result |
A malformed --timeout, --grace, --tenant, --batch or id is a parse error: Click's usage text on
stderr, exit 2, nothing on stdout. Every other refusal is an error object, an option call
does not have included: it is read as a typed flag.
Typed flags¶
An operation whose input is flat scalars also takes that input as flags after OP, instead of
--input:
$ forze ops call notes.create --title "Quarterly report"
| Field type | Flag |
|---|---|
str, int, float, Decimal, UUID, date, datetime, an Enum of strings, an IntEnum, a Literal of strings; optional or constrained ones too |
--field-name VALUE, or --field-name=VALUE |
bool |
--field-name sets it, --no-field-name clears it |
- Spelling. A field's flag is its name with
-for_. An alias never is. - Validation. The flags build the object
--inputwould carry, and it is validated the same way: each value is the string you typed, coerced by the DTO; an omitted flag takes the field's default; a missing required field or a bad value exits 4 and is never echoed. - A value that looks like a flag.
--title --prettygives--prettytocall, and--title --draftis refused: the token after a flag that needs a value cannot start with--. Write--title=--pretty. - On argv. Every value is on the command line, where
psand shell history show it. A secret field is refused; pass any other sensitive value with--input @PATH. - All or none. An input takes no flags when one of its fields is anything else (a list, a
model, a union), when it declares a
SecretStrorSecretBytesfield, or when a flag would be spelled like acalloption (--timeout,--input, …) or like another field's. Acalloption means the same for every operation.describelists the flags, or says why there are none. - Exclusive. Typed flags and
--inputcannot be combined.
Batches¶
--batch @PATH or --batch - calls the operation once per line of a JSONL file, or of stdin,
inside one runtime: one startup and one shutdown for the whole batch. Each line is an input
object, as --input would carry; blank lines are skipped.
$ forze ops call notes.create --batch @notes.jsonl
{"line": 1, "result": {"id": "…", "title": "a", …}, "execution_id": "…", "correlation_id": "…"}
{"line": 2, "result": {"id": "…", "title": "b", …}, "execution_id": "…", "correlation_id": "…"}
- Validated first. Every line is read and validated before the runtime is built. A line that does not parse or validate is the only line written, and nothing runs. The whole input is held in memory, so split a file too large for that.
- One line out per call. Stdout holds one JSON document per line that ran, in order, each
written as its call finishes:
{"line": N, "result": …}or the error object with"line": N. Each carries its ownexecution_id; all share onecorrelation_id, the--correlation-idgiven or one minted for the batch. Each line is a governed call: its identity is verified and its tenant resolved again. - Stops at the first failure. The failed line is the last one written, and its exit code, by the table below, is the batch's. Exit 0 means every line succeeded.
- No idempotency key. One key would claim every line, so
--idempotency-keyis refused. A retryable failure therefore exits 75 from a query and 69 from a command. - Signals. A signal stops the batch: the line in flight gets
--graceas a single call does, and no line starts after it. When lines remained, the batch exits 130 or 143. - The batch as a whole. A refusal before any line runs (a conflicting option, an unreadable
file, a runtime that does not start) is one error object without
line.
Output¶
After the declaration loads, call writes exactly one JSON document to stdout, followed by a
newline. For the duration of the call, file descriptor 1 points at stderr, so whatever the
application logs or prints lands there.
The result is encoded with the operation's declared output type. An operation that returns
nothing writes null.
The error object is the masked error envelope: a server-side error carries the generic detail, and a denial about a resource type a non-disclosing posture covers reads as not found.
{"error": {"code": "core.authentication", "kind": "authentication", "detail": "Invalid API key",
"retryable": false, "context": {}},
"execution_id": "…", "correlation_id": "…"}
execution_id and correlation_id are always present, whatever failed: they are minted before
the first step that can fail.
An interrupt before any commit started writes the one kind the CLI adds:
"code": "interrupted", "kind": "interrupted", a detail naming the signal, and
"retryable": false, since an operation may have side effects outside its transaction.
Two failures write nothing to stdout:
| When | Output | Exit |
|---|---|---|
| Parsing the arguments | Click's usage text on stderr | 2 |
Loading the declaration: no --app, an import error, a declaration that refuses itself, an object that is not an OpsApp |
a message, or the traceback without locals, on stderr | 2 when none is named, else 78 |
Exit codes¶
The code is derived from the error envelope's kind, after the denial posture applies. Retryability is read from the kind's egress policy first.
| Exit | Meaning | From |
|---|---|---|
| 0 | success | |
| 1 | internal error | internal, including commit_ambiguous; any unhandled exception |
| 2 | usage | a parse error; an unknown or unexposed operation; an input or option the operation cannot take |
| 3 | not found | not_found, including a collapsed denial |
| 4 | rejected | validation, domain, precondition, conflict |
| 5 | denied | authentication, authorization |
| 6 | timed out | timeout |
| 69 | temporary failure; a write may have landed | a retryable kind, from a command called without an idempotency key |
| 75 | temporary failure; safe to retry as is | a retryable kind, from a query or from a command called with an idempotency key |
| 78 | configuration | configuration; the declaration or the runtime refused before the operation ran, a runtime that is not SERVERLESS included |
| 130 / 143 | interrupted | SIGINT / SIGTERM, cancelled before any commit started |
69, 75 and 78 are EX_UNAVAILABLE, EX_TEMPFAIL and EX_CONFIG from sysexits.h. The
retryable kinds are concurrency, throttled, infrastructure and unavailable.
Error codes the CLI adds¶
| Code | Kind | Exit | Raised when |
|---|---|---|---|
cli_unknown_operation |
validation |
2 | the operation is unknown, or not exposed |
cli_input_not_accepted |
validation |
2 | --input, typed flags or --batch given to an operation that takes none |
cli_option_conflict |
validation |
2 | typed flags given with --input; --batch given with --input, typed flags, --idempotency-key or --pretty |
cli_flags_unsupported |
validation |
2 | typed flags given to an operation whose input takes none; the detail says why |
cli_malformed_flags |
validation |
2 | the typed flags do not parse: an unknown flag, a missing value or one spelled like a flag, a value to a switch, a field given twice, or an argument that is not a flag |
cli_input_unreadable |
validation |
2 | the @PATH file of --input or --batch cannot be read |
cli_malformed_input |
validation |
2 | the input, or a batch line, is not JSON, or not an object |
cli_inline_secret |
validation |
2 | the input declares a secret and was given inline or as typed flags |
cli_idempotency_unsupported |
validation |
2 | --idempotency-key on an operation that does not support one |
cli_tenant_unverifiable |
validation |
2 | --tenant with an anonymous or static identity |
tenant_request_unverifiable |
precondition |
2 | --tenant that neither a resolver nor the key's own tenant can validate |
cli_api_key_file_unused |
validation |
2 | --api-key-file without an API-key identity |
cli_api_key_unreadable |
validation |
2 | the --api-key-file file cannot be read |
cli_invalid_input |
validation |
4 | the input does not validate, or names an unknown key |
cli_api_key_missing |
authentication |
5 | the key's environment variable is unset or blank, and no file is given |
cli_runtime_not_serverless |
configuration |
78 | the runtime factory returned a runtime that is not SERVERLESS |
cli_unverified_identity_unacknowledged |
configuration |
— | a StaticIdentity built without acknowledge_unverified_identity=True; raised while the module imports |
Environment¶
| Variable | Read by | Default |
|---|---|---|
FORZE_OPS_APP |
--app |
none |
FORZE_API_KEY |
ApiKeyIdentity(env=…) |
the variable an API-key identity reads |