This reference describes the command contract and schemaVersion: 1 output of Portrait CLI 0.1.9. Check portrait --version and portrait --help against your installed package before building an integration. Follow Install and use the CLI for setup and account requirements.
Commands and effects
| Command | Effect |
|---|---|
auth login, auth status, auth logout |
Connect, check sign-in state, or remove this installation's connection |
sessions list, sessions history |
Read accessible conversations or a retained transcript |
sessions create |
Create an empty session; --resume resumes the specified conversation |
ask |
Send a new research turn; it can propose or execute supported work subject to its confirmation and usage rules |
files upload |
Save one explicitly selected regular file to My Library |
files status, files read |
Check processing or read extracted LLM text |
files delete |
Permanently remove the specified known document, without an interactive confirmation |
monitors list, monitors get |
Read one page; get --mark-read also changes the read state of that returned content |
install-skill |
Write the bundled agent skill locally; --force replaces its existing portrait directory recursively |
Authentication alone does not grant Easel access. ask and sessions require both full Easel access and the provisioned CLI bridge. File and direct Monitor operations have their own access checks. A read-like command name or a successful login does not authorize a later write.
ask accepts exactly one non-empty prompt argument. Place --session, --document, and --stream before it. Use shell-safe quoting; currency written as USD 10 billion avoids shell expansion of a dollar-prefixed number.
portrait ask --stream 'Compare the latest earnings commentary for AAPL and MSFT.'
JSON envelopes
With non-interactive stdout, a non-streaming operational command writes one JSON envelope. A successful command exits with status 0; a reported CLI failure exits with status 1. Help and version remain plain text. Interactive authentication and non-streaming interactive errors use concise text; login instructions and diagnostics go to stderr. Keep stderr separate from a JSON parser.
Illustrative ask success; the UUID and answer below are placeholders:
{"schemaVersion":1,"ok":true,"command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","finalText":"Illustrative answer."}}
Illustrative failure before a selected document is ready:
{"schemaVersion":1,"ok":false,"command":"ask","error":{"code":"DOCUMENT_NOT_READY","message":"Document is not ready.","httpStatus":null,"retryable":false}}
command identifies the operation, for example ask, files.status, or sessions.list. Require a supported schemaVersion, inspect ok, and read the command-specific data. Do not assume all data uses the same identifier casing. Error fields are code, message, nullable httpStatus, and boolean retryable. This schema does not include a serialized retry-delay field.
Authentication errors
Authentication uses the operating system keyring. Keep stdout and stderr separate: the browser verification instructions are not JSON response data.
| Error | Meaning and recovery |
|---|---|
DEVICE_CODE_EXPIRED |
Restart auth login and use the newly printed code |
AUTHORIZATION_DENIED |
Authorization was declined; do not retry automatically |
AUTH_NETWORK_ERROR |
The authorization service could not be reached or its response could not be read; check connectivity before retrying login |
AUTH_LOGIN_IN_PROGRESS, AUTH_MUTATION_IN_PROGRESS |
Another authentication change holds the local lock; finish that operation first |
KEYRING_TIMEOUT, KEYRING_BACKEND_ERROR, KEYRING_HELPER_ERROR |
Credential-store access failed; repair the OS keyring or execution environment |
KEYRING_WRITE_UNCONFIRMED, KEYRING_STATE_UNKNOWN |
Stored sign-in state is uncertain; restore keyring access and sign in again |
KEYRING_DELETE_UNCONFIRMED |
Local credential removal was not confirmed; do not report successful local logout |
LOGOUT_INCOMPLETE |
Read the message's separate remote-revocation and local-removal results; neither implies the other succeeded |
Successful logout data includes environment, revocationAttempted, revocationSucceeded, and localRemovalSucceeded. With no stored credential, no revocation may be needed. On failure, these outcomes are described in the error message rather than a success data object.
If local removal succeeded but remote revocation failed, running logout again cannot revoke a token that is no longer stored. Contact support if remote revocation must be confirmed. Do not collect or transmit the token to diagnose this condition.
Streaming completion
ask --stream writes NDJSON on stdout. Each line is one independent JSON object; there is no enclosing array and no ok field on these stream records.
{"schemaVersion":1,"type":"answer.delta","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","delta":"Illustrative "}}
{"schemaVersion":1,"type":"answer.final","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","finalText":"Illustrative answer."}}
{"schemaVersion":1,"type":"operation.complete","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001"}}
Use deltas for progress display only. The authoritative complete answer is answer.final.data.finalText; do not reconstruct it by joining deltas. Retain the returned session_id and require operation.complete plus a successful process exit before reporting transport completion. A completed transport still does not establish that the research answered the question correctly or that every proposed action ran.
A streaming failure is an operation.error record with the error at the top level:
{"schemaVersion":1,"type":"operation.error","command":"ask","error":{"code":"DOCUMENT_NOT_READY","message":"Document is not ready.","httpStatus":null,"retryable":false}}
EOF, a process interruption, or some received deltas without a successful terminal sequence leave the outcome uncertain. Preserve partial text as partial; do not label it a completed answer. Handle unknown fields without discarding known data, and stop for review if the schema version is unsupported.
Identifiers, readiness, and pagination
All paths in this table are within the JSON envelope. Each read makes one page or chunk request; the CLI does not automatically exhaust the result.
| Operation | Identifier or state to retain | Continue / finish |
|---|---|---|
sessions list --limit N |
IDs in data.sessions; data.next_page_cursor |
N is 1–50. Pass a non-empty returned cursor unchanged to --cursor; null or absent means no next page |
sessions create, ask |
data.session_id |
Reuse the returned ID. Resume preserves the durable identity; an already-active resume can return data.already_active=true |
files upload |
data.document_id |
Upload accepts one regular file, at most 26,214,400 bytes (25 MiB); it does not wait for extracted text to finish |
files status |
data.documentId, data.representations, data.llmReady |
Only llmReady=true establishes that CLI text reading and ask --document can use the LLM representation |
files read |
data.content, data.offset, data.limit, data.total_size |
Pass returned data.nextOffset as the next --offset; its absence means the end was reached. Default offset is 0 and limit is 65,536; maximum limit is 4,194,304 |
monitors list |
data.total, data.limit, data.offset, and each datapoint_monitor_id |
Default limit 50, maximum 100. Advance --offset with the page's limit until the returned total is covered; preserve the filters |
monitors get |
data.next_cursor |
Pass a non-empty cursor unchanged to --cursor; null or absent means no next page. Maximum content limit is 100 |
files read returns extracted LLM text, not original binary bytes. An uploaded Word document or workbook cannot be faithfully reconstructed from that text alone. A document still being prepared causes ask --document to refuse with DOCUMENT_NOT_READY; use a later status check instead of repeatedly submitting research turns.
For Monitor company filters, use full composite tickers returned by Portrait. Responses retain company UUID arrays and positionally matching tickers arrays; a ticker can be null. Do not shift the arrays or guess a ticker for an unresolved company. monitors get --project can pin a known Monitor to the Project ID returned by discovery; omit it when not needed.
--mark-read marks only the content retrieved in that page, after the read. Omit it for inspection that must preserve unread state. Paging through a changing unread feed while marking pages changes that feed; do not equate one marked page with marking the entire Monitor.
Recover from an uncertain request
- Keep the session ID, command, exit status, and safely redacted error or partial output.
- If the request may have reached Portrait, inspect
sessions historyfor that session before repeating an instruction. A saved transcript can trail work in flight, so absence of a final answer is not proof that nothing ran. - Reconnect authentication when required. Use a returned session ID to resume; do not substitute a new conversation merely to avoid an access or capacity refusal.
- Treat
retryable=trueas a transient-error classification, not permission to repeat effects. Reconfirm any changed scope or proposed costly operation with the user.
If a failed ask returned no session ID, use sessions list to find candidate conversations from that attempt and inspect their histories before choosing one. Match the request and time; do not guess from a similar title. If the prior outcome cannot be identified, report it as uncertain instead of starting replacement work.
Idempotency and message identity
Within one ask invocation, the CLI reuses its clientMessageId while performing supported transport recovery. A second invocation generates a new message identity, even with the same --session. The session ID selects conversation context; it is not an idempotency key. Without --session, the invocation also creates a new conversation. There is no CLI option for making separate invocations the same research turn.
HTTP 429 and retry delays
For Easel operations, HTTP 429 can produce RATE_LIMITED (retryable=true) or USAGE_REFUSED (retryable=false). Inspect the error code as well as the status: a usage refusal needs its usage or access condition resolved before another attempt.
For RATE_LIMITED, the CLI can retry once within the same request when the server provides a numeric Retry-After delay that fits the CLI's bounded recovery budget. A missing, unusable, or excessive delay does not trigger that automatic retry. The JSON error envelope does not expose the header or a machine-readable retry delay. If the CLI returns the error, retain the session and review its history before deciding whether another ask is safe; do not infer a delay from the HTTP status or repeat the turn solely because retryable is true.
Stopping the local process or losing its network connection does not confirm remote cancellation. This CLI version has no public cancel or end command. Use the supported conversation controls in Portrait and verify the state; do not relaunch a screen or other workflow solely because its answer stream ended unexpectedly.
Validate a consuming agent
Before using a parser for real research, check it with local fixtures for a complete stream, a stream ending before completion, an operation.error, an unready document, two-page results, and a null company ticker. Verify that it preserves exact IDs, detects incomplete output, and never changes read state or repeats a write merely to recover from a read failure.