Ontology starters (experimental)
A starter is a versioned base ontology for one domain. A graph starts from
it, and every connector of that domain maps its records to it. An app you
build on Organization, Person, Deal and Stage then works for each
customer, whether their CRM is HubSpot or Salesforce. Each customer’s own
fields land as extensions beside the base.
Starters share their core terms. A HubSpot contact and a Linear user can be
the same Person, so one graph can hold several starters.
The three starters
Section titled “The three starters”| Starter | Version | First source | Classes | Properties | Relations | Questions |
|---|---|---|---|---|---|---|
crm |
1.1.0 | HubSpot | 20 | 71 | 16 | 30 |
documents |
1.1.0 | SharePoint, Google Drive | 9 | 39 | 8 | 16 |
work |
1.1.0 | Linear | 14 | 54 | 19 | 18 |
Version 1.1.0 adds terms the connectors need. Deal gets won_reason.
User gets source_user_id, the person’s user id in the source tool. In
Work management, Iteration lists completed_at. A graph that holds 1.0.0
shows the starter as partly applied, and Apply adds the new terms.
CRM. Party, with Organization, Person (and its subclass User, a
person with an account) and Lead. Then Team, Task, Deal, Pipeline,
Stage, Ticket, Activity (with Call, Email, Meeting and Note),
Product, LineItem, Quote and Campaign. Relations include WORKS_AT,
OWNED_BY, IN_STAGE, IN_PIPELINE and FOR_ORGANIZATION.
Documents. Site, Library, Folder, Document and its subclass
Page, with Tag, Person and User. Relations include IN_SITE,
IN_LIBRARY, IN_FOLDER, CREATED_BY, MODIFIED_BY, TAGGED_WITH and
LINKS_TO.
Work management. Task and its subclass Issue, Project, Milestone,
Iteration and Comment, with the core Team, Pipeline, Stage, Tag,
Organization, Person and User. Relations include IN_PROJECT,
SUBTASK_OF, BLOCKS, ASSIGNED_TO and IN_STAGE.
Every relation has a cardinality, and every one but the symmetric
RELATED_TO has an inverse name. WORKS_AT goes from a Person to an
Organization, and its inverse EMPLOYS is a predicate of its own in SPARQL. Each starter also lists competency questions: the
questions its ontology answers, with the terms an answer needs and the SPARQL
query that answers it. A query runs as it is: it matches a class with
a/rdfs:subClassOf*, so a User counts as a Person without an entailment
regime. A question about one record or one period takes its input in a
VALUES block of UNDEF; as written, the query answers for every record.
Replace UNDEF with a value to ask about one:
# Input: an organization's name. UNDEF answers for every organization.VALUES ?organization { "Acme Corp" }Status
Section titled “Status”Nothing records which starter a graph uses. The status compares the starter’s terms with the graph’s ontology each time you ask:
absent: the graph holds none of the starter’s classes and relations. Properties do not count, because starters share them.partial: the graph holds some of the starter, for example an older version, or a class you declared yourself.applied: applying the starter would change nothing.
adds counts what applying would add now: classes, parent links,
properties, relations and widened relations. conflicts lists each term the
graph holds differently: a property with another value type, a relation with
another cardinality or inverse, a class with a parent the starter does not
declare, or a removed class or relation.
In the console
Section titled “In the console”Open Knowledge graph → Starters to see each starter’s status on the graph and apply one. When you create a graph, Start from a starter applies one to the new graph.
Apply a starter
Section titled “Apply a starter”Applying adds what the graph lacks in one new ontology version. A relation
the graph already has gains the starter’s classes instead: apply crm, then
work, and IN_STAGE goes from deals and tickets to tasks as well. Applying
again changes nothing and answers no_op: true.
import { LbbClient } from "@littlebigbrain/client";
const lbb = new LbbClient({ baseUrl: process.env.LBB_URL!, apiKey: process.env.LBB_API_KEY!,});const sales = lbb.graph("sales");
const preview = await sales.ontology.starters.apply("crm", { dryRun: true });console.log(preview.applied_ops.length, preview.ontology_version); // 115 2
const applied = await sales.ontology.starters.apply("crm");console.log(applied.status.state); // "applied"
const work = await sales.ontology.starters.apply("work");// work.applied_ops widens IN_STAGE with Task and FOR_ORGANIZATION with IssuedryRun answers the same without writing. expectedOntologyVersion refuses
with 409 conflict when the graph’s ontology moved since you read it.
A conflicting term makes apply refuse with 409 starter_conflict. Nothing is
written. The error lists each conflict:
try { await lbb.graph("support").ontology.starters.apply("work");} catch (error) { // error.code === "starter_conflict" // error.details.conflicts[0]: { kind: "property", name: "priority", // starter: "keyword", graph: "i64", message: "…" }}Python has the same calls on the graph’s ontology:
from lbb import LbbClient
with LbbClient(base_url, api_key=api_key) as lbb: sales = lbb.graph("sales").ontology.starters print([(s.id, s.status.state.value) for s in sales.list().starters]) applied = sales.apply("crm") print(applied.ontology_version, applied.status.state.value)Over HTTP:
curl --fail-with-body -sS -X POST "$LBB_URL/v1/ontology/starters/apply?graph=sales" \ -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \ -d '{ "starter": "crm", "dry_run": true }'GET /v1/ontology/starters and GET /v1/ontology/starters/detail?starter=crm
answer for a graph that does not exist yet: every starter is absent and
ontology_version is null. Apply needs the graph and answers
404 graph_not_found without it.
From an agent
Section titled “From an agent”The MCP server’s lbb_configure tool lists and applies starters:
{ "action": "list_starters", "graph": "sales" }{ "action": "apply_starter", "graph": "sales", "starter": "crm", "dry_run": true }Typed terms
Section titled “Typed terms”Both SDKs carry each starter’s terms as constants, with the IRIs SPARQL uses for them. A query then names its classes and relations without string typos:
import { starters } from "@littlebigbrain/client";
const { crm } = starters;const label = "http://www.w3.org/2000/01/rdf-schema#label";const { rows } = await lbb.graph("sales").query.sparql({ query: `SELECT ?name ?org WHERE { ?person a <${crm.classes.Person.iri}> ; <${label}> ?name ; <${crm.relations.WORKS_AT.iri}> ?o . ?o <${label}> ?org . } ORDER BY ?name`, entailment: "subclass",});// [{ name: "Ada Lovelace", org: "Analytical Engines Ltd" },// { name: "Grace Hopper", org: "Analytical Engines Ltd" }]With entailment: "subclass", a User such as Grace Hopper is also a
Person.
crm.classes.Organization.iri is https://littlebigbrain.com/class/organization,
crm.properties.domain.iri is https://littlebigbrain.com/p/domain with
valueType keyword, and crm.relations.WORKS_AT.inverseIri is
https://littlebigbrain.com/r/employs. crm.questions lists the competency
questions with their id, text and sparql.
In Python the constants are in lbb.starters:
from lbb.starters import crm
crm.classes.Organization.iricrm.relations.WORKS_AT.inverse_iricrm.properties.close_date.value_type # "date_time"New versions arrive as suggestions
Section titled “New versions arrive as suggestions”A graph that holds part of a starter can ask for the rest as one
ontology suggestion, for a person to
review. The suggestion is keyed starter:<id>/<version>, so asking again
returns the same one:
const { suggestion_id, created } = await sales.ontology.starters.update("crm");An absent starter answers 409 starter_not_applied: apply it instead. An
applied one answers { suggestion_id: null, created: false }.
Routes
Section titled “Routes”| Route | Does |
|---|---|
GET /v1/ontology/starters |
every starter with its status on the graph |
GET /v1/ontology/starters/detail?starter= |
one starter’s document, status and missing operations |
POST /v1/ontology/starters/apply |
add what the graph lacks; dry_run writes nothing |
POST /v1/ontology/starters/update |
file what the graph lacks as one suggestion |