---
name: blatent-struct
description: Build and troubleshoot integrations with Struct, Blatent's model for constrained JSON generation. Use for schema design, exact source quotes, state-based decisions, control flow, probabilities, registry workflows and OPA policies through the Blatent API.
metadata:
  docs_snapshot: "2026-10-27"
---

# Integrate with Struct

Blatent is the company; Struct is the model. Struct interprets input and generates
a JSON value for software to use. Input can be plain text, formatted text such as
XML or YAML, or any JSON value. The HTTP request itself is JSON.

Use this skill to choose constraints, build requests and test an integration.
Use existing user authorization for live tests and registry changes within the
task. The skill does not authorize unrelated API calls, moving an application's
alias, or executing a business action just because an answer recommends it.

The [API reference](https://blatent.ai/docs/reference/resolve) and
[OpenAPI download](https://blatent.ai/docs/downloads/openapi.json) are the contract
for the public interface. Read the relevant linked guide for a complete advanced
example; no SDK or adjacent runtime checkout is required.

## Choose the right mechanism

| Part of a request | Role |
| --- | --- |
| `input` | Material to interpret or the task to perform |
| `schema` | JSON Schema defining the generated value; descriptions explain its meaning |
| `state` | Optional known facts supplied by the application for this request |
| `policy` | Optional OPA rules, written in Rego, for relationships the answer must satisfy |

Descriptions guide interpretation. Supported schema constraints and attached
policies enforce their rules. Schema validity, policy compliance and semantic
accuracy are separate: a consistent total can still contain incorrectly extracted
amounts, and an exact quotation can still answer the wrong question.

| Need | Mechanism and guide |
| --- | --- |
| Types, allowed labels, object fields and arrays | [Define outputs](https://blatent.ai/docs/guides/define-output) |
| Meaning, evidence, units and missing-information behavior | [Write descriptions](https://blatent.ai/docs/guides/descriptions) |
| Lengths and regular expressions | [Constrain strings](https://blatent.ai/docs/guides/string-formats) |
| Normalized dates and other supported formats | [Standard formats](https://blatent.ai/docs/guides/standard-formats) |
| An exact excerpt from input | [Source quoting](https://blatent.ai/docs/guides/quote-source-text) |
| Alternatives, intersections and exclusions | [Combine constraints](https://blatent.ai/docs/guides/composition) |
| Reuse parts of a schema locally | [Schema definitions](https://blatent.ai/docs/guides/reuse-definitions) |
| Rules between generated fields | [Conditional outputs](https://blatent.ai/docs/guides/conditions) or [switches](https://blatent.ai/docs/guides/switches) |
| Choices restricted by supplied facts | [State conditions](https://blatent.ai/docs/guides/state-conditions) |
| One decision structure for each supplied item | [For-each loops](https://blatent.ai/docs/guides/schema-loops) |
| Arithmetic or richer business rules | [OPA policies](https://blatent.ai/docs/guides/opa-policies) |
| Relative preferences among candidate values | [Probabilities](https://blatent.ai/docs/guides/probabilities) |
| Compare candidates under interchangeable IDs | [Label permutations](https://blatent.ai/docs/guides/label-permutations) |

## Make a request

Use `https://api.blatent.ai` with `Authorization: Bearer <Blatent API key>`.
The key comes from the user's Blatent console and belongs in a backend environment
variable or secret manager. The examples read `BLATENT_API_KEY`.

Send `POST /v1/resolve` with exactly one non-null `schema` or `schema_id`. An inline
schema is an object or Boolean schema. A registered reference is tenant-owned.
Omit `model` to use the deployed default. If an explicit ID is needed, read it from
`GET /v1/models`; do not copy a version from an old example.

| Optional field | Contract |
| --- | --- |
| `input` | Text or any JSON value |
| `state` | Object containing this request's application snapshot |
| `parameters` | Object of named context values; does not substitute schema placeholders |
| `user_context` | Object with `scopes` (permission-name array) and/or `attributes` (object) |
| `policy` or `policy_id` | Inline OPA definition or registered policy reference; never both |
| `policy_max_retries` | Integer 0–128; defaults to 32 with a policy; retries after the initial attempt |
| `policy_feedback` | `off`, `chat` or `inline`; feedback guidance, separate from the retry budget |

Retry and feedback settings require a non-null policy binding. Omit unused
context. Unknown fields are rejected: this endpoint does not accept chat
`messages`, `temperature`, `max_tokens` or `response_format`. Do not invent an
SDK, streaming interface or batch endpoint.

### Starter in Python, TypeScript or cURL

This is the [first-request example](https://blatent.ai/docs/guides/first-request).
Save `triage.schema.json` beside the chosen client. Preserve the descriptions
when loading or registering it.

```json
{
  "type": "object",
  "properties": {
    "department": {
      "type": "string",
      "enum": ["billing", "technical", "other"],
      "description": "Route charges, invoices and refunds to billing. Route broken product behavior to technical. Use other for anything else."
    },
    "duplicate": {
      "type": "boolean",
      "description": "True only when the message explicitly reports being charged more than once for the same purchase."
    }
  },
  "required": ["department", "duplicate"],
  "additionalProperties": false
}
```

Python requires Python 3.10+ and `httpx>=0.28,<1`. Run `python classify.py`:

```python
import json
import os
from pathlib import Path
import httpx

request = {
    "input": "The invoice was charged twice. Can you help me get the duplicate charge refunded?",
    "schema": json.loads(Path("triage.schema.json").read_text(encoding="utf-8")),
}
with httpx.Client(
    base_url="https://api.blatent.ai",
    headers={"Authorization": f"Bearer {os.environ['BLATENT_API_KEY']}"},
    timeout=195,
) as client:
    response = client.post("/v1/resolve", json=request)
    response.raise_for_status()
    print(json.dumps(response.json()["output"]))
```

TypeScript uses Node.js 24+ and built-in `fetch`. Run `node classify.ts`:

```typescript
import { readFileSync } from "node:fs";

const request = {
    "input": "The invoice was charged twice. Can you help me get the duplicate charge refunded?",
    "schema": JSON.parse(readFileSync("triage.schema.json", "utf8")),
};
const response = await fetch(
  "https://api.blatent.ai" + "/v1/resolve",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BLATENT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(request),
    signal: AbortSignal.timeout(195_000),
  },
);
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(JSON.stringify((await response.json()).output));
```

cURL requires `jq`. Run `bash classify.sh`:

```bash
set -eu

body=$(jq -n --argjson body '{"input":"The invoice was charged twice. Can you help me get the duplicate charge refunded?"}' \
  --slurpfile f0 'triage.schema.json' \
  '$body | setpath(["schema"]; $f0[0])')
curl --fail-with-body --silent --show-error \
  "https://api.blatent.ai/v1/resolve" \
  -H "Authorization: Bearer $BLATENT_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary "$body"
```

Python and TypeScript print the generated `output`; cURL prints the full response.
The example's intended output is `{"department":"billing","duplicate":true}`.
This is an example of interpretation, not a guarantee for every future call.

### Read the response

Check the status before using `output`. The supplied schema governs that value,
not the surrounding response. Do not turn an error or missing `output` into an
invented successful answer.

| Response field | Meaning |
| --- | --- |
| `output` | Generated value, with the type defined by the schema |
| `model` | Public model identity |
| `schema_id` | Supplied registered reference, or null for inline schemas |
| `schema_digest` | Schema content identity |
| `state` | Supplied snapshot or null; not a state update |
| `state_version` | Null for the public inline-state interface |
| `policy` | On policy-bound success: `policy_id` (null for inline), `digest` and `status: "allow"` |
| `probabilities` | Requested candidate metadata, keyed by output JSON Pointer |
| `usage.input_tokens` | Nonnegative billable input count using o200k_base |

Allow additional response metadata. The response does not expose decision count
or price. `usage.input_tokens` is a billing count, not the model's internal token
count. Keep the `X-Blatent-Request-ID` header with results and errors.

## Design output schemas

Use explicit types, declared properties, `required` and useful array/string
bounds. Missing, null and empty are different values. Make nullability explicit;
an optional field is not automatically nullable. `additionalProperties: false`
closes only the object where it appears.

Describe category boundaries, evidence, units and absence behavior. Preserve
user-authored descriptions unless asked to change them. If review or uncertainty
is a legitimate result, allow it in the schema and explain when to use it.
Struct does not automatically add a fallback or weaken constraints after failure.

JSON Schema 2020-12 generation support has defined boundaries and compilation
limits. Consult [schema support](https://blatent.ai/docs/reference/schema-support)
before using less common combinations:

- Finite enums, constants, booleans, declared objects and bounded arrays are
  supported. Local nonrecursive `$defs` / `$ref` reuse definitions; recursive,
  dynamic or external references and dynamic property synthesis are unsupported.
- One-sided integer `minimum`, `maximum`, `exclusiveMinimum` and
  `exclusiveMaximum` are supported. Bounded integer ranges must fit compiler
  limits. Open fractional values do not imply support for arbitrary bounds or
  `multipleOf`; use an appropriate finite domain or OPA relationship.
- String lengths count decoded Unicode characters, with bounds up to 4,096.
  Patterns search by default; outer `^` and `$` anchor the whole string. Use the
  supported regular subset, not backreferences, lookarounds or inline flags.
- Supported generation formats are `date`, `time`, `date-time`, `duration`,
  `uuid`, `ipv4`, `json-pointer` and `relative-json-pointer`. RFC 3339 time and
  date-time include a timezone. Format validity does not prove correct extraction.
- Root `x-runtime.formatAssertions` defaults to true. Turning it off makes
  formats annotations; do so only when that is the intended contract.
- Object `x-runtime.propertyOrder` lists every declared property exactly once,
  including optional ones. It controls generation order; default order is
  lexicographic. It neither makes optional fields required nor changes how a
  consumer should access JSON keys.

Pydantic and Zod may parse output into local types. Their validators/refinements
are not automatically transmitted as server constraints. Retain JSON Schema as
the source of truth for features those libraries cannot express losslessly.

## Distinguish standard constraints from Struct extensions

`if`, `then`, `else`, composition and `$ref` are JSON Schema constructs.
Eligibility, switches, loops, quoting, probabilities and generation ordering use
Struct's `x-runtime` annotations. A standard validator alone does not enforce
these extensions or prove that the service can generate from a schema.

References have different roots and roles; do not substitute one syntax for another:

| Reference | Reads |
| --- | --- |
| JSON Schema `$ref: "#/$defs/Address"` | A local schema definition |
| Eligibility `{"ref":"state/order/paid"}` | A supplied request fact |
| Switch `on.path: "/severity"` | Output under the schema containing the switch |
| For-each `ref: "state/checks"`; binding `ref: "check/id"` | Supplied items, then a named loop item |
| Quote `ref: "/input/text"` | An input string selected by JSON Pointer |
| Label-permutation `scope: "/input/responses"` | A candidate object whose keys match the enum |
| Rego `input.output`, `input.state` | The candidate answer and supplied application snapshot |

### Conditions, composition and switches

Place a rule between sibling output fields on their enclosing object. `if` tests
the output; `then` and `else` narrow the surrounding schema. A `properties` test
alone does not require presence, so add `required` where presence matters.
`dependentRequired` triggers on presence even when a value is false.

For branch-narrowed fields, start with explicit finite `enum` or `const` choices.
`allOf` intersects constraints, `anyOf` permits one or more matching alternatives,
`oneOf` requires exactly one, and `not` excludes within the supported domain.
Composition does not override conflicting rules or add undeclared properties to
a closed object. Cross-field conditions on open-ended values are not supported
by this generation profile.

`x-runtime.switch` has `on.path`, a nonempty ordered `cases` array and an explicit
`default`. Each case has `when` and a JSON Schema `then`. The first matching case
applies without fall-through. `when` uses Struct operators: for example, `eq`
takes one value to compare with `on`, unlike the two operands of state predicates.
Branches add constraints; they cannot relax the surrounding schema.

### State and permission conditions

[State](https://blatent.ai/docs/guides/inline-state) is an optional snapshot from
the application, not persisted storage or an automatic permission rule. A
description can guide its use; eligibility or OPA enforces the relationship.
Root `x-runtime.requestSchema` validates supplied context before generation.
`x-runtime.stateSchema` validates the resulting state snapshot, not initial input;
it does not turn this API into a state store.

Put eligibility on the controlled field's schema, alongside its type or enum.
`x-runtime.eligibleWhen` controls whether the field may appear;
`x-runtime.choices.<value>.eligibleWhen` controls one finite choice. Required
fields must retain a legal value. Eligibility permits a choice; it does not
force that choice when alternatives remain.

Predicates read `input`, `state`, `parameters` and `user_context`, never generated
output. `eq`/`neq` compare two typed values; `gt`/`gte`/`lt`/`lte` compare numbers.
`contains` takes its container first; `in` takes it second. `all`/`any` combine
arrays of conditions, `not` negates one, and `exists` tests presence. Missing facts
leave comparisons unknown, including under `not`, and do not grant eligibility.
Present null still counts as present; numeric strings are not coerced to numbers.

The client's backend authenticates users and supplies verified permission names.
Struct does not fetch or authenticate those permissions. The
[scope guide](https://blatent.ai/docs/guides/verified-scopes) uses
`state.oauth.scopes`; `user_context.scopes` is another documented context field.
Choose one representation and make the schema/policy read that exact location.
Never send OAuth access, refresh or ID tokens to Blatent. The service header uses
the Blatent API key; user scopes are task context, not service authentication.

### For-each loops

Select Struct's template dialect with
`"$schema":"urn:blatent:json-schema:template-draft-v1"`. Send it in the usual
`schema` field. On an array schema, `x-runtime.forEach` has a supplied-array `ref`
and an `as` name; `items` describes one result per source item, in source order.

`x-runtime.bind.const` fixes a value from the current item;
`x-runtime.bind.description` supplies that item's instructions. Binding happens
before generation. A description binding still asks the model to decide an answer.
Use `state`, `parameters` or an enclosing loop item as sources, not generated output.
Respect declared array bounds. An allowed empty list yields `[]`; a missing or
wrongly typed source is an error. Keep binding facts and instructions under the
application's control. Read the [loop example](https://blatent.ai/docs/guides/schema-loops)
before constructing a template; ordinary JSON Schema arrays do not perform binding.

## Extract exact quotes

Use `x-runtime.quote` on the string being extracted, with a JSON Pointer `ref`
to a string under `/input` and `maxLength` from 1 to 4,096. Use `/input` for text
input or a path such as `/input/text` for structured input. A description saying
“copy exactly” is insufficient to enforce source membership.

The returned string is a contiguous excerpt preserving spelling, whitespace,
punctuation and Unicode. `unit` can be `number`, `date`, `word`, `identifier`,
`passage` or `span` (the default), or an array of distinct units. Units and optional
`locale` guide selection; they do not normalize values or change their JSON type.
Use formats for a normalized date, and quotes for a date exactly as written.

Allow null explicitly when absence is valid. For multiple matches, put the quote
annotation on array `items` and set `maxItems` no greater than 256; permit `[]`
when appropriate. String constraints still apply to the excerpt and cannot rewrite
it. Source membership does not guarantee relevance, correct absence detection,
exhaustive extraction or ordering. Quote requests cannot be combined with OPA.
Use the [complete quoting example](https://blatent.ai/docs/guides/quote-source-text)
as the starting point.

## Enforce business rules with OPA

OPA (Open Policy Agent) evaluates rules written in Rego. Use it for exact totals,
date relationships or generated values compared with application facts. Attach
`policy` with `engine: "opa"`, an `entrypoint` such as `data.totals.allow`, and
`modules` mapping filenames to Rego source. The selected rule must return Boolean
true. Alternatively, send a registered `policy_id`; schema registration is independent.

Rego reads the candidate through `input.output`, the task through `input.input`,
and supplied context through `input.state`, `input.parameters` and
`input.user_context`. Omitted/null context is absent. Policy `data` is immutable
data under Rego `data`, not automatically model context. A policy's optional
`description` supplies model guidance instead of its source; the actual Rego rules
are still enforced. Use the [policy reference](https://blatent.ai/docs/reference/policies)
for module sizes, supported built-ins and feedback diagnostic limits.

Start from [the total example](https://blatent.ai/docs/guides/opa-policies) or
[state rules](https://blatent.ai/docs/guides/state-policies). Explain the intended
answer in schema descriptions as well as enforcing it in Rego. A candidate denied
by policy is not returned as a successful output.

Search is model-guided and bounded, not exhaustive or guaranteed to find a global
optimum. `policy_max_retries: 0` allows the initial attempt only. More retries
cannot fix contradictory rules or missing information. `422 POLICY_SEARCH_EXHAUSTED`
means no compliant answer was found within budget, not that no solution exists.
An explicitly allowed review value is different from an HTTP failure.

For feedback, supply a `feedback_entrypoint` returning diagnostics and select
`policy_feedback: "chat"` or `"inline"`; `off` disables feedback guidance.
Only expose information the model may see. Feedback guides another candidate and
does not grant permission. See [policy search](https://blatent.ai/docs/guides/policy-recovery)
and [worked examples](https://blatent.ai/docs/examples/overview) for complete rules.

## Probabilities and label permutations

These are independent controls. On a supported finite choice field, set
`"x-runtime":{"probabilities":true}` to request candidate metadata without label
permutations. False or omission disables it; legacy `"all"`/`"none"` are accepted.
Read `probabilities["/choice"]` for an output field named `choice`; each entry has
`value`, `probability` and `log_probability`. Probabilities sum to one over the
candidates and are relative preference, not calibrated correctness confidence.

Without permutations, probability collection preserves ordinary token-by-token
selection. A multi-token answer need not have the largest complete-candidate
probability. Evaluate any review threshold on representative inputs.

Use `label_permutations` only when arbitrary candidate IDs should be interchangeable.
Its `scope` is a JSON Pointer to an input object whose keys exactly match the
field's string enum. Keep candidate-specific meaning inside the scoped values,
not in external references such as “prefer A.” Results map back to original IDs.
The name `responses` in `/input/responses` is an example, not a reserved key.

One annotated finite string field is supported; other output fields must be fixed
once it is chosen. OPA, ordering-only permutations and multiple independently
generated fields are unsupported in this mode. Two candidates use two assignments,
three use six, and four or more use three reproducible assignments. This adds work;
it does not guarantee better accuracy. Probability metadata is optional and does
not enable or disable the permutation selection rule.

## Register and roll out definitions

| Operation | Request / successful response |
| --- | --- |
| `POST /v1/schemas` | `name`, `version`, `schema` → `201` with `schema_id`, `schema_digest` |
| `GET /v1/schemas/{reference}` | `200` with `schema_id`, `schema_digest`, original `schema` |
| `POST /v1/schema-aliases` | `name`, `alias`, `target`, optional `expected_digest` → `200` with `schema_id`, `schema_digest` |
| `POST /v1/policies` | `name`, `version`, `definition` → `201` with `policy_id`, `digest` |
| `GET /v1/policies/{reference}` | `200` with `policy_id`, `digest` |

Follow [register and reuse](https://blatent.ai/docs/guides/register-schemas) and
[registry naming rules](https://blatent.ai/docs/reference/schemas). Capture returned
IDs for later calls; do not reconstruct them. Register stable definitions once,
then resolve using `schema_id` and/or `policy_id`. Keep supplying current state.

Versions are immutable. Identical content at the same version is idempotent;
different content returns `409 VERSION_CONFLICT`. Description edits count as
content changes. Policy changes also include data and evaluator identity. The
public registry provides the operations above, not general update/delete/list
endpoints or policy aliases.

For a schema alias, the target must share its name and tenant. Inspect its current
digest and send `expected_digest` when moving it. On conflict, re-read and reconcile;
do not remove the comparison to force an update. Another API instance may retain
the previous alias target for up to 30 seconds; an in-flight request keeps its
selected version. Use immutable versions/digests when reproducibility matters and
inspect the response's `schema_digest`. See [alias rollout](https://blatent.ai/docs/reference/schema-aliases).

## Handle failures and measure operation

Errors can contain a `detail` string (authentication), a `detail` list (request
validation), or an `error` object with `code`, `message`, `path` and `request_id`.
Some failures are non-JSON. Branch on status and code, preserve the request ID,
and allow unknown codes. No `retryable` field is promised.

| Failure | Response |
| --- | --- |
| `401`, account-limit `403`, unknown-reference `404` | Correct access or the reference; no unchanged retry |
| `403 POLICY_DENIED`, invalid/unsupported request `422` | Correct the request/rules; never silently drop a constraint |
| `422 POLICY_SEARCH_EXHAUSTED` or `CONSTRAINT_SEARCH_EXHAUSTED` | Handle absence of a compliant answer; review task and budget |
| `409 VERSION_CONFLICT` | New immutable version or deliberate alias reconciliation |
| `409 OUTCOME_INDETERMINATE` | Work may have run; read registry writes back before repeating |
| `413` | Reduce request size |
| Transient `429`, `503`, `529` | Honor `Retry-After` when present; bounded backoff and jitter |
| `504 CAPACITY_BUSY` | Deadline passed; execution may have started; bound any retry |

Use the [error reference](https://blatent.ai/docs/reference/errors) and
[retry guide](https://blatent.ai/docs/guides/errors) for code-specific handling.
There is no public idempotency key. A network timeout may leave the outcome
unknown; repeating generation can repeat work and produce a different answer.
Business side effects remain the application's responsibility.

The starter uses a client timeout of at least 195 seconds to allow long requests.
HTTPX's setting bounds individual network operations, not total wall-clock time.
Use a separate overall deadline for retries. This timeout is not a latency promise.

`GET /v1/health` needs no key; `GET /v1/health/auth` checks a Bearer key. Both return
`200` with an empty body, not JSON. Neither runs inference or verifies available
credit. [Model discovery](https://blatent.ai/docs/reference/models) is separate.

Current early-access [limits](https://blatent.ai/docs/reference/limits) are shared
per account: 10 resolves/second, 8 concurrent resolves and 1,000,000 input tokens
per minute. Successful resolves debit `usage.input_tokens`; failures do not debit
that token-rate allowance. Registry calls have separate limits of 60/minute and
4 concurrent. Body limits are 8 MiB for resolve and 4 MiB for registry calls.
Read `Retry-After` rather than hard-coding waits.

[Pricing](https://blatent.ai/docs/reference/pricing) is
`$0.042 / 1M × input_tokens × max(1, 0.05 × decisions)` in USD. Input, state, schema
and policy are tokenized separately with o200k_base and added; registered
definitions are counted too. Text is counted as supplied; structured values use
compact sorted-key JSON. Each selection, numeric generation or quote selection
counts as one decision. Free-form text is summed in decoded Unicode characters
and rounded up per 20 characters. Deterministic structure needs no decisions, but
the base input-token rate still applies. Completed operations during internal
search also count; do not estimate an exact bill from final JSON or output length.

Measure full HTTP latency in the client, keeping first use, warm execution and
registration separate. `Server-Timing` values are overlapping milliseconds:
`gateway` runs to response headers, `runtime` measures the gateway call to Struct,
and optional `total`, `inference` and `queue` describe processing inside Struct.
Do not add them together; absence is not zero. They exclude the application's
network round trip. See [latency](https://blatent.ai/docs/guides/latency).

## Verify the integration

Use local checks for client behavior, schema files and allowed/denied policy
cases. Confirm that descriptions and loaded files are transmitted unchanged and
registry clients reuse returned IDs. Test failures as well as success.

When live tests are authorized, send the exact intended request and retain its
response or error. Useful controls include both output branches, missing/wrongly
typed state, absent scopes, empty loop sources and source quotes with no relevant
match. Check extension behavior at the service; local JSON Schema validation
does not enforce it. Keep failed attempts rather than presenting only successes.

Evaluate semantics separately using representative labelled inputs. In particular,
test whether an extracted quote answers the question and whether missing data is
handled correctly, not just whether the text occurs in the source. A passing
example is not benchmark evidence or a reliability guarantee.

Record schema/policy identity, input, permitted state, settings, response/error,
request ID and timing boundaries without credentials or sensitive log data.
Distinguish fixture checks, deployed contract checks and semantic evaluation in
the handoff. Follow the [evaluation guide](https://blatent.ai/docs/guides/evaluation)
for broader accuracy and latency comparisons.
