Ontology Kernel SDK
Typed, request-scoped access to Scrydon's workspace-environment ontology kernel.
Ontology Kernel SDK
Use createKernelClient from @scrydon/ontology-sdk/client in server-side callers. The client is
request scoped: provide the admitted caller transport plus the exact organization, workspace, and
workspace-environment headers for the current request. Never cache user headers, expose service
credentials to browser code, or substitute an organization-only scope.
import { createKernelClient } from "@scrydon/ontology-sdk/client";
const ontology = createKernelClient({
baseUrl: `${ontologyServiceUrl}/api/ontology`,
headers: () => ({
...admittedCallerHeaders(),
"x-organization-id": organizationId,
"x-workspace-id": workspaceId,
"x-workspace-environment-id": workspaceEnvironmentId,
}),
});
const result = await ontology.objects.query({
ontology: "crm",
objectTypes: ["Customer"],
limit: 50,
});Published reads resolve the current revision unless you pass an immutable revisionId; branch
reads use a branch apiName. Writes use actions.submit with a final action type such as
schema.declare, schema.publish, branch.submit, or proposal.approve. Errors are
OntologyApiError values with a generated stable code, httpStatus, and bounded details.
A non-2xx answer that carries no kernel error envelope — a proxy's HTML 502, an empty 401 from the
mesh — is also an OntologyApiError: its code is MALFORMED_ERROR_RESPONSE, httpStatus is the
status that was returned, and details holds the response content type plus a bounded body preview.
Client namespaces
| Namespace | Methods |
|---|---|
actions | submit |
actionTypes | get, list |
aliases | list, resolve |
bindings | count, enroll, get, instances, list, materialize, materializeByObjectType, readiness, readinessOf, resolveByObjectType, resume, table |
branches | archive, create, declare, get, list, manifest, publish, rebase, submit |
decisions | get |
extensions | apply, autoInstall, installed |
geo | layers |
graph | expand, instanceGraph, layout, layoutRun, layoutStatus, neighbors, schemaSlice, search, trail, viewport |
links | list |
linkTypes | get, list |
mappings | suggest |
objects | get, list, neighbors, query |
ontologies | list |
proposals | get, list |
rdf | export, import |
revisions | resolve |
search | query |
types | get, list |
watches | dispatches.claim, dispatches.settle, evaluate, list |
Error contract
| Typed error | HTTP |
|---|---|
APPROVAL_REQUIRED | 409 |
BRANCH_BASE_STALE | 409 |
CONFLICT_DETECTED | 409 |
EXPORT_DENIED | 403 |
FORBIDDEN | 403 |
HISTORICAL_SOURCE_UNAVAILABLE | 422 |
HUMAN_REVIEW_REQUIRED | 422 |
IDEMPOTENCY_CONFLICT | 409 |
IDEMPOTENT_REPLAY | 200 |
MIGRATION_REQUIRED | 422 |
NOT_FOUND | 404 |
PROJECTION_LAGGING | 503 |
RECOMMENDATION_STALE | 409 |
REVISION_MISMATCH | 409 |
RULES_REJECTED | 422 |
SHAPE_VIOLATION | 422 |
SOURCE_READ_UNAVAILABLE | 503 |
STALE_READ_SET | 409 |
UNAUTHORIZED | 401 |
VALIDATION_FAILED | 400 |
Capability gaps, outages, and the kernel's "not yet"
Three families of rejection look alike on the wire and must be handled differently. The SDK
exports one helper per family so callers never re-derive the rule from raw details.
A binding that cannot produce rows
bindingReadCapabilityReason(error) answers why a binding read cannot return a page, or null
when the error is a genuine fault to surface as one. The verdict is keyed on
details.reasonCode — never on whether a human-readable details.reason happens to be present:
| Wire shape | Meaning | Helper returns |
|---|---|---|
VALIDATION_FAILED (400) with details.reasonCode: "BINDING_KIND_NOT_GOVERNED" | The binding kind has no governed reader (memex_page today). Permanent for this release; retrying never helps. | the server message — binding kind has no governed reader |
SOURCE_READ_UNAVAILABLE (503) with details.reasonCode: "BINDING_SOURCE_NOT_PROVISIONED" (plus details.reason when the kernel has a sentence) | The source is not provisioned yet — e.g. Table "aircraft_position" not registered — upload a CSV with that name in /tables. Only an operator can clear it. | details.reason, or the server message when the kernel sent no sentence |
Any other shape, including a SOURCE_READ_UNAVAILABLE (503) carrying only free-text details.reason | A real outage. Retry it. | null |
BINDING_READ_REASON_CODES is the exported constant object (KIND_NOT_GOVERNED,
SOURCE_NOT_PROVISIONED); branch on it rather than on the literal, and use
isBindingReadReasonCode(value) when narrowing a code that arrived as unknown. A graph or list
surface should degrade around the first two rows (show the binding as skipped with the reason)
and render only the third as an error.
A 503 with a details.reason but no details.reasonCode is an outage, not a capability gap.
Earlier releases classified on the presence of the sentence, which meant any 503 that happened to
carry diagnostic text stopped being retried. The code is the contract; the sentence is a courtesy
for whoever reads the screen.
Waiting for a projection
The kernel is event-sourced: a write is acknowledged when its ledger row commits, and reads see it
once the projector has caught up. A read issued immediately after a write can therefore answer
NOT_FOUND or PROJECTION_LAGGING for a resource that exists. isTransientProjectionError(error)
is true for exactly those two codes — the set is TRANSIENT_PROJECTION_ERROR_CODES — and is the
only predicate a bounded waiter should retry on. REVISION_MISMATCH is deliberately not in the
set: it means the caller's premise changed, and re-reading with the same premise is a spin, not a
recovery.
import { isTransientProjectionError } from "@scrydon/ontology-sdk/client";
const deadline = Date.now() + 5_000;
for (;;) {
try {
return await ontology.branches.get({ ontology, branch });
} catch (error) {
if (!isTransientProjectionError(error) || Date.now() >= deadline) throw error;
await new Promise((resolve) => setTimeout(resolve, 100));
}
}A link whose edges are computed, not stored
In graph.schema, a link with a declared join has no stored edge rows: its edges are joined from
the visible instances at read time. Such a link reports edgeCount: null with
reason: GRAPH_SCHEMA_LINK_REASONS.COMPUTED_FROM_DECLARED_JOIN, never edgeCount: 0 — a zero
would read as "bound, but no column values match", which is a different remedy. The full reason
vocabulary is GRAPH_SCHEMA_LINK_REASONS; a failed governed read keeps
SOURCE_COUNT_UNAVAILABLE.
Branch on the published constants rather than the raw strings — GRAPH_LINK_KIND for a link's
kind (REFERENCE / PROXIMITY) and GRAPH_DIAGNOSTIC_STATUS for a binding diagnostic's
status (READY / NOT_READY / SKIPPED). Both are exported from
@scrydon/ontology-sdk/client alongside their derived GraphLinkKind / GraphDiagnosticStatus
types, and the graph-slice schema's own z.enum(...) reads from them, so a retyped literal fails
the type-check instead of silently un-matching one branch of your UI.
GRAPH_LINK_KIND describes a link on the wire. Use LINK_TYPE_KINDS from
@scrydon/sdk-authoring/ontologies when you are discriminating a link type you authored in a
manifest — the two vocabularies happen to spell the same words today and are not interchangeable.