# Ontology starters (experimental)

Source: https://docs.littlebigbrain.com/guides/ontology-starters/

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

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

```sparql
# Input: an organization's name. UNDEF answers for every organization.
VALUES ?organization { "Acme Corp" }
```

## 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

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

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

```ts
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:

```ts
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:

```python
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:

```sh
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

The MCP server's `lbb_configure` tool lists and applies starters:

```json
{ "action": "list_starters", "graph": "sales" }
```

```json
{ "action": "apply_starter", "graph": "sales", "starter": "crm", "dry_run": true }
```

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

```ts
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`:

```python
from lbb.starters import crm

crm.classes.Organization.iri
crm.relations.WORKS_AT.inverse_iri
crm.properties.close_date.value_type  # "date_time"
```

## New versions arrive as suggestions

A graph that holds part of a starter can ask for the rest as one
[ontology suggestion](https://docs.littlebigbrain.com/guides/ontology-suggestions/), for a person to
review. The suggestion is keyed `starter:<id>/<version>`, so asking again
returns the same one:

```ts
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

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