> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Journal-entry testing

> client.search.je_testing — an audit procedure that publishes its own SQL

`client.search.je_testing` runs journal-entry testing over a workspace's ledger
package and returns a `Job` whose result is a `JetResult`.

A sibling of `client.search.deep`, not a preset of it. Deep search retrieves and
cites; this performs a test: the run orients itself in the ingested ledger
tables, writes SQL over them itself, and answers with `sql_receipts` — every
statement it executed — so a figure and the query behind it travel together.

```python theme={"dark"}
from ndi_sdk import NdiClient

with NdiClient() as client:
    job = client.search.je_testing(
        workspace_id,
        query="test whether any posted journal line was approved by its own preparer",
        paths=["ledger/journal.xlsx"],
    )
    job = client.jobs.wait(job.job_id)
    result = job.result
    print(result.answer)
    for figure in result.figures:
        print(figure.criterion, figure.count, "of", figure.population)
        print("  readable out of", figure.exception_listing_result_id)
    for ground_id in result.grounding_job_ids or []:
        grounded = client.jobs.wait(ground_id, timeout=300)
        print(grounded.result)
```

***

## Method signature

```text theme={"dark"}
def je_testing(
    workspace_id: UUID | str,
    *,
    query: str,
    context: str | None = None,
    reasoning_effort: "none" | "minimal" | "low" | "medium" | "high" | None = None,
    path_prefix: str | None = None,
    paths: list[str] | None = None,
    session_id: UUID | None = None,
    wait_seconds: int = 0,
    idempotency_key: str | None = None,
) -> Job
```

### Parameters

| Parameter          | Type                                                     | Required | Description                                                                 |
| ------------------ | -------------------------------------------------------- | -------- | --------------------------------------------------------------------------- |
| `workspace_id`     | `UUID` or `str`                                          | Yes      | Workspace that already holds an ingested ledger package                     |
| `query`            | `str`                                                    | Yes      | The procedure to perform, in your own words                                 |
| `context`          | `str`                                                    | No       | Background the run should treat as given                                    |
| `reasoning_effort` | `"none"` / `"minimal"` / `"low"` / `"medium"` / `"high"` | No       | Thinking budget; omitted uses the service default                           |
| `path_prefix`      | `str`                                                    | No       | Limit the run to files under this prefix                                    |
| `paths`            | `list[str]`                                              | No       | The files of the package under test (max 64). Intersects with `path_prefix` |
| `session_id`       | `UUID`                                                   | No       | Continue an earlier run's thread                                            |
| `wait_seconds`     | `int`                                                    | No       | Hold the submission request open (max 300)                                  |
| `idempotency_key`  | `str`                                                    | No       | Override the key the SDK normally mints                                     |

### Returns

A `Job` whose `result` is a `JetResult` (`result_type` is `je_testing`).

***

## Reasoning budget

`reasoning_effort` changes how much the fixed JET engine thinks. Omit it to
use the service default, or select a larger budget for a more thorough run.
This does not change the model. JET no longer accepts `effort` or `tier`;
new JET jobs have `job.effort = null`.

There is no `top_k` (the run authors its own citation list) and no
`allow_clarification` (a run that cannot proceed says so in its answer).

## Reading the result

| Field                  | What it is                                                                                            |
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `answer`               | The prose answer. `None` only on a run that exhausted its budget first                                |
| `figures`              | One entry per criterion: `definition`, `population`, `count`, `value`, and the `result_ids` behind it |
| `sql_receipts`         | Every statement executed, with its table bindings and a bounded preview                               |
| `binding`              | The definitional layer the run committed to — period column, posted encoding, clock                   |
| `discrepancies`        | Where the data contradicted the request's premise. Empty is an answer, not an omission                |
| `population_statement` | The population every figure was computed over, in the run's own words                                 |
| `quality`              | How well a performed run went: fail-open checks, tool failures, missing artifacts                     |
| `execution_status`     | Whether the run tested what it was asked to test                                                      |
| `exhausted`            | Budget ran out before finishing — a partial answer, not a failure                                     |
| `degraded`             | The run broke before finalizing an answer; the working is published, `figures` is empty               |
| `grounding_job_ids`    | Ground jobs planned for this answer's evidences. Poll each with `jobs.wait` after this result         |

A figure's `cannot_be_performed` means the workspace holds no material for that
criterion; `result_ids` then name the probes that establish the absence.

```python theme={"dark"}
result = job.result
if result.degraded:
    raise RuntimeError(result.degraded)

for receipt in result.sql_receipts:
    print(receipt.result_id, receipt.sql)
    # rows is the bounded preview; result_row_count is the stored result's size
    print(receipt.result_row_count, "rows", "(truncated)" if receipt.truncated else "")
```

## Follow-ups

Pass an earlier result's `session_id` to ask a follow-up in the same thread. Only
the API key that started a thread may continue it, and a thread whose turn is
still running returns `409 session_busy`.

See [Search methods](/concepts/search-methods) and the
[je-testing API reference](/api-reference/je-testing).
