Scrydon
Authoring: Ontologies

Ontologies

Author Ontology packs — typed Object Types, Link Types, Action Types, identity rules, and bindings

This artifact ships inside a Pack. For the shared lifecycle — install, pack build, upload — see Packs & Authoring SDK.

An Ontology is the type system your tenant runs on: Customer, Asset, Risk, Incident, the relationships between them (owns, affects, mitigates), and the typed actions you can take against them (AssignAsset, AssessRisk). The Authoring SDK lets you ship that as a versioned, validated Ontology pack.

An ontology is the type system. A knowledge base is the content that uses it. They are different things — one ontology, many knowledge bases. Multiple ontologies can run side-by-side in one tenant (default + per-domain + pack-imported).

Install

bun add -d @scrydon/sdk-authoring zod
npm install --save-dev @scrydon/sdk-authoring zod
import {
  defineOntology,
  defineObjectType,
  defineLinkType,
  defineActionType,
  defineIdentityRule,
  defineBinding,
  defineProperty,
} from '@scrydon/sdk-authoring/ontologies'

What lives in a pack

ElementPurpose
Object TypeA typed entity — Customer, Asset, RiskFinding. Has properties, a kind (analytical / transactional / binding_only).
Link TypeA typed relationship — affects(RiskFinding → Customer). Cardinality + optional reverse traversal name.
Action TypeA server-side mutation gated by OPA. Carries a Zod input schema and a side-effect descriptor (object_edit / link_edit / outbox / webhook).
Identity RuleHow instances are de-duplicated. Deterministic match keys, optional probabilistic ER (signals + thresholds).
BindingBridges an upstream source (silver_table, memex_page, process_flow_task, process_flow_action, extraction, stream, manual) into typed Object or Link instances.

A complete example

import {
  defineOntology,
  defineObjectType,
  defineLinkType,
  defineActionType,
  defineIdentityRule,
  defineProperty,
} from '@scrydon/sdk-authoring/ontologies'
import { z } from 'zod'

export default defineOntology({
  id: 'acme-crm',
  version: '1.0.0',
  displayName: 'ACME CRM',
  description: 'Salesforce-style CRM ontology',
  maturity: 'preview',
  author: { name: 'Acme Inc.' },

  objectTypes: [
    defineObjectType({
      slug: 'Account',
      displayName: 'Account',
      kind: 'transactional',
      properties: [
        defineProperty({
          slug: 'name',
          displayName: 'Legal name',
          scalarType: 'string',
          cardinality: 'required',
        }),
        defineProperty({
          slug: 'jurisdiction',
          displayName: 'Jurisdiction',
          scalarType: 'string',
          classification: 'internal',
        }),
        defineProperty({
          slug: 'riskTier',
          displayName: 'Risk tier',
          scalarType: 'string',
          piiFlags: ['regulatory'],
        }),
      ],
    }),
    defineObjectType({
      slug: 'Asset',
      displayName: 'Asset',
      kind: 'transactional',
      properties: [
        defineProperty({ slug: 'name', displayName: 'Name', scalarType: 'string' }),
        defineProperty({ slug: 'value', displayName: 'Value', scalarType: 'float' }),
      ],
    }),
  ],

  linkTypes: [
    defineLinkType({
      slug: 'owns',
      displayName: 'Owns',
      sourceTypes: ['Account'],
      targetTypes: ['Asset'],
      cardinality: '1-N',
      reverseName: 'ownedBy',
    }),
  ],

  actionTypes: [
    defineActionType({
      slug: 'AssignAsset',
      displayName: 'Assign asset to account',
      subjectTypeSlug: 'Asset',
      inputZod: z.object({ accountId: z.string().uuid() }),
      sideEffectDescriptor: { kind: 'link_edit' },
    }),
  ],

  identityRules: [
    defineIdentityRule({
      objectTypeSlug: 'Account',
      deterministicKeys: ['name', 'jurisdiction'],
    }),
  ],

  bindings: [],
})

Property model

FieldNotes
scalarTypeOne of string, int, float, bool, uuid, timestamp, date, json, bytes
cardinalityrequired, optional, or many
classificationpublic, internal, confidential, restricted — flows into DLP and RBAC
piiFlagsFree-form tags surfaced to DLP redaction at retrieval time

Object Type kinds

  • analytical — bulk facts, query-heavy. Backed by a silver_table binding.
  • transactional — editable entities (manual entry, process-flow output). Postgres-native.
  • binding_only — read-through projection only (e.g. memex_page, extraction). Never directly edited.

Bindings — the seven kinds

KindBridges
silver_tableA bronze/silver analytics table → an Object Type
memex_pageA Memex KB content node (slug/alias match) → an Object instance
process_flow_taskA process_instance_task row → a typed Object instance
process_flow_actionA process_instance_task_action of actionType=entity_link → a typed Link instance
extractionThe 5-WF analytics extraction pipeline → an Object instance
streamA streaming source (Kafka, webhook fanout) → an Object instance
manualDirect workbench entry → an Object instance
defineBinding({
  objectTypeSlug: 'Account',
  kind: 'silver_table',
  source: { table: 'silver_account' },
  identitySpec: { keys: ['external_id'] },
  columnMap: { name: 'legal_name', jurisdiction: 'country_code' },
})

Entity collapse — identitySpec.entityColumns

Observation-heavy sources (position tracks, sensor readings, log events) often carry millions of rows for a few thousand real-world things. Declaring entityColumns — a strict subset of identityColumns naming the persistent entity being observed — tells the graph to render one node per entity with an exact observation count, instead of one node per row:

defineBinding({
  objectTypeSlug: 'AircraftPosition',
  kind: 'silver_table',
  source: { table: 'adsb_positions' },
  identitySpec: {
    identityColumns: ['icao24', 'seenAt'],  // one observation (position fix)
    entityColumns: ['icao24'],              // the persistent thing (airframe)
  },
  columnMap: { icao24: 'icao24', seenAt: 'seenAt', latitude: 'lat', longitude: 'lon' },
})

Effects on the graph page:

  • One node per aircraft, labeled with an exact N observations count computed at read time under your permissions — never a sample presented as the data.
  • Relationship joins on entity-level columns (e.g. icao24) connect at the entity level.
  • Every graph response reports shown / total per type; anything the readability budget hides is represented by an aggregate node carrying the exact remainder count.
  • The overview renders the connected fabric: instances that participate in at least one relationship. Instances with no computed relationship are not scattered across the canvas as isolated dots — they are folded into their type's aggregate node (with the exact count) and remain browsable through type expansion and search. An ontology with no computable relationships at all falls back to a per-type sample so the page stays browsable.
  • Aggregate nodes are click-to-expand: each click pages a bounded set of that type's members onto the canvas (tethered around the aggregate) and the remaining count decrements exactly; when nothing remains the aggregate disappears.
  • Expand neighbors shows what is connected first — neighbors grouped by relationship and target type with counts — so you add the groups you care about instead of flooding the layout.
  • The Map view (toggle at the bottom of the graph page) renders coordinate-bearing types as map cells carrying exact in-view counts under your permissions. Zooming subdivides cells; when a type's in-view total is small enough, the real instances render instead. Truncated cell lists are labeled as lower bounds — never presented as final numbers.
  • Relationships render on the Map view too: reference link types with an explicit join rule draw arcs between cells, each carrying the exact number of linked pairs between those two areas (hover to see it); proximity links draw thin connectors among the instances currently shown, and the view states that they cover the shown instances only — zoom in to materialize the rest. Each link type reports its in-view pair total alongside the type totals.

Validation: entityColumns must be a non-empty strict subset of identityColumns (equal sets are rejected — omit entityColumns instead). Pack install rejects manifests that violate this.

Graph visualization

Dense packs can include optional graph visualization hints. These hints do not change the ontology schema; they tell graph surfaces how to render an instance view efficiently.

Use mode: 'compact' when a pack has one high-value root Object Type and many high-cardinality child bindings. The graph endpoint projects only the root binding, renders aggregate nodes per root instance, and skips row projection for collapsed Object and Link bindings.

defineOntology({
  id: 'acme-docs',
  version: '1.0.0',
  displayName: 'ACME Documents',
  description: 'Document intelligence ontology',
  maturity: 'preview',
  author: { name: 'Acme Inc.' },

  visualization: {
    graph: {
      mode: 'compact',
      rootObjectType: 'DocumentCorpus',
      rootLimit: 100,
      rootGroup: {
        slug: 'DocumentCorpusIndex',
        label: 'All document corpora',
        edgeLabel: 'contains corpus',
      },
      collapsedObjectTypes: ['Document', 'DocumentSection', 'ExtractedEntity'],
      collapsedLinkTypes: ['ContainsDocument', 'MentionsEntity'],
      aggregates: [
        {
          slug: 'Document',
          label: 'Documents',
          linkSlug: 'ContainsDocument',
          edgeLabel: 'contains documents',
          countObjectTypes: ['Document'],
        },
        {
          slug: 'ExtractedEntity',
          label: 'Extracted entities',
          linkSlug: 'MentionsEntity',
          edgeLabel: 'mentions entities',
          countObjectTypes: ['ExtractedEntity'],
          countLinkTypes: ['MentionsEntity'],
        },
      ],
    },
  },

  objectTypes: [],
  linkTypes: [],
  actionTypes: [],
  identityRules: [],
  bindings: [],
})

Identity Rules — deterministic + probabilistic

Identity rules tell the platform how to merge instances. The deterministic path is required; the probabilistic path is opt-in for fuzzier sources.

defineIdentityRule({
  objectTypeSlug: 'Account',
  deterministicKeys: ['external_id', 'name + jurisdiction'],
  probabilistic: {
    blockingKeys: ['name'],
    signals: [
      { name: 'name_levenshtein', weight: 0.6, on: 'name' },
      { name: 'jurisdiction_match', weight: 0.4, on: 'jurisdiction' },
    ],
    thresholds: { auto: 0.85, review: 0.70 },
  },
})

Action Types — server-side, OPA-gated mutations

Action Types are the only typed write path. Every action runs the same way:

  1. Zod-validate the input against inputZod.
  2. OPA evaluates policyRegistry.evaluate({ resource_type: 'action', action: <slug>, ... }).
  3. The side-effect descriptor decides what executes — direct object/link edit, an outbox dispatch, or a webhook.
  4. Audit row is written with bundleHash + policy_execution_grant.grantId.
sideEffectDescriptor.kindWhat happens
object_editUpsert into the Object Type's transactional store
link_editUpsert into the Link Type's edge store
outboxEnqueue a transactional outbox event
webhookFire a configured webhook (URL in config)

Maturity

Pick one — surfaced as a badge in the workbench:

  • verified — production-ready, owned and maintained
  • preview — feature-complete, may evolve
  • experimental — early access, expect breaking changes

Validation

The pack is validated against OntologyManifestSchema (Zod) at build time and again on the server at upload time. Common failures:

ErrorCause
id must be lowercase kebab-casePack id must match /^[a-z][a-z0-9-]*$/
Slug must start with a letter, alnum + _ -Object/Link/Action/Property slug failed regex
Must be a valid semver stringversion is not semver
linkType source/target slug not foundsourceTypes / targetTypes reference an unknown Object Type slug

Pack-as-integration-bundle (today)

Ontology packs ship inside an integration bundle via definePackOntology — the registry layer dispatches packs the same way it dispatches vendors. A standalone scrydon-ontologies CLI is on the roadmap; until then, follow the Integrations build flow with a top-level definePackOntology(...) export.

import { definePackOntology } from '@scrydon/sdk-authoring/integrations/define'
import myCrmOntology from './crm.ontology'

export default definePackOntology({
  id: 'acme-crm',
  version: '1.0.0',
  displayName: 'ACME CRM',
  description: 'Salesforce-style CRM ontology',
  maturity: 'preview',
  author: { name: 'Acme Inc.' },
  objectTypes: myCrmOntology.objectTypes,
  linkTypes: myCrmOntology.linkTypes,
  actionTypes: myCrmOntology.actionTypes,
  bindings: myCrmOntology.bindings,
  identityRules: myCrmOntology.identityRules,
})

Where to next

On this page

On this page