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. schema.declare and
schema.publish change main directly and are allowed only for organisation administrators; other
callers create a branch (branch.create), edit it (branch.declare) and submit it for review
(branch.submit) — see Branches and proposals. 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.
Each objects.query set reports matched, the number of objects it returns, and scanned, the
number of rows the service read to find them. A bound source that cannot be read fails the query
with SOURCE_READ_UNAVAILABLE; the service never answers from the ontology ledger instead. The
result still carries sourceFallbackToLedger, which current servers always return as false: treat
true (from an older server) as ledger data, never as source evidence. A set does not say whether
the scan of a bound source stopped before the end of that source. That signal stays inside the
service, where watch evaluation uses it to refuse to score a watch on a partial read: the watch's
outcome from watches.evaluate carries a reason such as incomplete-read instead of a detection.
A short or empty set is therefore not proof that the source holds nothing more; narrow the query with
filter or objectTypes before treating it as final.
App discovery
ontology.apps.list({ view: "for-me", search: "north sea", limit: 24 }) reads
the current published app catalog. Views are for-me, all, extensions, and
mine; every view enforces discovery audience, clearance and tenant policy.
Pass the returned nextName as afterName to continue. Search is literal and
case-insensitive; limits are bounded to 50 entries.
ontology.apps.get({ name: "north-sea" }) returns the same safe metadata for
one discoverable app. Neither method returns notebook source, artifact pins,
release permits, audience lists or retained releases. canRequestRun describes
the current run audience; it is not a runtime admission or data-access grant.
These reads require a direct human viewer, including verified service forwarding
on that viewer's behalf. Browsing starts no notebook compute.
ontology.apps.runRelease is reserved for the Analytics control plane. It
resolves immutable runtime material under the operator's current app access;
its private result stays on the server. Ordinary SDK callers and Horizon cannot
use this method to obtain notebook artifacts. A release pin alone grants no
runtime or source-data access.
ontology.apps.previewQuery is a private Analytics control-plane read for an
unpublished candidate. It uses the author's current ontology permissions and
the candidate's declared queries and links. It cannot publish or execute actions,
and supplying a candidate does not grant data access. Its private source proof
must stay on the server. This method alone does not start or admit a visual
preview; notebook authors should not call it directly.
ontology.apps.previewWaitQueries is the corresponding private, bounded source
invalidation wait. A changed query is read again under current policy; changed
source or policy proof requires a fresh snapshot. Both methods keep physical
source proofs server-side. Analytics issues opaque, frame-bound browser cursors.
The browser subpaths apps/session-controller, apps/session-frame, and
apps/widget-queries share bounded session expiry, opaque-frame removal and the
widget read/wait scheduler between Analytics preview and Horizon. Their transport
callbacks must resolve current server authority; these helpers grant none.
Notebook authors use the Python components and the Analytics preview control.
ontology.apps.readResponse is also reserved for Analytics. It takes the saved
release pin, declared query and watch IDs, and a qualified detection reference.
It returns a private dispatch association only while the detection remains
visible and its watch assertion is valid and readable. A missing or ambiguous
association returns response: null. Its result is not a flow access grant:
Horizon must separately check the actual response through Agentic before
revealing its instance ID. Notebook authors cannot provide a response URL or
use this control-plane method directly.
App publication history and recovery
Authorized publishers can read app release summaries with
ontology.apps.history({ name: "north-sea", limit: 25 }). The response separates
current lifecycle state from distinct retained releases. Pass nextReleaseId
as beforeReleaseId for the next page. These reads require publication permission;
they are not the Horizon viewer catalog and expose no artifact locations or permits.
If a publication response is lost, the Analytics authoring service can call
ontology.apps.publicationResult with the original preparation/request UUIDs,
candidate hash, expected lifecycle assertion and release notes. A committed
result proves that request committed, even if a later release or withdrawal is
now current. rejected carries only a safe error code. unknown does not prove
failure: the request may still be in flight. Reconciliation performs no write
and does not renew validation evidence. Keep the original request identity;
never replace an ambiguous attempt with a fresh publication request.
Submit a built-in kernel action by its constant rather than a string literal, so a typo fails the
type-check instead of the request. KERNEL_ACTION_TYPE_NAMES (with its union type
KernelActionTypeName and the isKernelActionTypeName guard) is exported from
@scrydon/ontology-sdk/client; the kernel's own action registry is keyed by the same object, and
the generated list below is read from it. A published SCHEMA_ACTION may not reuse a built-in name.
import { KERNEL_ACTION_TYPE_NAMES } from "@scrydon/ontology-sdk/client";
await ontology.actions.submit({
actionType: KERNEL_ACTION_TYPE_NAMES.OBJECT_ASSERT,
idempotencyKey,
input: { objects: [/* ... */] },
});Client namespaces
| Namespace | Methods |
|---|---|
actionApprovals | list |
actionCatalog | list |
actionHistory | get, list |
actions | prepare, submit |
actionTypes | get, list |
aliases | list, resolve |
apps | actionResult, authoringQuery, get, history, list, previewQuery, previewWaitQueries, publicationResult, readQuery, readResponse, revalidate, revalidationCandidate, revalidationTargets, rollbackCandidate, runRelease, submitAction, waitQueries |
bindings | count, enroll, get, instances, list, materialize, materializeByObjectType, readiness, readinessOf, resolveByObjectType, resume, table |
branches | archive, create, declare, get, list, manifest, publish, rebase, submit |
decisions | citing, get |
derivations | freshness |
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 |
outboundActionStatus | get |
proposals | get, list |
rdf | export, import |
revisions | resolve |
riskAssessments | evaluate |
search | query |
taxonomies | get, import, list, listTerms, publishKinds, retractTerms, revisions, rollback, writeTerms |
types | get, list |
watches | dispatches.claim, dispatches.settle, evaluate, list |
Built-in action types
action.approval.approveaction.approval.denyaction.catalog.disableaction.catalog.enableaction.outbound.reconcileaction.tasking.dispatchapp.publishapp.revalidateapp.rollbackapp.withdrawbranch.archivebranch.createbranch.declarebranch.publishbranch.rebasebranch.submitobject.assertobject.retractoperational.decisionpack.record-installproposal.approveproposal.rejectrisk.assessschema.declareschema.publishwatch.detect
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 |
Decisions that cite a detection
Every operational decision taken in a case that a watch detection opened (a map verdict, an approval, a rejection) cites that detection as evidence in the decision ledger. The decision takes the detection's marking: a decision made on a restricted detection is itself restricted, and a reader cleared below it does not receive the row.
The citation is made when the decision is delivered, as the person who decided. These limits follow from that:
- If that person cannot read the detection, the decision is still recorded, without the citation, and so at the marking of whatever else it cites.
- If the detection cannot be read because the read failed, the delivery is retried instead of recorded uncited. A failure that retrying cannot fix (an expired token, an unreadable answer) is retried a bounded number of times and then dead-lettered with a reason.
- A decision cited at a restricted marking can only be revised by someone cleared at
least as high. A lower-cleared reviser is refused (
FORBIDDEN) and the original decision stays in force. - A decision that already cites the maximum number of objects cannot also cite the detection. It is refused and dead-lettered with a reason, never recorded at a lower marking.
- The detection is looked up from the live case when the decision is delivered. If the case was deleted first, the decision is recorded without the citation.
A case that no detection opened is unchanged.
decisions.citing answers "which decisions were taken on this alert?":
const page = await kernel.decisions.citing({
objectType: "MaritimeDetection",
objectId: detectionId,
limit: 20,
});
// page.items: decision, rationale, actor, decidedAt, supersedes, marking.
// Newest first. Follow page.nextCursor while page.hasMore is true.limit defaults to 20 and is at most 100. A decision that revises an earlier one
names it in its own supersedes. Decisions recorded before this change cite
no detection and are not returned.
Watch dispatch settlement
For an authorized dispatch worker, watches.dispatches.settle reports each
claimed obligation using its eventId, attemptCount and the claim's
leaseOwner. A delivered result now requires the actual instanceId returned
by the flow engine. It accepts at most 256 characters and rejects NUL or unpaired
surrogates. A retry or dead_letter result may include a bounded error and must
omit instanceId. Missing or invalid delivery evidence is rejected.
The kernel stores the response association atomically with delivery settlement. An expired lease, stale attempt or foreign tenant cannot settle the obligation. The association survives queue cleanup, but does not grant access to its flow. Coordinate SDK, platform dispatcher and ontology receiver upgrades; do not report delivery when an older component refuses the new contract.
Agentic records the response once per tenant, ontology and detection identity. Retries return that same instance after completion, cancellation or deletion; they do not start another response. A changed watch or response template is refused for an already recorded identity. A new detection episode requires a new detection identity. The returned ID proves dispatch, not current access or continued existence of the instance.
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, source table has no markings column — a governed read cannot filter its rows by marking, or source table's markings column is not an array of marking slugs — a governed read cannot filter its rows by marking. 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.