Skip to content

CLI scripting and automation

The jnh CLI is designed for both interactive terminal use and automated scripts or CI/CD pipelines. All commands follow a consistent set of conventions for output formatting, exit codes, error handling, and non-interactive execution.

Installation

jnh supports browser-based OAuth (Google and GitHub) and device-code flows for headless or SSH sessions, communicating over standard HTTP/JSON:

brew install flowerinthenight/tap/jnh
curl -fsSL https://storage.googleapis.com/jennah-cli-dist/install.sh | bash
irm https://storage.googleapis.com/jennah-cli-dist/install.ps1 | iex

To upgrade jnh to the latest release:

jnh upgrade

Version pinning and custom paths

Export JENNAH_VERSION=jnh-v1.2.3 before running the installation script to pin a specific release version, or set JENNAH_INSTALL_DIR to specify a custom target directory.

Upgrading Homebrew installations

When jnh is installed via Homebrew, use brew upgrade jnh rather than jnh upgrade so Homebrew tracks updates.

Output formats

Commands that return data support four output formats:

Format Description Primary use case
table Aligned columns with headers (default) Terminal reading
json Complete API response payload as JSON Programmatic parsing with tools like jq
ndjson Newline-delimited JSON (one record per line) Streaming and processing large datasets with bounded memory
name Resource identifiers only, one per line Shell loops, pipes, and command substitution

Format resolution order

The CLI resolves the output format in the following order of precedence:

  1. The -o or --output command-line flag
  2. The JENNAH_OUTPUT environment variable
  3. Default format: table
# Table format (default)
jnh datasets list

# Explicit JSON output
jnh datasets list -o json

# Set default format for all commands in this shell
export JENNAH_OUTPUT=json
jnh datasets list

# Flag overrides the environment variable
jnh datasets list -o table

Deterministic output

The output format does not change based on whether stdout is connected to a terminal or redirected to a pipe. For example, jnh datasets list and jnh datasets list | cat produce identical output. To guarantee structured output in automated environments, set JENNAH_OUTPUT=json or pass -o json explicitly.

Commands that do not return structured data (such as login, logout, version, enterprise switch, and completion) reject explicit structured output flags like -o json. If JENNAH_OUTPUT is set in the environment, these commands ignore it and execute normally without error.

Table vs. JSON data

The table format selects common fields and truncates long values for terminal readability. To view all available fields without truncation, use -o json.

Compact JSON

-o json output is indented. Pass --compact to print it on one line instead:

jnh datasets get analytics -o json --compact

--compact applies to -o json only. It is rejected with -o ndjson, which is already one line per record, and with the other formats.

Selecting fields

Use --fields to limit output to specific columns or JSON keys:

# Display two columns in table format
jnh datasets list --fields datasetId,status

# Filter JSON output to specific fields
jnh datasets list --fields datasetId,status -o json

# Output identifiers only (one per line)
jnh datasets list -o name

# Pipe identifiers into other commands
jnh datasets list -o name | xargs -n1 jnh datasets get -o name

--fields accepts the column names displayed in table headers. If an invalid field name is passed, the CLI returns an error listing the valid fields. For complex filtering, sorting, or nested queries, use -o json with jq.

The -o name format prints resource identifiers without headers or decorative text. For commands that do not produce resource identifiers (such as delete operations), -o name produces no standard output.

Non-interactive flag

jnh uses --yes to bypass interactive confirmation prompts. The -q and --quiet flags are not supported.

Streaming output

The -o ndjson format outputs one JSON object per line. When a response contains multiple entity types or sections, each line includes a section field alongside the record payload:

jnh scopes memory inspect my.agent -o ndjson
{"section":"chunk","record":{"chunkId":"chunk_...","tokenCount":"56"}}
{"section":"node","record":{"nodeId":"n_...","label":"Chew"}}

You can process streaming records in real time:

# Stream memory updates and extract chunk IDs
jnh scopes memory inspect my.agent --tail -o ndjson | jq -r '.record.chunkId'

When --tail is used, -o json is rejected because an open-ended stream cannot form a single closed JSON document. Use -o ndjson for continuous streams.

jnh scopes memory and jnh agents memory

Memory is held by a scope, and an agent workspace is one kind of scope, so jnh scopes memory <verb> <scope-id> is the canonical spelling and works for either kind. jnh agents memory <verb> <agent-instance-id> is the agent-shaped spelling of the same operations, with the same flags, and is unchanged: scripts written against it keep working. The one exception is form, which exists only as jnh agents memory form, because memory formation is available on agent workspaces alone.

Error handling and exit codes

Diagnostics and error messages are written to stderr. When a structured output format is requested, stdout receives only valid data on success. On failure with -o json or -o ndjson, an error envelope is written to stderr.

Error envelopes

When using -o json or -o ndjson, errors are written to stderr in a standard envelope structure:

{
  "error": {
    "code": 404,
    "reason": "NOT_FOUND",
    "message": "The dataset was not found.",
    "origin": "server"
  }
}
Field Description
code HTTP status code of the response (omitted if the request was not sent)
reason Machine-readable error code
message Human-readable explanation
hint Optional remediation guidance
origin Origin of the failure (client, server, or transport)

Error origins

Scripts can inspect the origin field to determine how to handle a failure:

  • client: The CLI rejected the command locally before sending any network request (e.g., conflicting flags or invalid syntax). The reason field begins with a CLI_ prefix (such as CLI_UNKNOWN_COLUMN or CLI_CONFLICTING_FLAGS). Retrying without changing the input will produce the same result.
  • server: The server rejected the request (e.g., TRIAL_EXPIRED, PERMISSION_DENIED, or NOT_FOUND). Correct the input, update permissions, or address server state before retrying.
  • transport: The request could not reach the server or timed out before receiving a response. Transient network errors can typically be retried.

Exit codes

jnh returns standard exit codes so scripts can check status without parsing error messages:

Exit code HTTP / gRPC status Description
0 - Success
1 - General or local validation failure
2 400 Bad Request Invalid argument
3 403 Forbidden Permission denied
4 401 Unauthorized Unauthenticated
5 404 Not Found Resource not found, or not reachable with your selectors
6 501 Not Implemented Unimplemented operation
7 409 Conflict Resource already exists
130 - Interrupted (SIGINT)

Exit code 7 allows creation scripts to treat pre-existing resources as non-fatal when desired.

Exit code 5 deserves one note, because it covers two outcomes a script may want to tell apart and cannot. On a named agent, subject scope, or dataset, the platform answers a resource you do not reach exactly as it answers one that does not exist, so that a refusal cannot be used to discover which identifiers are real (see Reading a denial). A script that treats 5 as "it is gone, carry on" should be sure that losing reach would also be safe to carry on from.

Pagination and limits

Most list commands (datasets list, keys list, the team listings, and approvals allowlist list) automatically paginate through all available records until the full set is returned.

To stop a listing early, pass --limit (keys list does not support --limit). When --limit is specified, the response includes a pagination token if additional records remain:

# Fetch up to 10 datasets and check for a continuation token
jnh datasets list --limit 10 -o json | jq '.nextPageToken'

A non-empty nextPageToken indicates that more results exist on the server.

agents list, scopes list, and approvals list work differently: each run sends one request and prints one page. They take --page-size and --page-token instead of --limit. When more remain, table output displays the continuation token, and -o json returns it as nextPageToken. To read the whole listing, repeat the command with --page-token until the token comes back empty:

tok=""
while :; do
  page=$(jnh agents list -o json --page-token "$tok")
  echo "$page" | jq -r '.agents[]?.agentInstanceId'
  tok=$(echo "$page" | jq -r '.nextPageToken // ""')
  [ -z "$tok" ] && break
done

Write operations

Dry runs

Mutating commands support the --dry-run flag. This prints the request payload to stdout without sending it to the server:

$ jnh datasets create analytics --location ap-northeast-1 --dry-run
DRY RUN: nothing was sent, and nothing was checked with the platform.   # stderr
Permissions, quota, and every other server-held condition are unknown here;
only what the CLI can determine on its own has been applied.
POST /v1/datasets                                                       # stdout
{
  "datasetId": "analytics",
  "location": "ap-northeast-1"
}

In table output, the request body is written to stdout while the dry-run warning is sent to stderr.

With -o json or -o ndjson, the stderr warning is not printed. Instead, stdout receives a single object that wraps the request and notes that nothing was checked:

$ jnh datasets create analytics --location ap-northeast-1 \
    --dry-run -o json
{
  "dryRun": true,
  "method": "POST",
  "path": "/v1/datasets",
  "checkedWithPlatform": false,
  "request": {
    "datasetId": "analytics",
    "location": "ap-northeast-1"
  }
}

To capture only the request body, select it with jq '.request'.

A dry run performs local syntax and client-side validation. It does not verify server-side conditions (such as permissions, quotas, or resource state). Non-mutating read operations (such as resolving dependencies) may still occur during a dry run to construct the request.

Non-interactive execution

Destructive commands (such as datasets delete, agents delete, and keys revoke) prompt for interactive confirmation by default. A command whose replacement semantics make omission destructive prompts on the same terms: vocabulary declare replaces the declaration at its level, so it prints what the new declaration drops and then asks. In non-interactive environments (or when stdin is not a terminal), these commands fail unless --yes is passed:

$ jnh agents delete my.agent < /dev/null
error: refusing to delete agent "my.agent" without confirmation:
       nothing was read from stdin
hint:  pass --yes to assert the intent explicitly

To run destructive commands in scripts or automation pipelines, pass --yes:

jnh agents delete my.agent --yes

Environment variables set by CI runners (such as CI=true) do not bypass confirmation prompts; the --yes flag must be provided explicitly.

Forming memory

jnh agents memory form submits conversation turns to memory formation and prints the receipt. The request is a JSON or YAML document with the same fields as the REST request body, read from a file or from standard input:

jnh agents memory form support-bot --from-file turns.yaml
transcript-to-turns | jnh agents memory form support-bot --from-file -
Flag Description
--from-file The request document, or - for standard input (required)
--observed-at When the conversation took place; overrides the document's observedAt
--formation-key Send this formation key instead of the derived one
--no-formation-key Send no key, to form the same conversation again on purpose
--timeout How long to wait for the receipt (default 30 minutes)

Without --formation-key, the command derives a key from the request's content, so rerunning a command whose formation committed replays that receipt instead of forming twice. A rerun after a formation that stored nothing forms again. The receipt names the key it sent.

A formation can take minutes, so the command waits longer than other commands. Interrupting it, or reaching --timeout, cancels a formation still in progress. A cancelled formation writes nothing, but if it already reached the model it still counts toward your formation allowance. The failure reports CLI_TIMED_OUT with a hint naming the key to resend under.

Authentication in scripts

Authenticate automated scripts using an API key via environment variables or flags:

export JENNAH_API_KEY=jennah_sk_...   # or pass --api-key
export JENNAH_ENDPOINT=...            # optional; or pass --endpoint

API keys are scoped to a specific enterprise and do not require interactive login. See Roles and access control for key management and permission scopes.

Management-class operations are unavailable to an API key under any permission grant, so the CLI refuses them locally rather than issuing a request that cannot succeed. These include role and invitation management, key creation and revocation, and the memory vocabulary writes (vocabulary declare and vocabulary remove). Their reads are unaffected where the underlying permission is not management-class.

Help text and command output reference variable names rather than their values, preventing accidental credential exposure in logs.

Enterprises and plan

jnh enterprise list lists the enterprises you belong to, with your role in each, and marks the active one with *. jnh enterprise switch <enterprise-id> makes another of them active for the logged-in session. Switching is unavailable with an API key, because a key belongs to one enterprise.

jnh billing shows the active enterprise's plan: its tier, the source that owns it, whether it is managed outside Jennah (and if so, the URL where it is managed), and the subscriptions bound to it. It also shows the formation units used against the allowance in the current window (the calendar month in UTC on a paid plan, the day in UTC on the free plan, the whole trial on a trial). On a plan that offers overage, it shows the overage units used, the overage multiple, the overage band in units, and whether overage is billed. Usage is reported in units only: no remaining count and no price. Run jnh usage for a breakdown of formations and tokens.

On a plan billed for overage, formations past the allowance continue into an overage band equal to the multiple times the plan's monthly fee, in units, and are refused once the band is used. jnh billing set-overage-cap <multiple> changes the multiple, from 0 to 10 in steps of 0.5. The default is 2, and 0 turns overage off, so formations are refused at the allowance. The change applies to the next formation. Only a root or administrator signed in with jnh login can change it. An API key cannot change it, even one created by an administrator; the CLI refuses the command locally when an API key is in use.

jnh enterprise list
jnh billing
jnh billing -o json
jnh billing set-overage-cap 4
jnh billing set-overage-cap 0    # overage off

Your own formation model

jnh formation-model runs memory formation on your organization's own model in Amazon Bedrock. prepare --provider bedrock prints the values you grant access with, set checks the model with one formation call and saves it only if that call works, get shows it, check runs that same check against the saved model without changing it, and remove goes back to Jennah's model. Only a root or administrator signed in with jnh login can run them; the CLI refuses them locally when an API key is in use. jnh billing shows how many formations were sent to your model this period, failed ones included. See Your own model.

jnh formation-model prepare --provider bedrock
jnh formation-model set --provider bedrock \
  --role-arn arn:aws:iam::<account>:role/<role> \
  --region ap-northeast-1 \
  --model global.anthropic.claude-sonnet-5-5
jnh formation-model get
jnh formation-model check
jnh formation-model remove

Shell completion

Generate shell autocompletion scripts for your shell:

jnh completion bash > /etc/bash_completion.d/jnh   # also zsh, fish, powershell

Tab completion dynamically queries the authenticated enterprise, providing completions for resource identifiers such as dataset IDs, agent IDs, and API key IDs (each key ID is shown with its label as the description).