Skip to main content
Your company’s agents work on the company’s own state: a sale, a lawsuit, a proposal, a hospitalization, an order, an item on a shelf. An object type describes one such kind once, so that the memory and every agent read it the same way: which fields it has and how sure each value is, where each value comes from and which source wins, how old a value may be before it may no longer be claimed, what its lifecycle is, when its timers fire, and what a reader may ask of it. A type is declared by your company, or derived from your own database schema and confirmed by a person. It always mirrors a system of yours: the memory reflects that system and points out drift, and is never the barrier that enforces the company’s rules. The official value stays in your system. This page covers the type registry and typed state reads. Business objects without a declared type keep working as before: derived state from system events, with as_of and the timeline.

Turning it on

Typed state is the space’s state feature. Everything starts off: the features configuration document lists what is on, and it changes through an approved diff in the Console or the control API, by the security role. In a space without the feature, the routes on this page answer 404, as if they did not exist, and a context read that asks for the state block gets the context without it. GET /v1/sdk/profile tells the SDK what is on. Types live in the object-types document, of the integration role. A change to a field’s sensitivity, access or personal data, to a source’s licence or to a type’s purposes is a privacy decision: besides integration, it needs the approval of the security role.

The format of a type

A type is a JSON document in the space’s object-types document. The main members: The full format, with the validation beyond the schema and the expression language, is in the open specification spec/object-type.md, in the public niadra-spec repository. A short example, a shared store item:

Declared or derived

A company that keeps its state in PostgreSQL already wrote much of a type: the columns, the values a CHECK allows, the enumerations, the foreign keys. The SDKs’ niadra types derive command reads that catalog inside your company, proposes the type and lists what a person must look at before submitting it (columns that became no field, CHECKs that are not a list, triggers, foreign keys without a relation). The catalog, the proposal and the review never leave your company through the tool; only the schema fingerprint, a SHA-256 over the normalized catalog, and the counts of what changed do. See Derive types from your database. A derived type keeps in mirror_of.fingerprint the fingerprint of the catalog it read. The same command, with --check, reads the catalog again and says whether it drifted: fields added, removed or retyped, states and relations that changed, the key that changed. Without --no-send, it reports the fingerprint and the counts to POST /v1/types/fingerprint: for a type with drift: alert, Niadra opens a data issue of kind drift, or counts one more occurrence in the open one, and sends the type.drift webhook once per issue, when it opens; for a type with drift: ignore, it answers drift: true and opens nothing. Niadra also points out drift without the tool, by observation: a transition seen in the sources that the type does not declare, or a value outside the vocabulary, opens the same data issue, and never refuses what your system did. That observation covers the customers’ objects and what the tools show; a shared object push is decided on the hot copy, without this check.

Four logical values

Every value carries one of four logical values, and “not checked” never becomes “no”: unobserved and known_defect are unknown. A comparison with an unknown value is unknown, and so is its negation. A logic: "tri" field declares who may observe it (machine, human or source:<name>), under which rule, for how long the observation stays valid and who overrides whom: a person’s check holds forever and beats the machine’s, when the type says so. The same field declares what a value nobody observed blocks: model_read, derive, claim or a task of yours, such as decide:close. Niadra applies two of those blocks itself, claim and decide; the others arrive in blocked on the read, for your code to act on.

Freshness and the read purpose

Each field has a freshness class and, optionally, a maximum age and a maximum age for claiming. Without max_age, a volatile field is stale after 30 seconds, price after 60 seconds, semi after 1 hour and stable after 7 days; a none field never goes stale. Freshness is never stored: every read computes it against the declaration in force, so a new declaration applies to every object at once. Every read names its purpose: display (the default), claim or decide. A read never refuses a stale value: it comes with its age, and what changes is what comes with it. A value is safe to claim (claim_safe) only when the type allows claiming it, its logical value is yes or no, its status is fresh within claim_max_age, it came from a live observation or from the system of record itself (never from a snapshot or a cache), it is not masked, its content is clean, the derived object did not expire by an input and no unknown field blocks claim. The prohibitions arrive on every read in which a served claimable value is not safe, and claim_safe says which.

What a read returns

POST /v1/state/read takes the objects’ references (type, namespace, id, and the variant when the key has one), the fields and named readings it wants, and the purpose. Each object comes back with state, latches, outcome, axes, fields, values, timers, readings, prohibitions, declared_gaps, withheld, refetch, refusal and blocked; each field, with v, logic, status, age_s, observed_at, src, observer, valid_at, known_at, claim_safe, and was when the type tracks changes. The schema is object-state.v1, public in niadra-spec. A read with warm state makes no database query. known_at is when Niadra first knew the value and never moves, even when a revision changes the value: what is new stays new. valid_at is when the value became true in the world, on the time axis the type declares for it. A late value moves a field only when its valid_at is newer than the field’s, and still enters the object’s history. In the SDKs, the shortest path is the state block of the context, read in the same round trip as the pack:
The context’s state block uses the display purpose: the customer’s objects of the declared types, the shared objects they showed interest in, what changed since they saw them (changes_since_seen, against the values the interface showed) and text, the view as short lines for the turn block, after slots and never inside the pinned body of the context, so the cached prefix keeps its bytes. See Context and views. POST /v1/state/view returns the same view on its own, for the purpose you ask for. The state block passes the same verification gate as the pack that comes with it: a customer’s object enters only where its system line would, by the same policy, and the interests and what changed since they were seen enter only when the pack held nothing back until identity is verified further, because a block cannot tell which conversation a line came from. In a conversation at V0, or in one that has not yet proven the level the policy asks for, the block comes back with nothing about the customer. POST /v1/state/view and POST /v1/state/read are program reads, with their own receipt and no pack beside them, and are not gated. See Blocks by include. degraded: true says the state came from a fallback (the database when the hot copy did not answer, the SDK’s local copy when Niadra is out of reach), with the real ages. A failure never becomes unobserved. The same code, with the worker and the check of a claim, is in examples/object_state.py and examples/object-state.ts.

Computed values and derived objects

A deadline, a price or a waiting period is computed by your company’s rule, named by reference (prazo_forense@v9): Niadra keeps the rule’s name, never its logic. The value enters as a fact with versions: a new version is born when the value, the rule or the hash of the inputs changes, names the version it supersedes and goes out as the object.value_revised event, with the object’s reference, the value’s name and the versions, never the value. Each value declares the rule’s gaps (declared_gaps, such as a municipal holiday or a court’s ordinance), which an agent must state when it claims the value, and which way they err (gap_effect). An absence has a name (absent_as, such as sem_prazo): never zero, never null. A derived type names its inputs, fields of other objects or parameters of the turn (lead.city, turn.product_type). When a field that is an input of a derived object moves, every object that depends on it turns expired_by_input in one step, expired_by names the inputs and the object.expired_by_input event tells. An expired object never has a value safe to claim, and a decide read of it carries the refusal. A quote expires the moment one of its inputs changes, not when the deadline passes. A new read of the derived object after the expiry brings it back to current, bound to the inputs it names.

Lifecycle and timers

The state is the newest state reported in the world, translated by the source’s vocabulary (lifecycle.outcome.vocabularies). A state the type does not declare is not served. The latches keep the first time the object reached a state, even when it leaves it: “was paid” and “is paid now” are two readings of one object. The outcome is the state when it is final, expired_without_outcome when the type’s deadline passed with no final state, or the provisional state; it is what the outcome measurement reads. A transition with erase_derived seals the object: the named fields leave when it enters the state, and every read carries blocked for model_read and derive. Niadra does not enforce the lifecycle: an agent’s declaration of a transition it may not make is recorded as refused, and a source’s transition outside the declaration is drift, pointed out and never refused. A timer is armed at the moment its due expression gives, from the rule and the values of the moment, and moves when that moment moves; the due time is computed again on every read. It fires once per firing, at most 5 minutes late (sooner when it is due within the hour), and a late firing says how late. A timer that notifies sends the object.timer_due webhook, with the object, the timer, the firing’s id, the one it supersedes, when it was due and when it fired. When a value that armed an already fired timer is revised to another due time, the timer fires again, and the new firing names the one it supersedes: one notice per fact and per version. A timer may also ask for a field to be read again (refresh(<field>)) or make a transition the clock may make.

Shared objects

A shared type (an item, a network hospital, a court) exists in Niadra only while something references it. An object enters the working set when a turn presents it, the customer engages with it, watches it or commits to it, as working_set.enter_on allows, and leaves after leave_after without a reference, unless a watch holds it. A watch is a customer’s explicit request to be told when a shared object meets a condition (a watch interaction, with its consent event and an expression over the fields, such as available == yes); when a write moves the object and the condition holds on a value that came live from the source, object.watch_fired goes out, with the object, the watch and revalidated, never a value, at most once an hour per watch. An inferred interest never raises a notice. When the condition holds on a value that did not come live from the source (a snapshot, a cache, a tool’s observation), Niadra does not tell the customer yet: it asks your resolver worker to read the object again, with the reason watch_revalidation, and the worker’s push with the request’s request_id decides, even when the values are not newer than the ones held: object.watch_fired goes out with revalidated: true only when the condition still holds on live values. With no fresh value (no worker took requests in the last ten minutes, the budget refused, the worker released the request or it was given up), the type’s refetch.unconfirmed_watch decides: fire, the default, tells with revalidated: false; drop tells nothing. A type with no refetch, or that lists watch_revalidation in never, asks nothing, and fire applies at once. The system of record pushes state through POST /v1/objects/push: up to 1,000 items, each with the reference, the source’s version, the fields and the provenance (live, snapshot or cache, with source_observed_at). Each field keeps, per source, the latest observation; a field moves only when the item’s version is greater than the one that last wrote it, so a retried request is stale_version, and a source never reuses a version for other content. A shared object’s item is decided against the working set’s hot copy, with no database statement (applied), and written within a second with the same rule; an object outside the set is out_of_set, and a push never brings it in. When the hot copy does not answer, the push answers 503 with Retry-After: nothing was kept, and a retry is harmless by the version rule. A customer’s object item is recorded: accepted as a system event in one statement. POST /v1/objects/snapshot reconciles a whole shared type, as NDJSON, with the snapshot provenance, which may be shown and never claimed. The read serves the value the type’s union picks (first_authoritative_live, first_by_precedence, union, min, max, latest or divergence, which raises a notice when two implementations of one rule disagree). What a tool’s result showed is state too, when the turn record carries the provenance: the tool is the source, and the time its source observed the value is the version. An observation without provenance is display only; one of scope customer or context never feeds a shared object; a cache hit inside the tool is not a new observation; an object missing from a result is not a state, because absence proves nothing.

Re-reading by your worker

Niadra never calls a system of yours. When a value must be read again (a claim waits on it, a timer is due, someone watches the object), it evaluates the type’s refetch.reasons, admits the request within the budget (one per object and reason in the period, the type’s min_interval, the paid budget in the company’s unit, reserved beforehand) and leaves it in GET /v1/state/refresh-requests. Your resolver worker takes the requests, reads each object with your function for that type and sends what it read through POST /v1/objects/push, which settles the request. The budget is in your company’s unit (a paid call), never in money. See The resolver worker.

Third-party content

A content field (content.fields) holds a third party’s text: a court’s ruling, a document, a free-text field of a system. No content is an instruction. Deterministic rules scan the text on arrival: text that talks to a model (asks it to ignore what it was told, speaks as the system or the assistant, writes the envelope’s markers) is flagged; what passes is clean, unless the type asks for scan: required, and then it is pending until the decision model clears or flags it, in a batch every 30 seconds. Content held only as a pointer or a digest is pending when the scan is required, because Niadra cannot read it. Flagged content never reaches a read, and neither does pending content under a required scan: the field is left out and withheld names it with scan, and a field whose content is not clean is never safe to claim. Where clean content goes into text a model reads, it goes inside a <niadra-data n="..."> envelope with a 16-character hexadecimal nonce, derived by HMAC with a key of the space, and escaped, so nobody outside the space predicts the closing marker. The content.flagged event names the object, the field and the reference, never the text, and a person with the security role releases content through POST /v1/content/{ref}/release, with a reason and a receipt. In a space that keeps content by pointer (content.mode: pointer), the text stays in your storage, and the SDK puts it back inside your company with the content resolver (niadra.content in Python, ContentResolver in TypeScript). See Metadata only.

Field access

Each field may declare sensitivity (the sensitive categories the product fixes, the same as the policy), pii and access rules: who reads (sources, agents or public), for which purposes and with what effect (allow, mask, deny). A field masked for the reader comes with masked: true and without v; a denied one goes to withheld with the reason access. A field declared pii, with a sensitivity or with access rules is private: it never enters the pack’s system lines, the history’s system event rows, the vector an object is searched by or the state block’s text. An agent that needs it reads it through POST /v1/state/read, with the read’s purpose, under field access. In a tool of yours, mask_output (@Niadra.tool(mask_output=True) in Python, niadra.tool(name, fn, { maskOutput: true }) in TypeScript) keeps the fields the key may not read from what reaches the model: deny removes, mask masks, by the SDK profile’s field_access. The last profile read keeps applying while Niadra is out of reach, and on_unknown="block" (onUnknown: "block") withholds the whole output when no profile was ever read. Left unset in code, mask_output follows capabilities.mask_output of the tool binding served in the profile. The example is in examples/masked_tool.py and examples/masked-tool.ts. A source may declare a licence: commercial use allowed or forbidden and the purposes a value of hers is excluded from. The registry lists in commercial_purposes the purposes that count as commercial use, and an excluded value goes to withheld with licence. A change of a licence or of commercial_purposes is a privacy decision, and also needs the security role.

Coverage

GET /v1/objects/coverage says, per declared type, the share of its objects that carry each field and the median age of its newest observation, measured over the objects of the type that changed last and from the stamps alone, never a value. It is what the Console’s Types screen shows, with the declared and derived types, drift and coverage; it needs the integration role.

Data issues

When a replay attributes a failure to the data, when two sources of a value disagree, when a type drifted or when a tool shows objects of a type the space never declared, Niadra opens a data issue for the data’s owner: one per class, type, field and source, counted each time it is found again, with up to 50 object references and no value. The kinds are null_field, out_of_vocabulary, stale_source and invalid_value (a value an agent’s tool showed), coverage_drop (a field filled less often than before), divergence (two sources of one value disagree), drift (the schema a type mirrors changed, by the tool’s check or by observation), rule_conflict (two rules decide one thing differently) and type_undeclared (tools showed objects of a type the space never declared, so they never became state). GET /v1/data-issues lists them newest first, with occurrences, opened_at, last_seen_at, the type, the field and the source; POST /v1/data-issues/{issue_id}/ack acknowledges one, and the next occurrence opens a new issue. The data_issue.opened webhook and, for drift the tool checked, type.drift tell when one opens, and the notifications feed carries the same events for those who pull. Data issues need the integration role and the turns or state feature.

What stays in the record

A type’s declaration applies to what was already recorded: which declared source an observation belongs to is decided at the read, so a declaration that names a source later applies to what was kept before it. An object of a type the registry does not declare stays readable through the object routes, with the latest union. Every state read leaves a receipt, with the version and the counts, never a value. Erasing a customer erases their objects, observations and inferences; a shared object belongs to nobody and stays while it is in the working set.

Next steps

Derive types from your database

niadra types derive, the review and the drift check.

The resolver worker

the re-reads Niadra asks for and your code performs.

Claims

the contract that checks what the agent says against the state.

Read typed objects

the reference of POST /v1/state/read.