Skip to content

Terms and Features

Terms are the core data structures of the Reasoning Layer. A term is a typed feature structure: it belongs to a sort and carries named features with values.

A term has:

  • A sort (its type in the lattice)
  • Named features with typed values
  • An identity (UUID) assigned by the server
  • A validation state indicating whether it satisfies its sort’s type witnesses

The key insight is that rules, facts, goals, and constraints are all represented as terms — data and logic share the same structure.

Term CRUD endpoints return TermResponse, not raw TermDto. This is a critical distinction. TermResponse wraps the term with validation state:

interface TermResponse {
term: TermDto; // The underlying term data
state?: TermState; // Validation state
witnessProofs?: WitnessProofDto[]; // Proofs of satisfied witnesses
residualWitnesses?: ResidualWitnessDto[]; // Suspended witnesses
}
interface TermDto {
id: string; // Term UUID
sortId: string; // Sort (type) UUID
tenantId: string; // Owning tenant UUID
ownerId: string; // Owner user UUID
features: Record<string, ValueDto>; // Named features (tagged format)
}

Note that TermDto.features uses the tagged ValueDto format ({"type": "String", "value": "hello"}). For the untagged format used in inference, see Values and Formats.

Use plain JS values to construct feature values — the SDK auto-converts them to the tagged format:

import { Value, ReasoningLayerClient } from '@kortexya/reasoninglayer';
const client = new ReasoningLayerClient({
baseUrl: 'https://platform.ovh.reasoninglayer.ai',
tenantId: 'your-tenant-uuid',
auth: { mode: 'cookie' },
});
const response = await client.terms.createTerm({
sortId: personSortId,
ownerId: 'user-uuid',
features: {
name: 'Alice',
age: 30,
active: true,
score: 0.95,
supervisor: Value.reference(supervisorTermId),
skills: ['TypeScript', 'Rust'],
pending_review: null,
},
});
// response is TermResponse, not TermDto
console.log(response.term.id); // Term UUID
console.log(response.state); // "complete" | "residuated" | "no_witnesses"
console.log(response.term.features); // Features with tagged ValueDto values

The TermState field on TermResponse indicates whether the term satisfies its sort’s type witnesses:

State Meaning
"complete" All type witnesses are satisfied
"residuated" Some witnesses are suspended, awaiting more information
"no_witnesses" The sort has no witness requirements

When a term satisfies its type witnesses, TermResponse includes proof objects:

interface WitnessProofDto {
witnessId: string; // ID of the satisfied witness
bindings: Record<string, string>; // Variable bindings that satisfy it
certainty: number; // Confidence (0.0 to 1.0)
}

Example:

const response = await client.terms.createTerm({
sortId: employeeSortId,
ownerId: userId,
features: {
name: 'Bob',
salary: 85000,
},
});
if (response.witnessProofs) {
for (const proof of response.witnessProofs) {
console.log(`Witness ${proof.witnessId} satisfied with certainty ${proof.certainty}`);
console.log('Bindings:', proof.bindings);
}
}

Residuation is the suspension of computation when information is missing. It is not deferral, lazy evaluation, or an error. It is the system’s way of saying “I cannot determine this yet, but I will revisit it when the missing information arrives.”

When witnesses residuate, TermResponse includes residual witness objects:

interface ResidualWitnessDto {
witnessId: string; // The witness that could not be satisfied
partialBindings: Record<string, string>; // Bindings found so far
trigger: string; // What would trigger re-evaluation
}
const response = await client.terms.getTerm(termId);
if (response.state === 'residuated' && response.residualWitnesses) {
for (const residual of response.residualWitnesses) {
console.log(`Witness ${residual.witnessId} is waiting for: ${residual.trigger}`);
console.log('Partial bindings:', residual.partialBindings);
}
}

Only the provided features are updated; omitted features remain unchanged:

const updated = await client.terms.updateTerm(termId, {
features: {
salary: 95000,
department: 'Engineering',
},
});
const exists = await client.terms.termExists(termId);
// returns true or false
await client.terms.deleteTerm(termId);

Create multiple terms in a single request:

const result = await client.terms.bulkCreateTerms({
terms: [
{
sortId: personSortId,
ownerId: userId,
features: { name: 'Alice', age: 30 },
},
{
sortId: personSortId,
ownerId: userId,
features: { name: 'Bob', age: 25 },
},
],
});
// result.termIds: string[] — UUIDs in the same order as the request
// result.processingTimeMs: number

Features on TermDto use the tagged ValueDto format. The 10 variants are:

Variant Builder JSON
String "hello" (plain) {"type": "String", "value": "hello"}
Integer 42 (plain) {"type": "Integer", "value": 42}
Real 3.14 (plain) {"type": "Real", "value": 3.14}
Boolean true (plain) {"type": "Boolean", "value": true}
Uninstantiated null (plain) {"type": "Uninstantiated"}
Reference Value.reference(uuid) {"type": "Reference", "value": "uuid"}
List [...] (plain) {"type": "List", "value": [...]}
FuzzyScalar Value.fuzzyScalar(0.5, 0.8) {"type": "FuzzyScalar", "value": {...}}
FuzzyNumber Value.fuzzyNumber(shape) {"type": "FuzzyNumber", "value": {"shape": {...}}}
Set Value.set(lower, upper) {"type": "Set", "value": {"lower": [...], ...}}

For a detailed comparison of tagged vs. untagged formats, see Values and Formats.

The inference surfaces (getRules(), forward and backward chaining, fact listings) return a PsiTermDto whose features use the untagged FeatureValueDto read format, exactly as the engine sends it. An object value is one of five shapes:

Shape Meaning
{term_id, var_name} A logic variable (?N)
{term_id, literal} An interned literal, such as a guard’s bound
{term_id, sort_id, sort_name, features, ...} A whole term, inlined
{sort_ref, sort_name?} A sort named as a value
{term_id} A reference to another term

A sort value used to arrive as {term_id}. It now arrives as {sort_ref, sort_name?}; sort_name is absent when your lattice does not hold the sort. Use isSortRefFeatureValue() to tell a sort from a term, and discriminateFeatureValue() to narrow an unknown value.

The write format is different ({ sortRef }, { termId }), so convert a listed value before you send it back — for example to rebuild a rule for replaceRule:

import { featureValueToInput } from '@kortexya/reasoninglayer';
const features = Object.fromEntries(
Object.entries(rule.head.features).map(([name, v]) => [name, featureValueToInput(v)]),
);

A rule’s certainty is not a head feature; read RuleEntryDto.certainty. The display text shows every data feature, sorted by name, in full.