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.

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.

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 agents list -o name | xargs -n1 jnh agents 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.

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, an error envelope is returned.

Error envelopes

When using -o json, errors follow a standard envelope structure:

{
  "error": {
    "code": 404,
    "reason": "NOT_FOUND",
    "message": "The dataset was not found.",
    "origin": "server"
  }
}
Field Description
code HTTP or gRPC status code (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

By default, list commands automatically paginate through all available records until the full set is returned.

To limit the number of returned items, pass --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.

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"
}

The request body is written to stdout while the dry-run warning is sent to stderr. This makes it straightforward to capture the payload:

jnh datasets create analytics --location ap-northeast-1 \
  --dry-run -o json > request.json

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. 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.

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.

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

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 labels.