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

# POST /substrate/v1/verify — evaluate a state expression

> Evaluate a JS-style predicate against a tracked process's live state, polling until the condition passes or timeout elapses — no action is dispatched.

The `/verify` endpoint lets you evaluate a state expression against a process without dispatching an action. The daemon reads the current state, resolves the expression against the `objects` map, and returns whether it evaluated to `true`. If the expression is not yet satisfied, the daemon polls the process at its normal probe interval until the expression passes or `timeout_ms` elapses. Use this endpoint as a pre-flight check before high-stakes actions, as a post-action confirmation step in custom agent loops, or anywhere you need to wait for the application to reach a particular state.

## Endpoint

```http theme={null}
POST /substrate/v1/verify
X-Substrate-Key: <key>
Content-Type: application/json
```

**Rate limit:** 100 requests per second.

## Request Fields

<ParamField body="pid" type="integer">
  OS process ID of the target application. Provide either `pid` or `app_id`.
</ParamField>

<ParamField body="app_id" type="string">
  Stable app identifier from `GET /apps`. Provide either `app_id` or `pid`.
</ParamField>

<ParamField body="expression" type="string" required>
  A JavaScript-style predicate evaluated against the `objects` map of the current `SubstrateState`. The expression has access to all dot-notation keys as if they were top-level JavaScript variables, and to the `context` object for URL-based checks. See the [Supported Expressions](#supported-expressions) section below for syntax and examples.
</ParamField>

<ParamField body="timeout_ms" type="integer" default="5000">
  How long in milliseconds to poll before returning a result, even if the expression has not yet become `true`. Accepted range: 100–30000 ms. If the expression is already `true` on the first evaluation, the daemon returns immediately without waiting for the full timeout.
</ParamField>

## Response Fields

<ResponseField name="passed" type="boolean" required>
  `true` if the expression evaluated to a truthy value before `timeout_ms` elapsed. `false` if the timeout was reached without the expression becoming true.
</ResponseField>

<ResponseField name="expression" type="string" required>
  The expression string that was evaluated, echoed back for confirmation.
</ResponseField>

<ResponseField name="computed_value" type="any" required>
  The final resolved value of the expression — often a boolean, but can be a number or string when you use comparison operators. Useful for diagnosing why an expression failed to pass.
</ResponseField>

<ResponseField name="explanation" type="string" required>
  Human-readable summary of the evaluation result, e.g. `"Expression evaluated to true after 1 poll cycle"` or `"Timed out after 5000ms — inbox.unread_count was 3, expected 0"`.
</ResponseField>

<ResponseField name="latency_ms" type="integer" required>
  Total time in milliseconds from request receipt to response, including all polling cycles.
</ResponseField>

<ResponseField name="poll_cycles" type="integer" required>
  Number of probe cycles the daemon ran during this verification. A value of `1` means the expression passed on the first read; higher values indicate the daemon had to wait for state to settle.
</ResponseField>

<ResponseField name="changed_fields" type="array" required>
  Array of `{ path, before, after }` objects listing every field that changed value during the verification window. Empty when `poll_cycles` is `1` (the expression was already true).
</ResponseField>

<ResponseField name="actual_value" type="any">
  The value of the primary operand in the expression at the time of final evaluation. For example, for `"inbox.unread_count < 10"` this would be the actual value of `inbox.unread_count`. Provided when the daemon can infer a single key from the expression.
</ResponseField>

<ResponseField name="evaluated_at" type="datetime" required>
  ISO 8601 timestamp of the final evaluation.
</ResponseField>

## Supported Expressions

Expressions are evaluated against the `objects` map as if each key were a top-level variable. The `context` object is also in scope for URL and view checks.

```javascript theme={null}
// Equality check
"inbox.emails[0].is_read == true"

// Numeric comparison
"inbox.unread_count < 10"
"flight.altitude_m > 45.0"

// Logical AND / OR
"flight.is_armed == true && flight.mode == 'GUIDED'"
"inbox.unread_count == 0 || inbox.emails[0].is_read == true"

// Negation / inequality
"inbox.emails[0].id != 'msg_18821af3'"

// Existence / truthiness
"inbox.unread_count > 0"

// String method (JavaScript semantics)
"context.url.includes('inbox')"
"context.view == 'gmail_compose'"
```

<Note>
  Expressions use JavaScript evaluation semantics. Loose equality (`==`) is supported but strict equality (`===`) is recommended to avoid type coercion surprises with numeric string values.
</Note>

## Use Cases

**Wait for an action to take effect** — After `POST /action` returns, call `POST /verify` with the expected post-condition to confirm the application state has settled before moving to the next step in your agent loop.

**Pre-flight check before a destructive action** — Verify `"inbox.emails[0].id == 'msg_expected'"` before calling `send_reply` to ensure the correct thread is selected.

**Poll until a background job completes** — Set `timeout_ms` to 30000 and use an expression like `"export.status == 'complete'"` to block your agent until the application finishes a long-running operation.

## Examples

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST http://127.0.0.1:7842/substrate/v1/verify \
    -H "X-Substrate-Key: sk_nsp_..." \
    -H "Content-Type: application/json" \
    -d '{
      "pid": 12847,
      "expression": "inbox.unread_count > 0",
      "timeout_ms": 3000
    }'
  ```

  ```python Python SDK theme={null}
  from nelieo import NspClient

  client = NspClient(api_key="sk_nsp_...", base_url="http://127.0.0.1:7842")
  session = await client.attach("Gmail")

  # Pre-flight: confirm the expected email is in the inbox.
  check = await session.verify(
      "inbox.emails[0].id == 'msg_18821af3'",
      timeout_ms=2000,
  )
  if not check.passed:
      raise RuntimeError(f"Pre-flight failed: {check.explanation}")

  # Execute the action.
  result = await session.execute(
      "send_reply",
      parameters={"thread_id": "thread_7af2b3", "body": "On it!"},
      verify_expression="inbox.sent_count > 0",
  )

  # Standalone post-action poll until sent count increases.
  outcome = await session.verify("inbox.sent_count > 14", timeout_ms=5000)
  print(outcome.passed)         # True
  print(outcome.poll_cycles)    # 2
  print(outcome.latency_ms)     # 284
  ```
</CodeGroup>

### Example Response

```json theme={null}
{
  "passed": true,
  "expression": "inbox.unread_count > 0",
  "computed_value": true,
  "actual_value": 47,
  "explanation": "Expression evaluated to true after 1 poll cycle",
  "latency_ms": 52,
  "poll_cycles": 1,
  "changed_fields": [],
  "evaluated_at": "2026-07-20T13:00:01.452Z"
}
```

## Notes

* A `passed: false` response uses HTTP `200 OK` — the endpoint itself succeeded. Only use HTTP status codes to detect transport or authentication errors.
* When `timeout_ms` is large (e.g., 30000), the request will block for up to that long. Ensure your HTTP client's read timeout is set higher than `timeout_ms` to avoid client-side disconnects.
* The daemon triggers a fresh probe cycle on every poll iteration. Each cycle adds 50–200 ms depending on runtime and application size.
