---
name: okra-agent
description: Teach coding agents to use the okra CLI context commands for cited, bounded PDF work.
---

# okra-agent

Use this skill when a user asks you to inspect, answer questions about, or extract evidence from PDFs with the `okra` CLI. Prefer the context-first commands because they return bounded, cited context instead of forcing you to read entire documents.

## Install

```bash
npm install -g @okrapdf/cli   # the `okra` binary (thin wrapper over @okrapdf/sdk)
```

## Auth

Live okra API calls require a paid-tier API key:

```bash
export OKRA_API_KEY="okra_..."
```

Do not print or commit the key. `okra auth login` also exists, but automation should normally rely on `OKRA_API_KEY`.

Local PDF-to-HTML streaming is a separate BYOK path and needs no Okra account:

```bash
export UNSTRUCTURED_API_KEY="..."
okra view ./paper.pdf --web
```

The viewer/server are local, but PDF page batches go directly to Unstructured.
The provider key is environment-only; never put it in argv. This local-only
command is intentionally outside the API-derived noun/verb reference below.

## Quickstart

Two paths, both cited. `--json` is implied when stdout is piped.

```bash
# 1 — Ask / pull evidence from a PDF
okra upload ./report.pdf --json                          # → result.document_id
okra context ask "<question>" --source-id <doc> --json   # cited answer + result.confidence
okra context get "<target>" --source-id <doc> --json     # bounded, cited blocks for a precise target

# 2 — Typed structured data
okra extract <doc-or-file> --schema ./schema.json --cite --json   # result.data + per-field citations
okra extract ./invoice.pdf --content-type invoice --json          # a content type supplies schema + grounding
```

Every result is a JSON envelope: **branch on `.ok` first**, read `result`, and run a relevant `next_actions[].cmd` instead of rediscovering the workflow. Citations are identical everywhere — Anthropic `page_location` (`cited_text`, `start_page_number`, `bbox`, a `citation_url` proof image, `match`). Prefer `--cite`/the context commands whenever a value is load-bearing; an ungrounded value reads as a guess. The rest of this skill is the detail behind these two paths.

## Full CLI Noun/Verb Reference

This skill is the context-first loop. For the complete agent surface — every
noun, verb, and action command — see the generated reference, which is derived
from the API resource catalog (`/v1/resources`) and cannot drift from it:

- [`CLI_REFERENCE.generated.md`](./CLI_REFERENCE.generated.md)

Do not hand-edit that file. It is regenerated by
`pnpm --filter @okrapdf/sdk docs:cli:reference` from
`src/cli/resource-catalog.generated.json`, and a cross-package contract test
(`src/cli/catalog-contract.test.ts`) fails CI if the CLI and the catalog drift.

## Canonical Agent Surface

Global CLI flags from the actual `okra` binary:

- `-j, --json`: structured JSON output.
- `-q, --quiet`: suppress progress and human-readable frills.
- `-o, --output <format-or-file>`: `json`, `table`, or an output file path.

Use these context commands:

```bash
okra context resolve [source] [--offline <fixture>] [--sha256 <sha>]
okra context structure [sourceId] [--offline <fixture>] [--max-depth <n>] [--no-artifacts]
okra context get <query...> [--offline <fixture>] [--source-id <id>] [--max-tokens <n>] [--section <id>]
okra context ask <question...> [--offline <fixture>] [--source-id <id>] [--section <id>]
okra context open [sourceId] [--offline <fixture>] [--page <n>] [--section <id>]
```

`--section <id>` is repeatable on `context get` and `context ask` — it **narrows** retrieval to those section(s) (use ids from `context structure`). An empty / `omitted_reason: "not_found"` result *with* `--section` means the query didn't match **within** those sections, not that it's absent from the document — drop `--section` or try a different section before concluding the content isn't there.

The short alias is:

```bash
okra ask <sourceId> "<question>"
okra ask "<question>" --source-id <sourceId>
okra ask "<question>" --offline <fixture>
```

Without `--doc`, `okra ask` delegates to `okra context ask`. With `--doc <id>`, it uses legacy processed-document chat; prefer the context surface unless the user explicitly gave a processed document ID and wants legacy chat.

## JSON Envelope

When stdout is not a TTY, eligible commands default to machine JSON. Piping to another process is enough:

```bash
okra context resolve "https://example.com/report.pdf" | jq .
```

Interactive terminals can force the same behavior with `--json` or `-o json`.

The CLI wraps JSON results in this envelope:

```json
{
  "ok": true,
  "command": "context resolve",
  "result": {},
  "cost": { "usd": null },
  "citations": [],
  "next_actions": []
}
```

Read `result` for the command-specific payload. Read `citations` for lifted citation URLs; context responses also include page-level `result.citations[*].citation_url` values, which are deterministic `res.okrapdf.com` URLs. Read `next_actions` as the CLI's suggested follow-up commands and run the relevant ones instead of rediscovering the workflow from docs.

**`citation_url` needs your API key.** It's the page-image proof for a citation. For an owned (private) doc, fetch it with the same `Authorization: Bearer $OKRA_API_KEY` header — without it the image returns `404` (private docs are not anonymously readable; the 404 is auth, not a missing image). Public docs render without auth.

**Errors are the same envelope with `ok: false`** — not just confirmation gates. Every failure (not found, bad input, upstream error) prints a machine envelope to **stdout** so `... --json | jq` always parses, and exits nonzero:

```json
{
  "ok": false,
  "command": "context resolve",
  "error": "not_found",
  "message": "No owned document matches this source. Upload it first (okra upload).",
  "code": 404,
  "next_actions": []
}
```

Always branch on `.ok` first. On `ok: false`, read `error` (stable code), `message` (human reason), and run a relevant `next_actions[].cmd` if present. Do not scrape stderr for failures — stderr carries only human log lines; the parseable error contract lives on stdout.

## Confirmation Gates

Some commands intentionally refuse in JSON/non-TTY mode before public, irreversible, or human-attested actions. A refusal has `ok: false`, `error`, `message`, and `next_actions`.

If `error` is `confirmation_required` or `human_review_required`, stop and show the user the gate details. Do not add `--confirm-rights`, `--yes`, `--attest`, or similar confirmation flags yourself. Only rerun the suggested `next_actions[].cmd` after the user explicitly approves the exact action.

Current examples:

```bash
okra collections publish <collection> --json
okra audit <doc_id> --attest --json
```

Public publish requires `--confirm-rights` because it asserts ownership or license rights. Audit attestation is never auto-flipped by the CLI; open the audit report and resolve the human review gate.

**`audit` and `redact` run on the okraPDF self-host runtime, not the cloud API.** Against the cloud API they return `ok: false`, `error: "not_supported_on_cloud"` (these are recipe workflows — PDF/UA · WCAG accessibility audit, policy-driven redaction), with `next_actions` pointing at `okra serve` (start the local runtime) or `okra profile use <self-host-profile>`. Switch to a self-host profile (or run `okra serve`) before invoking them; don't treat the cloud refusal as a document failure.

## Upload & Long Documents

```bash
okra upload ./report.pdf --json                 # waits up to 300s, returns doc id + URLs
okra upload ./report.pdf --no-wait --json       # returns doc id + job_id immediately
okra jobs wait <jobId-or-docId> --timeout 600 --json
okra upload - < ./report.pdf                    # explicit stdin source; a file path never reads stdin
```

For 100+ page documents prefer `--no-wait` and resume with `okra jobs wait` — a wait timeout is normal, not a failure. Timeout envelopes carry `document_id` and `job_id` as fields; never re-upload to retry, and never parse ids out of error prose. `okra parse <docId> --engine <engine>` queues a reparse job through the same `jobs wait` lifecycle.

## Partial Parses

`context resolve` and `context structure` report `pages_parsed`, `page_count`, `page_coverage`, and a `warnings` array. If `page_coverage` is `partial` or `empty`, unparsed pages are invisible to `get`/`ask` — treat every absence as unknown and verify load-bearing claims against page images (`citations[*].citation_url`). Read the `warnings`: if it says the **parse is in progress**, the doc is still being parsed — **wait** (`okra jobs wait <docId>`), do not reparse. Only `okra parse <docId>` to re-run once the parse has **finished** but coverage is still incomplete.

## Scanned / image-only documents

Born-digital PDFs (with a text layer) parse instantly and free on the default first pass. Scanned / image-only PDFs need OCR, which the default routing now runs automatically — **no `--engine` needed**. OCR takes longer than a born-digital parse (typically tens of seconds for a few pages), so a scanned doc reads `page_coverage: empty` then `partial` until its pages finish.

While a parse is in flight, `context structure`/`resolve` return a `Parse in progress … wait` warning — **wait** (`okra jobs wait <docId> --json`), do **not** reparse (reparsing restarts the in-flight work). Re-check `pages_parsed` / `page_coverage` before relying on the content.

**A freshly-OCR'd doc can briefly lag its own `complete` status.** Right after a scanned upload reports `phase: complete` (or `context structure` shows `page_coverage: complete`), the very first `extract`/`get`/`ask` can occasionally return `ok: false` or empty data while the OCR'd text blocks finish becoming queryable. That is a transient settling lag, not a real failure: pause briefly (or `okra jobs wait <docId> --json`) and retry the **same** command **once** — do **not** re-upload or reparse (that restarts OCR). Born-digital docs do not hit this. If a retry still fails, treat it as a real error and read the `error`/`message`.

To pin a specific OCR vendor (e.g. a known-hard layout), pass `--engine`:

```bash
okra parse <docId> --engine llamaparse --json
okra jobs wait <docId> --timeout 600 --json
```

If the requested engine isn't available, the error (`error: "engine_not_available"`, code `400`) returns the live `available_engines` array plus a `next_actions[].cmd` pre-filled with a supported engine. **Read `available_engines` from that error envelope rather than hardcoding a name** — the set is server-driven and changes. It currently includes `llamaparse`, `mineru`, `docling`, `unstructured`, `reducto`, `google_document_ai`, `mistral_ocr` (and more). Pick one from the returned list, or omit `--engine` for default routing.

## Citation Precision

Cite the single governing source for each claim. When multiple similar regions match (versioned diagrams, repeated tables), check applicability (serial, revision, effective date) before corroborating with extra citations — an extra wrong-document citation reads as an error even when the extraction is right.

## Structured Extraction

When you need a typed object (not prose), use schema extraction instead of `ask`:

```bash
okra extract <doc_id> --schema ./schema.json --cite --json          # one document, grounded
okra collections extract <name> --schema ./schema.json --cite --json # same schema across a corpus (fan-out), grounded
```

- `--schema` is a JSON Schema (file path or inline). `result.data` is the parsed object. For collection fan-out, each result row carries `doc_id` + `data` (the parsed object — not a string to re-parse).
- `--cite` is opt-in grounding. It adds `result.citations`: one entry per grounded field, Anthropic-shaped — `{ type: "page_location", field, cited_text, start_page_number, end_page_number, bbox, bbox_source, citation_url, match }`. Use `citation_url` to show the source region and `match` (`exact`/`fuzzy`) as confidence.
- `field` is a **JSONPath into `result.data`** — nested object and array leaves are grounded too, so an invoice schema's `vendor.name`, `line_items[0].amount`, `line_items[1].amount` each get their own citation. Map a citation back to its value by that path.
- Fields that could not be grounded are **dropped** from `citations` (never a fabricated citation). So a missing field citation means the value was not found in the document — treat it as low-confidence and verify before relying on it.
- Prefer `--cite` whenever extracted values are load-bearing; an ungrounded number reads as a guess.
- `--cite` works on **both** single-doc `extract` and the corpus fan-out `collections extract`. On fan-out, each result row's `data` carries its own `citations`, and each citation adds `document_index` + `document_title` so you know which document it came from. Each fan-out row also carries a `status`; a `status: "failed"` row means that document did not extract (not that a field is absent).
- **One citation shape everywhere.** `extract --cite`, `context get`, `context ask`, and `collections extract --cite` all return the identical Anthropic `page_location` object — `{ type: "page_location", cited_text, start_page_number, end_page_number, citation_url, match, … }` (plus `bbox`/`bbox_source` when a precise element box exists, `document_index`/`document_title` on fan-out). Parse citations the same way on every surface; a Claude-aware agent reads them natively.

## Content Types (typed extraction without hand-wiring a schema)

For common document kinds, a **content type** bundles the schema + evidence policy so you don't author JSON Schema or remember `--cite`. Discover them, then extract by name:

```bash
okra content-types list --json                 # registered content types (id, label, fields, …)
okra content-types show invoice --json          # the full manifest (schema, fields, evidence policy)
okra extract <doc_id> --content-type invoice --json   # typed extraction with the invoice schema
okra invoice extract <doc-or-file> --json       # first-class noun — identical to `extract --content-type invoice`
```

- `--content-type <id>` supplies the manifest's schema **and forces grounding** when the manifest requires it — so `result.citations` come back **without** passing `--cite`. Prefer this over a hand-written `--schema` whenever a content type fits the document.
- Each content type also gets a **first-class noun**: `okra <id> extract <source>` (e.g. `okra invoice extract ./invoice.pdf`, alias `okra inv extract`) is exactly `okra extract <source> --content-type <id>` — same `command: "extract"` envelope, same citations, same `next_actions`. Use whichever reads cleaner.
- Pass **either** `--content-type` **or** `--schema`, not both (a content type already supplies its schema) → otherwise `error: "invalid_request"`.
- An unknown id returns `error: "not_found"` with a `next_actions` pointing at `okra content-types list`. Run that to see what's available before guessing.
- `result.data` and `result.citations` have the same shape as a plain `extract --cite`; everything in "Structured Extraction" above still applies (JSONPath `field`s, dropped-when-ungrounded, one citation shape).

## Chaining Discipline

Default chain:

```bash
okra context resolve "<url-or-title>" --json
okra context structure "<source_id>" --json
okra context get "<specific extraction target>" --source-id "<source_id>" --json
okra context ask "<specific question>" --source-id "<source_id>" --json
```

Use the IDs from the previous envelope:

1. Run `resolve` first for a URL, SHA-256, or title hint. Capture `result.source_id`.
2. Run `structure` for that `source_id`. Use `result.outline[*].id`, page ranges, `result.tables`, and `result.figures` to choose a small target.
3. Run `get` for evidence blocks when you need source text, table summaries, or nearby snippets. Pass `--section <id>` when structure identified a precise section.
4. Run `ask` when you need a cited answer. Keep the question narrow.
5. Run `open` when you need the original-source URL for a cited page or section.
6. If an envelope includes useful `next_actions`, prefer those commands for the next step.

Do not jump straight to broad extraction if a resolve -> structure -> get/ask path can narrow the work.

## Verification Loop

After every extraction or answer:

- Check `ok`, `command`, `cost`, `citations`, and `next_actions`.
- Check the quality fields that actually exist for that command:
  - `context ask` → `result.confidence` (0–1) and `result.follow_up_sections`.
  - `context get` → `result.context_blocks` (empty = nothing returned) and `result.omitted_reason`. Two distinct empty cases: `"not_found"` = the query matched nothing (rephrase or widen), vs `"max_tokens_too_small"` = a block **did** match but exceeded your token budget — re-run with `--max-tokens` ≥ `result.min_tokens_needed` (don't conclude the content is absent).
  - `extract` → `result.data` plus, with `--cite`, per-field `result.citations`. **Extract has no standalone confidence score — the citation coverage IS the confidence**: a field present in `citations` is grounded; a field missing from `citations` was not found in the document → low-confidence, verify it.
- If citations are missing, confidence is low, or the answer is too broad, run `context structure` again and then `context get` with tighter `--section` or `--max-tokens`.
- Use `context open --page <n>` or `context open --section <id>` when you need to inspect or hand off the original source location.
- **Dispatch a precise check with `okra documents verify <doc> "<claim>" --page <n> --json`** when a value is load-bearing and citation presence isn't enough. It runs a vision model against the page image and returns `{ verdict: "supported" | "contradicted" | "not_visible", evidence_snippet, page_image_url, confidence }` (Anthropic-style page-scoped verdict). Citations prove a region was *retrieved*; verify proves the claim is actually *true on the page* — so it catches figures, charts, and table cells the text parse may have missed. Pass `--bbox '{"x":..,"y":..,"w":..,"h":..}'` to focus a region. A `contradicted`/`not_visible` verdict comes back with a `next_actions` pointer to `context get` to pull the cited region. This is the strongest single-claim verification step — prefer it over manually downloading `citation_url` images.
- Finish the loop only after the answer or extracted fields are backed by citations or a `supported` verdict, or clearly state the limitation and the exact follow-up command that would resolve it.
