Skip to content

Ontology starters (experimental)

View Markdown

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.

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" }

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.

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.

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 Issue

dryRun 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:

Terminal window
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.

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 }

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.iri
crm.relations.WORKS_AT.inverse_iri
crm.properties.close_date.value_type # "date_time"

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 }.

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